From 65c7a1eaf4324efa434d3c7525f2dfc27882079c Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 1 Nov 2026 11:57:59 +0000 Subject: [PATCH 1/2] TSLANG_ISSUES: 22, one-field @value T | null crash; 23, h && cond with a @cpp class | null Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 177 +++++++++++++++++++++++++++++------------------ 1 file changed, 109 insertions(+), 68 deletions(-) diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index c5f788e9d..01d1b1e14 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -5,93 +5,134 @@ should be a complete file that was run (`tslang file.ts`, JIT, Windows x64), wit ## Open -Items 1-20 (collected 2026-10-12 to pre-alpha108) are fixed or answered; their write-ups are in this -file's git history. The last one, 20 (an enum `E | undefined` compared with an `E` variable was always -`false`, with "Not applicable logical operator for type: 'E'"), was a missing enum case in the -comparison lowering: regression test `tslang/test/tester/tests/00enum_compare.ts`. +Items 1-21 (collected 2026-10-12 to pre-alpha111) are fixed or answered; their write-ups are in this +file's git history. The last one, 21 (a C `string` result not copied when its `.d.ts` was first +loaded by an imported module), was fixed by #761 and checked with pre-alpha112: `strerror(2)` and +`strerror(13)` called from the main module and from an imported one give four different texts. -### 21. A C `string` result isn't copied when its `.d.ts` was first loaded by an imported module +### 22. `T | null` of a `@value` type whose only field is a class or interface: compiler crash -tslang v0.0-pre-alpha111. Found 2026-10-31. +tslang v0.0-pre-alpha112 (Windows x64). Found 2026-10-31. A regression from #760 (pre-alpha111 +compiled all of these). -#575 (issue 6) copies the `string` a C function returns (`ts.StringCopy`), so a reused buffer or -another string's middle never becomes a program string. That copy is skipped when the -`declare function` comes from a `.d.ts` that a module this file imports also references: the -result is the raw `char*` again, with issue 6's effects. Under `-mm=gc`, stored results show the -buffer's last text. Under `-mm=rc`, retaining one writes its count into the 8 bytes before C's -pointer, and releasing it frees C's memory. +#760 stores `T | null` of a `@value` type T without a tag when T has a class or interface field +(`MLIRTypeHelper::getValueNullNiche`): null is T with that pointer zeroed. When that field is T's +**only** field, the lowering fails: -Repro: four files in one folder. +- comparing such a value with null (`== null`, `=== null`, `if (v)`) crashes tslang (0xC0000005, + exit 139), under every memory model (gc and rc tried), JIT and `--emit=obj`/`--emit=llvm` alike; +- passing a `@value` class value to a `T | null` parameter fails with + `error: LLVM Translation failed for operation: builtin.unrealized_conversion_cast`. -`c_decls.d.ts`: -```ts -declare function strerror(errnum: int): string; -``` +Add a second field (a number, before or after the reference) and both compile and print the right +results. `--emit=mlir` succeeds in every case; it is the lowering to LLVM that fails. +`test/tester/value/value_missing_niche.ts` passes with pre-alpha112: its `Hit` and `Mark` have two +fields each. -`other2.ts`: +Repro 1, `one_null.ts` - `tslang --emit=jit one_null.ts`: ```ts -/// -export class Other { - name(): string { - return "other"; - } +class H { x = 1; } + +@value +interface One { + readonly h: H; } -``` -`main2.ts`, which imports `other2.ts`: -```ts -/// -import { Other } from "./other2"; +function f(d: number): One | null { return d < 0 ? null : { h: new H() }; } -// strerror reuses one buffer: each result has to be copied when it is stored. function main() { - const messages: string[] = []; - messages.push(strerror(2)); - messages.push(strerror(13)); - print(messages[0], "|", messages[1], "|", new Other().name()); + print(f(-1) == null, f(1) == null); } ``` +Expected `true false`. Actual: "PLEASE submit a bug report ... Exception Code: 0xC0000005", stack +all in tslang.exe. With `readonly n: number;` added to `One` (and `n: 1` in the literal) it prints +`true false`. The same crash with a `@value class` with a constructor, and with a top-level +`let p: One | null = null; print(p == null);`. -`main3.ts`, the same without the import: +Repro 2, `one_param.ts` - `tslang --emit=jit one_param.ts`: ```ts -/// +class H { x = 1; } + +@value +class One { + readonly h: H; + constructor(h: H) { this.h = h; } +} + +function take(o: One | null): int { return 1; } function main() { - const messages: string[] = []; - messages.push(strerror(2)); - messages.push(strerror(13)); - print(messages[0], "|", messages[1]); + print(take(new One(new H()))); } ``` - -Run (`tslang --emit=jit -mm=gc --jit-cache=false `; `-mm=rc` prints the same): +Expected `1`. Actual: ``` -main2.ts -> Permission denied | Permission denied | other (wrong: the first should be "No such file or directory") -main3.ts -> No such file or directory | Permission denied (right) +error: LLVM Translation failed for operation: builtin.unrealized_conversion_cast +tslang: error: failed to emit LLVM IR ``` +With a second field (`readonly n: number;`, set in the constructor) it prints `1`. The same +program with a `@value interface` and an object literal (`const o: One = { h: new H() }; take(o)`) +compiles and prints `1`. + +MLIR (repro 1, `--emit=mlir`): `f` returns +`!ts.optional}>>>>`, null made +as `ts.Cast (!ts.null) -> !ts.union>`, the value as +`ts.Cast (!ts.value<@One, ...>) -> !ts.union<...>`: the niche (untagged) form, as for two fields. + +Suspect: the lowering of a one-field `@value` type next to the niche code. `getValueNullNiche` gives +the path `{0}` for the one field; `valueNullCompare` (LowerToLLVM/UnaryBinLogicalOrHelper.h) +`LLVM::ExtractValueOp`s that path from `asLLVMUnion(...)`, and the null/value casts into the union +(CastLogicHelper.h, near lines 743 and 1313) build it the same way. If a one-field tuple's LLVM type +(or the union's converted type for it) isn't the `{ptr}` struct those expect - a bare `ptr`, or a +different struct than the value's - the extract/insert has no element 0 (the crash) and the type +conversion leaves an `unrealized_conversion_cast` (repro 2). + +How native_ts_graphics hits it: core/donut.ts (generated) wraps every Donut object as a `@value` +class whose only field is `readonly handle: XHandle` (an `@cpp declare class`), and takes them as +`X | null` parameters (`App.createToneMappingPass(..., previousToneMappingPass: ToneMappingPass | +null)` does `previousToneMappingPass ? (previousToneMappingPass as ToneMappingPass).handle : null`). +With pre-alpha112, compiling core/donut.ts crashes tslang (exit 139, desktop and Android), and +examples/feature_demo.ts:942 (the `createToneMappingPass` call) fails with repro 2's error, so no +example builds. No workaround in native_ts_graphics yet: it can't build with pre-alpha112 until this +is fixed (a dummy second field in every wrapper would change the generated classes' layout for +nothing). + +### 23. `h && cond` with `h` a `@cpp` class `| null`: "instanceof can't be used with a @cpp class" + +tslang v0.0-pre-alpha113 (Windows x64). Found 2026-10-31. + +A `@cpp` handle that may be null tests fine alone (`if (h)`, `if (!h)`, and `if (!a || !b)` of two +of them), but not as the operand of `&&` with a boolean: the condition fails to compile with an +`instanceof` error the source never wrote. + +Repro, `repro.ts` - `tslang --emit=mlir repro.ts` (any emit; `-mm=gc`): +```ts +@cpp declare class Handle { private readonly __handle: index; } +declare function make(): Handle | null; +declare function use(h: Handle): void; -The IR shows it (`--emit=llvm -mm=rc`). In `main3.ts` the call is followed by -`strlen`/`__tslang_heap_malloc`/`memcpy_s`. In `main2.ts` it is followed directly by -`call void @tsretv_...(ptr %result)`, which counts the raw pointer. - -Where to look: `isForeignCall` in `tslang/lib/TypeScript/MLIRGenImpl.h` (about line 10118) answers -false unless `declaredFunctions[name].second == theModule.getOperation()`. -`MLIRGenFunctions.cpp:972` records each declaration with the module being generated when it was -seen. When the `.d.ts` was first reached through the import, the entry presumably belongs to -`other2.ts`'s pass, or this file's `/// ` is skipped as already loaded, so the check -fails. A declaration's foreignness shouldn't depend on which module first loaded its `.d.ts`: a -top-level `declare function` with no body anywhere in the program is C either way. - -How native_ts_graphics hit it: `core/rtxpt_game.ts` imports `./donut` (`donut.ts` references -`donut_interop.d.ts`) and calls `Donut_GetListedFile`, `Donut_JsonGetString` and -`Donut_GetNodeName` itself. -- Desktop (`gc`): stored file paths and light names aliased one buffer, until the C++ side returned a - separate string for each value. -- Android (`-mm=rc`): the stored paths were freed at the end of `RtxptGame.sceneLoaded`, which - crashed with "Scudo ERROR: misaligned pointer when deallocating". - -The workaround there copies each result with a template literal, `` `${Donut_GetListedFile(i)}` `` -(the template's copy never retains the raw pointer). It can go once this is fixed. - -Suggested regression test: `00c_string_copy.ts`'s case, split over a module and an importer that -reference the same `.d.ts`, checking that the stored `strerror` texts differ. +function main() { + const h = make(); + const n = 3; + if (h && n == 3) { + use(h); + } +} +``` +Expected: compiles (the condition is h != null and n == 3). Actual: +``` +repro.ts:8:5: error: instanceof can't be used with a @cpp class: its objects are C++ objects, with no type information +repro.ts:8:5: error: can't instantiate function. '___cast' not all generic types can be identified +``` +`if (h) { if (n == 3) { use(h); } }` compiles and runs. + +Suspect: `h && n == 3` is typed as a union (`Handle | null | boolean`), and making it a boolean goes +through `MLIRGenImpl::castFromUnion` (MLIRGenCast.cpp, near line 3104), which generates the generic +`___cast` with an `instanceof` test for each class member of the union; the `instanceof` +lowering (MLIRGenImpl.h, near line 6610) rejects a `@cpp` class there. For a `@cpp` member (a plain +pointer with no vtable of tslang's) the union's truthiness needs only the null test, not +`instanceof`. + +How native_ts_graphics hits it: examples/basic_triangle_nri.ts (NRI handles, `@cpp declare class`es +in core/nri/nri_interop.d.ts) had `if (readbackBuffer && frameIndex == TEST_FRAMES - 1)`. +Workaround there: nested `if`s. From 3f6ee0882d800938b40c4e223ae18518949e2315 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 1 Nov 2026 11:58:04 +0000 Subject: [PATCH 2/2] TSLANG_ISSUES: 22 and 23 fixed (#763, #765) Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 136 ++--------------------------------------------- 1 file changed, 5 insertions(+), 131 deletions(-) diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index 01d1b1e14..a92feb7e3 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -5,134 +5,8 @@ should be a complete file that was run (`tslang file.ts`, JIT, Windows x64), wit ## Open -Items 1-21 (collected 2026-10-12 to pre-alpha111) are fixed or answered; their write-ups are in this -file's git history. The last one, 21 (a C `string` result not copied when its `.d.ts` was first -loaded by an imported module), was fixed by #761 and checked with pre-alpha112: `strerror(2)` and -`strerror(13)` called from the main module and from an imported one give four different texts. - -### 22. `T | null` of a `@value` type whose only field is a class or interface: compiler crash - -tslang v0.0-pre-alpha112 (Windows x64). Found 2026-10-31. A regression from #760 (pre-alpha111 -compiled all of these). - -#760 stores `T | null` of a `@value` type T without a tag when T has a class or interface field -(`MLIRTypeHelper::getValueNullNiche`): null is T with that pointer zeroed. When that field is T's -**only** field, the lowering fails: - -- comparing such a value with null (`== null`, `=== null`, `if (v)`) crashes tslang (0xC0000005, - exit 139), under every memory model (gc and rc tried), JIT and `--emit=obj`/`--emit=llvm` alike; -- passing a `@value` class value to a `T | null` parameter fails with - `error: LLVM Translation failed for operation: builtin.unrealized_conversion_cast`. - -Add a second field (a number, before or after the reference) and both compile and print the right -results. `--emit=mlir` succeeds in every case; it is the lowering to LLVM that fails. -`test/tester/value/value_missing_niche.ts` passes with pre-alpha112: its `Hit` and `Mark` have two -fields each. - -Repro 1, `one_null.ts` - `tslang --emit=jit one_null.ts`: -```ts -class H { x = 1; } - -@value -interface One { - readonly h: H; -} - -function f(d: number): One | null { return d < 0 ? null : { h: new H() }; } - -function main() { - print(f(-1) == null, f(1) == null); -} -``` -Expected `true false`. Actual: "PLEASE submit a bug report ... Exception Code: 0xC0000005", stack -all in tslang.exe. With `readonly n: number;` added to `One` (and `n: 1` in the literal) it prints -`true false`. The same crash with a `@value class` with a constructor, and with a top-level -`let p: One | null = null; print(p == null);`. - -Repro 2, `one_param.ts` - `tslang --emit=jit one_param.ts`: -```ts -class H { x = 1; } - -@value -class One { - readonly h: H; - constructor(h: H) { this.h = h; } -} - -function take(o: One | null): int { return 1; } - -function main() { - print(take(new One(new H()))); -} -``` -Expected `1`. Actual: -``` -error: LLVM Translation failed for operation: builtin.unrealized_conversion_cast -tslang: error: failed to emit LLVM IR -``` -With a second field (`readonly n: number;`, set in the constructor) it prints `1`. The same -program with a `@value interface` and an object literal (`const o: One = { h: new H() }; take(o)`) -compiles and prints `1`. - -MLIR (repro 1, `--emit=mlir`): `f` returns -`!ts.optional}>>>>`, null made -as `ts.Cast (!ts.null) -> !ts.union>`, the value as -`ts.Cast (!ts.value<@One, ...>) -> !ts.union<...>`: the niche (untagged) form, as for two fields. - -Suspect: the lowering of a one-field `@value` type next to the niche code. `getValueNullNiche` gives -the path `{0}` for the one field; `valueNullCompare` (LowerToLLVM/UnaryBinLogicalOrHelper.h) -`LLVM::ExtractValueOp`s that path from `asLLVMUnion(...)`, and the null/value casts into the union -(CastLogicHelper.h, near lines 743 and 1313) build it the same way. If a one-field tuple's LLVM type -(or the union's converted type for it) isn't the `{ptr}` struct those expect - a bare `ptr`, or a -different struct than the value's - the extract/insert has no element 0 (the crash) and the type -conversion leaves an `unrealized_conversion_cast` (repro 2). - -How native_ts_graphics hits it: core/donut.ts (generated) wraps every Donut object as a `@value` -class whose only field is `readonly handle: XHandle` (an `@cpp declare class`), and takes them as -`X | null` parameters (`App.createToneMappingPass(..., previousToneMappingPass: ToneMappingPass | -null)` does `previousToneMappingPass ? (previousToneMappingPass as ToneMappingPass).handle : null`). -With pre-alpha112, compiling core/donut.ts crashes tslang (exit 139, desktop and Android), and -examples/feature_demo.ts:942 (the `createToneMappingPass` call) fails with repro 2's error, so no -example builds. No workaround in native_ts_graphics yet: it can't build with pre-alpha112 until this -is fixed (a dummy second field in every wrapper would change the generated classes' layout for -nothing). - -### 23. `h && cond` with `h` a `@cpp` class `| null`: "instanceof can't be used with a @cpp class" - -tslang v0.0-pre-alpha113 (Windows x64). Found 2026-10-31. - -A `@cpp` handle that may be null tests fine alone (`if (h)`, `if (!h)`, and `if (!a || !b)` of two -of them), but not as the operand of `&&` with a boolean: the condition fails to compile with an -`instanceof` error the source never wrote. - -Repro, `repro.ts` - `tslang --emit=mlir repro.ts` (any emit; `-mm=gc`): -```ts -@cpp declare class Handle { private readonly __handle: index; } -declare function make(): Handle | null; -declare function use(h: Handle): void; - -function main() { - const h = make(); - const n = 3; - if (h && n == 3) { - use(h); - } -} -``` -Expected: compiles (the condition is h != null and n == 3). Actual: -``` -repro.ts:8:5: error: instanceof can't be used with a @cpp class: its objects are C++ objects, with no type information -repro.ts:8:5: error: can't instantiate function. '___cast' not all generic types can be identified -``` -`if (h) { if (n == 3) { use(h); } }` compiles and runs. - -Suspect: `h && n == 3` is typed as a union (`Handle | null | boolean`), and making it a boolean goes -through `MLIRGenImpl::castFromUnion` (MLIRGenCast.cpp, near line 3104), which generates the generic -`___cast` with an `instanceof` test for each class member of the union; the `instanceof` -lowering (MLIRGenImpl.h, near line 6610) rejects a `@cpp` class there. For a `@cpp` member (a plain -pointer with no vtable of tslang's) the union's truthiness needs only the null test, not -`instanceof`. - -How native_ts_graphics hits it: examples/basic_triangle_nri.ts (NRI handles, `@cpp declare class`es -in core/nri/nri_interop.d.ts) had `if (readbackBuffer && frameIndex == TEST_FRAMES - 1)`. -Workaround there: nested `if`s. +Items 1-23 (collected 2026-10-12 to pre-alpha113) are fixed or answered; their write-ups are in this +file's git history. The last two: 22 (`T | null` of a `@value` type whose only field is a class or +interface crashed the compiler, a regression from #760) was fixed by #763; 23 (`h && cond` with `h` +a `@cpp` class `| null` failed with "instanceof can't be used with a @cpp class") was fixed by #765, +regression test `tslang/test/tester/tests/00cpp_union_truth.ts`.