From 922cd5032aeb38b124f4e6b1434a64188beee433 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 31 Oct 2026 12:29:13 +0000 Subject: [PATCH] TSLANG_ISSUES: 21, a C string result isn't copied when its .d.ts came through an import #575's copy of a C function's `string` result is skipped when the `declare function` is in a .d.ts that an imported module also references (isForeignCall checks the module the declaration was recorded with). The raw char* then becomes a program string again: aliased buffers under gc, C++ memory counted and freed under rc (native_ts_graphics's SampleGame crashed on Android). Repro run on pre-alpha111, the IR, where to look and a suggested regression test. Co-Authored-By: Claude Opus 5.5 --- TSLANG_ISSUES.md | 94 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/TSLANG_ISSUES.md b/TSLANG_ISSUES.md index cdff25569..c5f788e9d 100644 --- a/TSLANG_ISSUES.md +++ b/TSLANG_ISSUES.md @@ -5,7 +5,93 @@ should be a complete file that was run (`tslang file.ts`, JIT, Windows x64), wit ## Open -None. 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-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`. + +### 21. A C `string` result isn't copied when its `.d.ts` was first loaded by an imported module + +tslang v0.0-pre-alpha111. Found 2026-10-31. + +#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. + +Repro: four files in one folder. + +`c_decls.d.ts`: +```ts +declare function strerror(errnum: int): string; +``` + +`other2.ts`: +```ts +/// +export class Other { + name(): string { + return "other"; + } +} +``` + +`main2.ts`, which imports `other2.ts`: +```ts +/// +import { Other } from "./other2"; + +// 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()); +} +``` + +`main3.ts`, the same without the import: +```ts +/// + +function main() { + const messages: string[] = []; + messages.push(strerror(2)); + messages.push(strerror(13)); + print(messages[0], "|", messages[1]); +} +``` + +Run (`tslang --emit=jit -mm=gc --jit-cache=false `; `-mm=rc` prints the same): +``` +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) +``` + +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.