Skip to content

Enable nullable annotations in language types in Scripting#351

Merged
slozier merged 1 commit intoIronLanguages:mainfrom
BCSharp:scripting_nullable_2
Apr 29, 2026
Merged

Enable nullable annotations in language types in Scripting#351
slozier merged 1 commit intoIronLanguages:mainfrom
BCSharp:scripting_nullable_2

Conversation

@BCSharp
Copy link
Copy Markdown
Member

@BCSharp BCSharp commented Apr 27, 2026

Activates creation of additional polyfills MaybeNullAttribute and NotNullWhenAttribute.

public Stream InputStream { get { InitializeInput(); return _inputStream; } }
public Stream OutputStream { get { InitializeOutput(); return _outputStream; } }
public Stream ErrorStream { get { InitializeErrorOutput(); return _errorStream; } }
public Stream InputStream { get { InitializeInput(); return _inputStream!; } }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this change is slated for later, but you could use [MemberNotNull(nameof(_inputStream), nameof(_inputReader), nameof(_inputEncoding))] on InitializeInput ans friends.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it, but to mark those member fields MemberNotNull, InitializeInput (and friends) would have to change to really guarantee to the compiler they are never null on return. This means that the conditions on line 103 and 105 would have to change to if (_inputStream is null || _inputEncoding is null || _inputReader is null). Since the first test (L103) is executed on every property access, it means that every time the test would take 3 times as much as before — just to make the compiler happy about nullability guarantees that are obvious for the human reading the code (and maybe to some AI agents 😄 ). In the end I thought it was not worth the change, and the null-forgiving operator is justified. What is nice is that the whole anaysis can be done locally; it does not require looking up any other source file.

Generally, I want to do changes file-by-file, in file groups, to make the review easier and for my own well-being, so there is nothing to be slated for later. If I submit a file for a review, that is it, if something looks questionable, suspicious, or unclear, fire right away.

/// it returns false.
/// </summary>
bool TryGetValue(out dynamic value);
bool TryGetValue(out object? value);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really know what the consequences of changing from dynamic to object are. If it wasn't an accidental change I assume you've thought it through and have a good reason for it. 😄

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about this one. The reason I changed it to object? is because SetValue below accepts object?. It seems that the compiler is totally happy with assignment of object to dynamic without casting, as well as dynamic to object without casting. So it treats them equivalently?

One can argue that dynamic has more capabilities that object so perhaps it should be treated as a specialized object (as if a subtype). We can decide on this as a rule — in such case dynamic? is more appropiate. If so, there are possibly more places where I need to change things (including future planned PRs).

In TryGetValue the type is out, and in SetValue it is in, so it is as if co-variant vs. variant typing, but because the type is actually not changed, it felt weird to set an object into the scope, just to retrieve it later magically as dynamic.

I need to sleep on this.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type conversion is neither clear not intuitive. Intuitively, the the type of object put into the scope should be the same as the type of the object retrieved from it. This means object/object or dynamic/dynamic. But there is no reason for the scope to hold onto the dynamic type; it doesn't perform any dynamic operations on it. Therefore type object seems more appropriate.

From the caller's perspective it changes nothing: you can still declare the retrieved value as dynamic if you want to. For instance, this works:

if (scopeVariable.TryGetValue(out dynamic? variable) {
    ...
}

and obtain a variable typed as dynamic.

So in the end, I would not make any changes here.

@slozier slozier merged commit 70ae0c0 into IronLanguages:main Apr 29, 2026
8 checks passed
@BCSharp BCSharp deleted the scripting_nullable_2 branch April 29, 2026 03:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants