Skip to content

@value T | null fixes, interfaces print their fields, C string from an imported .d.ts - #761

Merged
ASDAlexander77 merged 3 commits into
mainfrom
value-missing-ops
Oct 31, 2026
Merged

ASDAlexander77 merged 3 commits into
mainfrom
value-missing-ops

Conversation

@ASDAlexander77

@ASDAlexander77 ASDAlexander77 commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Three failures of a @value type that may be missing (T | null, T | undefined), all on main before #760, plus printing of interface values. Found while working on #760.

@value T | null (98e92da)

  • rc: return d < 0 ? null : { at: d, owner: new Owner(d) } in a function returning Mark | null read freed memory. The branch released the new Owner before the retain after the ?: ran. consumeConditionalResults' producerOf now sees through the cast from a tuple to the @value type it is laid out as. So yieldsLiteralOfBranchTemporary recognizes the literal, and the branch retains first. 2,000,000 calls peak at 4.7 MB, so nothing leaks.
  • ??: find(-1) ?? find(3) with find(): Hit | null was typed Hit | (Hit | null) and did not compile. An optional member of a union contributed its whole element type as one member. processUnionTypeItem now processes the element like any other member, so a union inside it contributes its members.
  • Printing:

The new test value/value_missing_ops.ts runs under gc, rc and none, for a T holding a class (no tag) and a T of numbers only (tagged). Own rejects it: it can't yet tell which branch of a ?: owns a literal it merges.

Printing an interface (7fa8c15)

Printing an interface value, or putting one into a string, crashed at run time, whatever object was behind it: a class instance, an object literal, or an interface held by a @value. Per your ruling, an interface now prints its fields, as an object literal does ({ n: 3, name: 'b' }), with fields of the interfaces it extends first.

  • An interface that declares toString calls it, as a class does.
  • A null interface prints null.
  • A field whose interface type is already being printed shows [Object], because interface Chain { next: Chain } would otherwise expand forever.

castTupleToString and the new castInterfaceToString share one formatter, fieldsToString. The new test tests/00interface_print runs under every model.

A C string result from a .d.ts an imported module loaded first (2da1465, TSLANG_ISSUES 21)

The string a C function returns is copied (#575), except when the .d.ts declaring it was first reached through a module this file imports. declaredFunctions kept that earlier pass's module, and isForeignCall required the current one, so the raw char* was used as is. Stored strerror results then all showed the buffer's last text, and under -mm=rc the reference count was written into C's memory and the release freed it. Now a top-level declare function with no body counts as C whichever module loaded its .d.ts. The new test is in c-string-import/, under gc, rc, none and own, compiled and JIT. Its .d.ts lives outside tests/ because tests/*.d.ts are build output and git ignores them.

Testing

  • ctest 6560/6560 (Windows Release).
  • Fuzz seeds 83000-85599 clean.
  • DefaultLib compiles under own, rc, gc and none.

🤖 Generated with Claude Code

ASDAlexander77 and others added 2 commits October 31, 2026 11:55
…rint

Three failures of a @value type that may be missing, all on main before #760:

- rc: `d < 0 ? null : { at: d, owner: new Owner(d) }` returned as `Mark | null`. The
  branch released the new object before the result's retain after the join, which then
  retained freed memory. consumeConditionalResults' producerOf looks through a tuple
  cast to the @value type it is laid out as, so yieldsLiteralOfBranchTemporary sees the
  literal and the branch retains first.
- `h ?? find(3)` was typed `Hit | (Hit | null)` and did not compile: an optional member of
  a union gave its element as one member. processUnionTypeItem now processes the element
  like any other member, so a union in it gives its members.
- print / string conversion: an undefined `T | undefined` printed as null or as a zeroed
  T (the optional went through ValueOrDefault); it is "undefined" now. A null `T | null`
  stored without a tag (#760) printed T's fields; castNullableToString takes a @value
  base too and prints "null".

Test value/value_missing_ops (gc, rc, none; own cannot yet tell which branch of a `?:`
owns a literal it merges). ctest 6546/6546; fuzz seeds 83000-83599 clean; DefaultLib
compiles under own and rc.

Printing a @value value holding an interface still crashes, as printing any interface
does - not changed here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Printing an interface value, or putting it into a string, crashed at run time,
whatever object was behind it - a class, an object literal, or one a @value value
holds. USER RULING: print its fields. castInterfaceToString:

- its `toString` when the interface has one, as a class's is;
- else its fields, read through the interface, those of the interfaces it extends
  first, in an object literal's format (`{ n: 3, name: 'b' }`, fieldsToString,
  which castTupleToString now shares);
- null as "null";
- a field of an interface type already being printed is `[Object]`: the text is
  made inline, and `interface Chain { next: Chain }` nests without end.

Test tests/00interface_print (every model). ctest 6552/6552; fuzz seeds
84000-84599 clean; DefaultLib compiles under all four models.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77 ASDAlexander77 changed the title @value T | null: ?: of a literal holding a new object (rc), ??, and print @value T | null: ?: (rc), ??, and print; interfaces print their fields Oct 31, 2026
…first

TSLANG_ISSUES 21. The string a C function returns is copied into a string of the
program's own (#575), but not when the `.d.ts` declaring it was first reached through a
module this file imports: `declaredFunctions` kept the module of that earlier pass, and
isForeignCall wanted the one being generated. The raw `char*` was taken as it was - stored
strerror() results showed the buffer's last text, and under -mm=rc the count was written
into C's memory and the release freed it (native_ts_graphics on Android: "Scudo ERROR:
misaligned pointer when deallocating").

A top-level body-less `declare function` is C whichever module's pass loaded its `.d.ts`:
isForeignCall now asks that, and that this module's function has no body.

Test c-string-import/ (gc, rc, none, own; compiled and JIT), whose `.d.ts` is outside
tests/, where *.d.ts are build output and ignored. ctest 6560/6560; fuzz seeds
85000-85599 clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77 ASDAlexander77 changed the title @value T | null: ?: (rc), ??, and print; interfaces print their fields @value T | null fixes, interfaces print their fields, C string from an imported .d.ts Oct 31, 2026
@ASDAlexander77
ASDAlexander77 merged commit 3c4b135 into main Oct 31, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the value-missing-ops branch October 31, 2026 13:03
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.

1 participant