From b7b50022c14ca1f8d6455208c5c08d0e677baedb Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sun, 11 Oct 2026 11:39:47 +0100 Subject: [PATCH] docs/bugs: issues found porting native_ts_graphics samples A report (native-ts-graphics-findings.md) and a minimal repro per issue, each verified on v0.0-pre-alpha92: comparing Opaque with Opaque | null crashes the compiler (0xC00000FD, no message); boolean array elements read back wrong; `let s = 0` is inferred as an integer so `s += 1.5` truncates; escapes stay unprocessed in templates with substitutions; Math.max of negative numbers returns Number.MIN_VALUE; `boolean && (Class | null)` as a condition fails to compile. Plus issues seen earlier that no longer reproduce, and two C interop behaviors worth documenting. Co-Authored-By: Claude Opus 5.5 --- docs/bugs/native-ts-graphics-findings.md | 190 +++++++++++++++++++++++ docs/bugs/ntg_and_nullable_class.ts | 20 +++ docs/bugs/ntg_boolean_array_elements.ts | 20 +++ docs/bugs/ntg_let_zero_is_integer.ts | 28 ++++ docs/bugs/ntg_math_max_negative.ts | 10 ++ docs/bugs/ntg_opaque_nullable_compare.ts | 20 +++ docs/bugs/ntg_template_literal_escape.ts | 17 ++ 7 files changed, 305 insertions(+) create mode 100644 docs/bugs/native-ts-graphics-findings.md create mode 100644 docs/bugs/ntg_and_nullable_class.ts create mode 100644 docs/bugs/ntg_boolean_array_elements.ts create mode 100644 docs/bugs/ntg_let_zero_is_integer.ts create mode 100644 docs/bugs/ntg_math_max_negative.ts create mode 100644 docs/bugs/ntg_opaque_nullable_compare.ts create mode 100644 docs/bugs/ntg_template_literal_escape.ts diff --git a/docs/bugs/native-ts-graphics-findings.md b/docs/bugs/native-ts-graphics-findings.md new file mode 100644 index 000000000..8673a091a --- /dev/null +++ b/docs/bugs/native-ts-graphics-findings.md @@ -0,0 +1,190 @@ +# tslang issues found while porting graphics samples (native_ts_graphics) + +These issues came up while porting about 100 D3D12 and Vulkan samples to TypeScript in +[native_ts_graphics](https://github.com/ASDAlexander77/native_ts_graphics), from October 2026. +Each one has a minimal repro beside this file (`ntg_*.ts`), written in the style of the other +files here (`print` / `assert`, top-level code). + +**Verified on:** `tslang version v0.0-pre-alpha92` (LLVM 22.1.8), the `I:\tslang\tslang.exe` build +of 2026-10-06, on Windows 11, 2026-10-11. Run each repro with: + +``` +tslang --emit=jit --jit-cache=false docs/bugs/ntg_.ts +``` + +Every repro below still fails on that build: a failed assertion (exit code 3), a compile error, +or a compiler crash. + +| # | Issue | Kind | Repro | +|---|---|---|---| +| 1 | Comparing `Opaque` with `Opaque \| null` crashes the compiler | Compiler crash, no message | `ntg_opaque_nullable_compare.ts` | +| 2 | Boolean array elements read back wrong | Wrong value, silent | `ntg_boolean_array_elements.ts` | +| 3 | `let s = 0` is an integer, so `s += 1.5` truncates | Wrong value, silent | `ntg_let_zero_is_integer.ts` | +| 4 | Escapes not processed in templates with substitutions | Wrong value, silent | `ntg_template_literal_escape.ts` | +| 5 | `Math.max` of negative numbers returns 2.2e-308 | Wrong value, silent (default lib) | `ntg_math_max_negative.ts` | +| 6 | `boolean && (Class \| null)` as a condition fails to compile | Compile error | `ntg_and_nullable_class.ts` | + +The silent ones (2 to 5) cost the most time: the programs compile and run, and only the output +is wrong. + +--- + +## 1. Comparing `Opaque` with `Opaque | null` crashes the compiler + +```ts +class State { + current: Opaque | null; + ... + set(next: Opaque): boolean { + if (next != this.current) { ... } +``` + +**Observed:** tslang exits with `0xC00000FD` (stack overflow) and prints nothing: no diagnostic, +no line number. Under Ninja the build step just shows `FAILED`. + +**Expected:** compiles (it's an identity comparison of two handles), or at least a diagnostic. + +**Impact and workaround:** this happened in a renderer that tracked the currently bound pipeline +to skip redundant binds. Finding it took a bisection, stubbing method bodies one at a time. The +workaround is to track such state with an integer (e.g. a pipeline index, -1 for none). From +bash the exit code shows as 127 (a missing DLL in that environment); run tslang from cmd or +PowerShell to see `0xC00000FD`. + +## 2. Boolean array elements read back wrong + +```ts +const FLAGS = [false, true, false, true]; +let n = 0; +for (let i = 0; i < FLAGS.length; i++) { + if (FLAGS[i]) { n++; } +} +print(n); // 10 (expected 2) +const b: boolean = FLAGS[1]; +print(b); // false (expected true) +``` + +**Observed:** `if (FLAGS[i])` is never taken for any element, a counted loop gives a nonsense +count (10 for 4 elements), and printing the elements one by one inside a namespace function gave +them inverted (`true false true false`). The array's `length` is right. This happens both at the +top level and inside namespaces. + +**Expected:** 2, true. + +**Impact and workaround:** one port's table of per-axis rotation flags was silently skipped, so +bounding boxes weren't rotated. The workaround is number arrays compared with `!= 0`. It looks +like an element-type or width mismatch (i1 vs i8?) in boolean array loads. + +## 3. `let s = 0` is an integer, so adding a fraction truncates + +```ts +let s = 0; +s += 1.5; +print(s); // 1 (expected 1.5) + +function sum(values: number[]): number { + let a = 0; + for (...) a += values[i]; + return a; // sum([1.5, 0, 2]) gives 3, expected 3.5 +} +``` + +**Observed:** the variable's type is inferred from the integer literal as an integer, so every +later `+=` / `= s + x` truncates. `let u: number = 0` works. + +**Expected (TypeScript semantics):** `s` is a `number` (a double). This may be a deliberate +design choice for performance. If so, it's worth documenting prominently, because ordinary +TypeScript code (accumulators, running totals) changes meaning silently. A warning when a +fractional value is assigned to an integer-inferred variable would catch it. + +**Workaround:** write `0.0`, or annotate `: number`. + +## 4. Escapes not processed in template literals with substitutions + +```ts +const n = 1; +const s = `${n}\n${n}`; +print(s.length); // 4: "1", "\", "n", "1" (expected 3) +``` + +**Observed:** the characters around a substitution keep their escape sequences literally. A +template without substitutions (`` `a\nb` ``) and a string literal (`"a\nb"`) are fine. + +**Expected:** 3 (a newline character). + +**Impact and workaround:** two-line ImGui labels (`` `Timestamps Period:\n ${period} ns` ``) +showed a literal `\n` on screen. Split such texts into separate strings, or concatenate a +`"\n"` string literal. + +## 5. `Math.max` of negative numbers returns 2.2e-308 + +```ts +print(Math.max(-3, -5)); // 2.22507e-308 (expected -3) +``` + +**Observed:** the default library's `Math.max` (TypeScriptCompilerDefaultLib `src/lib.ts`, line +1552: `let m = Number.MIN_VALUE;`) starts its running maximum at `Number.MIN_VALUE`, the smallest +*positive* double, rather than `-Infinity`. So any all-negative input returns 2.2e-308. The +library's own `tests/math.ts` (line 107, `isEq(Math.max(-1, -7), -1)`) checks exactly this case. +`Math.min` of positive numbers is fine. Also, `Number.MIN_VALUE` there is 2.22507385855e-308 (the +smallest *normal* double); in JavaScript it is 5e-324 (the smallest subnormal), as the comment +beside it notes. + +**Expected:** -3. Start from `-Infinity`, or from the first argument (and `Math.max()` with no +arguments is `-Infinity`). + +**Impact and workaround:** a camera's pitch clamp broke. Clamp with explicit comparisons. + +## 6. `boolean && (Class | null)` as a condition fails to compile + +```ts +function use(enabled: boolean, handle: Handle | null): number { + if (enabled && handle) { + return handle.value; + } + return 0; +} +``` + +**Observed:** + +``` +error: can't instantiate function. '___cast' not all generic types can be identified + if (enabled && handle) { +error: failed statement +``` + +**Expected:** compiles, and `handle` is narrowed to `Handle` inside the block. + +**Workaround:** nest the conditions: `if (enabled) { if (handle) { ... } }`. + +--- + +## Seen earlier but not reproduced in minimal form + +These came up in larger programs during the port series, but small repros of them pass on the +build above. Either they were fixed since, or they need more context than the obvious minimal +form. They're listed in case they ring a bell: + +- **Literal-union ternaries in arithmetic:** `width * (split ? 0.5 : 1.0)` evaluated to 0, and + `(held ? 1.0 : 0.0) - (...)` broke a camera's movement. Assigning the ternary to a `: number` + local first fixed it. Separately, `const left = c ? 448.0 : 96.0` got a literal-union type that + wouldn't mix with other numbers in arithmetic or ternaries without a `: number` annotation. + Minimal versions, with `number` and with `f32` operands, now compute the right values. +- **An array literal mixing `f32` values and number literals, passed as `number[]`:** failed with + `invalid cast from [f32, 0, ...]`. It now compiles and passes the right values. +- **Methods passed as callbacks** (`pass.setRenderCallback(this.onRender)`) seemed to resolve only + for members declared earlier in the class, so the examples declare `init()` (which registers the + callbacks) last. A minimal class calling `invoke(this.onRender)` with `onRender` declared after + `init` works. + +## Interop behaviors worth documenting + +These aren't necessarily bugs, but each cost time because the behavior was silent: + +- **`string` returns from declared C functions keep the raw `char*` without copying.** A C++ + helper that reused one `std::string` buffer for several calls handed TypeScript strings that + changed under it (garbage after the next call). The C side must return pointers to storage that + outlives the TypeScript string. +- **`number` parameters of declared C functions are doubles.** A C++ function declared with a + `float` parameter reads garbage, with no error (rotations came out as identity). The C side must + take `double`. diff --git a/docs/bugs/ntg_and_nullable_class.ts b/docs/bugs/ntg_and_nullable_class.ts new file mode 100644 index 000000000..e9bb1ce95 --- /dev/null +++ b/docs/bugs/ntg_and_nullable_class.ts @@ -0,0 +1,20 @@ +// `boolean && (class | null)` as an if condition fails to compile: "can't instantiate function. +// '___cast' not all generic types can be identified". Expected output: 7, done. +class Handle { + value: number; + constructor() { + this.value = 7; + } +} + +function use(enabled: boolean, handle: Handle | null): number { + if (enabled && handle) { + return handle.value; + } + return 0; +} + +const v = use(true, new Handle()); +print(v); +assert(v == 7); +print("done"); diff --git a/docs/bugs/ntg_boolean_array_elements.ts b/docs/bugs/ntg_boolean_array_elements.ts new file mode 100644 index 000000000..09ade193f --- /dev/null +++ b/docs/bugs/ntg_boolean_array_elements.ts @@ -0,0 +1,20 @@ +// Elements of a boolean array literal read back wrong: printed inverted, and never true in an +// `if`. Expected output: 2, true, b true, done. +const FLAGS = [false, true, false, true]; +let n = 0; +for (let i = 0; i < FLAGS.length; i++) { + if (FLAGS[i]) { + n++; + } +} +print(n); + +const b: boolean = FLAGS[1]; +print(b); +if (b) { + print("b true"); +} + +assert(n == 2); +assert(b); +print("done"); diff --git a/docs/bugs/ntg_let_zero_is_integer.ts b/docs/bugs/ntg_let_zero_is_integer.ts new file mode 100644 index 000000000..09a2e85a1 --- /dev/null +++ b/docs/bugs/ntg_let_zero_is_integer.ts @@ -0,0 +1,28 @@ +// `let s = 0` is inferred as an integer, so adding a fraction truncates. In TypeScript `s` is a +// number (double): expected output 1.5, 1.5, 1.5, 3.5, done. +let s = 0; +s += 1.5; +print(s); + +let t = 0; +t = t + 1.5; +print(t); + +let u: number = 0; +u += 1.5; +print(u); + +function sum(values: number[]): number { + let a = 0; + for (let i = 0; i < values.length; i++) { + a += values[i]; + } + return a; +} +const total = sum([1.5, 0, 2]); +print(total); + +assert(s == 1.5); +assert(t == 1.5); +assert(total == 3.5); +print("done"); diff --git a/docs/bugs/ntg_math_max_negative.ts b/docs/bugs/ntg_math_max_negative.ts new file mode 100644 index 000000000..acf0dfbb3 --- /dev/null +++ b/docs/bugs/ntg_math_max_negative.ts @@ -0,0 +1,10 @@ +// The default library's Math.max starts from Number.MIN_VALUE (the smallest positive double) +// rather than -Infinity, so the maximum of negative numbers is 2.22507e-308. Expected output: +// -3, 3, done. +const m = Math.max(-3, -5); +print(m); +assert(m == -3); +const k = Math.min(3, 5); +print(k); +assert(k == 3); +print("done"); diff --git a/docs/bugs/ntg_opaque_nullable_compare.ts b/docs/bugs/ntg_opaque_nullable_compare.ts new file mode 100644 index 000000000..6c1e056ab --- /dev/null +++ b/docs/bugs/ntg_opaque_nullable_compare.ts @@ -0,0 +1,20 @@ +// Compiling a comparison of an Opaque with an `Opaque | null` field crashes the compiler with a +// stack overflow (exit code 0xC00000FD), printing nothing. Expected output: done. +class State { + current: Opaque | null; + + constructor() { + this.current = null; + } + + set(next: Opaque): boolean { + if (next != this.current) { + this.current = next; + return true; + } + return false; + } +} + +const s = new State(); +print("done"); diff --git a/docs/bugs/ntg_template_literal_escape.ts b/docs/bugs/ntg_template_literal_escape.ts new file mode 100644 index 000000000..8d35fc0e7 --- /dev/null +++ b/docs/bugs/ntg_template_literal_escape.ts @@ -0,0 +1,17 @@ +// Escapes in a template literal with substitutions stay unprocessed: withSubstitution below holds +// the four characters 1, backslash, n, 1 (a template without substitutions and a string literal +// are fine). Expected output: 3, 3, 3, done. +const plain = "a\nb"; +print(plain.length); +assert(plain.length == 3); + +const template = `a\nb`; +print(template.length); +assert(template.length == 3); + +const n = 1; +const withSubstitution = `${n}\n${n}`; +print(withSubstitution.length); +assert(withSubstitution.length == 3); + +print("done");