Repository navigation
T | null | undefined accepts null, and an empty optional equals no value - #422
Merged
Merged
Conversation
…value - Strict null checks: `string | null | undefined` (and a class) became optional<string> without the null the checker needs, so null was a compile error. The null member now goes in before undefined wraps the type: optional<union<string, null>>. - Without strict null checks a nullable type still absorbs null, but a union of several types is a tagged value, not one pointer: `string | number | null` keeps null as a member (it was a verify error to store null in it). - `number | null | undefined` holding null crashed the lowering on `=== null`: LogicOp read the union tag with ExtractValueOp from a ts.union value. An optional's unwrapped value arrives unconverted. - An optional compared with a value was unwrapped without asking whether it holds one, so an empty `string | undefined` was `=== null` and an empty `boolean | undefined` `=== false`. The values are compared only when it holds one; loosely it still equals null. - A value cast to `T | null` is cast to T first: an object literal to `Interface | null`, a const array to `number[] | null`, were left for the lowering, which cannot convert them. - The `// @strict-null` pragma never reached the type helper's copy of the options, so unions were built strict in files compiled non-strict. The lowering-error test used `number[] | null = [1, 2]` to raise its error, which now compiles; it reads the array out of a `number[] | null` instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a value (#423) `let n: number | undefined = 0.5; n === 0` was true. The numeric sync in adjustTypesForBinaryOp looked for the first promotion type either operand had exactly; an optional never is one, so the other operand's type won - an integer literal's si32 - and the optional was cast out to it, 0.5 becoming 0. The same cast read an empty optional as 0: `undefined === 0`, `undefined < 1` were true. A comparison with one optional numeric side now widens both to the wider of the optional's element type and the other operand's type, and keeps the optional, so the lowering compares only when it holds a value (OptionalLogicHelper). `-` and `*`, which share the case, are left as they were. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
T | null | undefinednow acceptsnull, and an optional that holdsundefinedno longer compares equal tonull,false,""or0-like values of its own type. Three bugs were reported; fixing them needed three more.The three bugs
1. Strict null checks (the default):
string | null | undefinedrejectednull.string | nullisunion<string, null>, thenullbeing there only for the checker. With| undefined, the single-type branch ofgetUnionTypeMergeTypesreturnedoptional<string>before thatnullwas added. Thenullnow goes in first:optional<union<string, null>>. Classes had the same bug.2. Without strict null checks:
string | number | nullcouldn't storenull.Next to a nullable type (string, a class),
nullis dropped, because the pointer holds it. A union of several types is a tagged value, not one pointer, sonullnow stays as a member when more than one type is left after merging.3.
number | null | undefinedholdingnullcrashed the lowering on=== null.LogicOpreads the union tag withExtractValueOp.LogicalBinaryOpLoweringpasses it converted operands, but an optional's unwrapped value (fromOptionalLogicHelper) is still ats.union. The value is converted to the LLVM struct first when it isn't one already.Three more fixes
OptionalLogicHelperunwrapped it without asking. The TODO said so: "rewrite code to take in account that Opt value can be undefined". So an emptystring | undefinedwas=== null, and an emptyboolean | undefinedwas=== false. The new tests fail without this. Loosely, an empty optional still equalsnull(01arguments.tsrelies on it).// @strict-nullpragma now reaches the union builder.MLIRTypeHelperkeeps a copy ofCompileOptions, and MLIRGen'smthtook it before the pragma was read. Files marked@strict-null falsegot non-strict casts but strict unions. This changes union layout in every existing@strict-null falsetest. The suite passes.T | nullis cast toTfirst (castToNullableUnionType). An object literal passed asInterface | null, or a const array asnumber[] | null, went straight to the lowering, which can't convert them. Fixing bug 1 routedMaybe<Path>(null | undefined | Path) onto this path, andpath.tsfailed.Tests
00union_null_undefined.ts(strict) and00union_null_undefined_nonstrict.ts, each registered for compile and JIT. Each fix was seen failing first.lowering-errors/array_literal_element_type.tsraised its error withnumber[] | null = [1, 2], which now compiles. It now reads.lengthfrom anumber[] | null(see below).ctest -C Release3321/3321.01arguments,path) pass.Found, not fixed here
let n: number | undefined = 0.5; n === 0is true. MLIRGen's numeric sync casts the optional to the literal's type (si32), and an empty one becomes 0, soundefined === 0andundefined < 1are true too. Next PR.number | nullcan't be compared with2or printed (can't cast from 'null' to 's32', from___cast).number[] | nullfails in the lowering (cast from array type ... is not supported).{ k: number } | nullcan't be tested withif (p).C | number=== 3fails withinvalid cast from C to s32.null == undefinedis false when an optional holdsnull(whenOneValueIsUndef).🤖 Generated with Claude Code