Skip to content

feat(ts): type a function value's () result as undefined - #303

Merged
kixelated merged 6 commits into
mainfrom
quest/m3/ts-unit-closures
Oct 30, 2026
Merged

kixelated merged 6 commits into
mainfrom
quest/m3/ts-unit-closures

Conversation

@kixelated

@kixelated kixelated commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

A function value returning () was typed => void, so TypeScript inferred void for what it yields. tsc then refused the undefined the output annotates () with. Reproduced: data.iter().map(|_| ()).collect::<Vec<()>>() failed with nested.ts(661,2): Type 'void[]' is not assignable to type 'undefined[]', and VecDeque<()> failed with Type 'Iter<void>' is not assignable to type 'Iter<undefined>'.

The quest's plan (retype only a closure's return) doesn't cover every case. The same void also leaks through:

  • a fn pointer, such as data.iter().copied().map(f) with f: fn(u8);
  • a closure-typed parameter passed on, such as a generic each<T>(f: impl Fn(u8) -> T) at T = ();
  • a dyn Fn;
  • a function item used as a value, such as .map(ignore), which is declared : void.

Auditing that last case turned up a pre-existing bug: a function item read back from a generic's parameter lowered to undefined. each(data, half) became rt.iterMap(..., undefined) for any return type, and tsc refused it.

Approach

  • fn_ty (closure, fn pointer, FnDef, and dyn Fn types) and closure arrows type a () result as undefined.
  • fn_value casts a declared function that returns () to that type, as in ignore as (a0: rt.u8) => undefined. The arrow fallback calls it without return.
  • Declared functions keep : void. So do the member types of a trait object, whose methods are declared functions.
  • local_expr reads a FnDef local as fn_value rather than undefined.
  • The lang fixture's nested gains a units case: Vec<()>, VecDeque<()>, Option<()>, Result<(), E>, and Result<T, ()> from closures. It also covers a fn pointer, a function item, a generic at T = () given a closure and a function item, a dyn Fn, and a method returning an associated type = (). It runs differentially against native wasm32.

Snapshots: 47 files changed, all derived from this change. I checked this with a script that maps => undefined/): undefined => back to void and strips the new casts; afterwards only these differences remain:

  • monomorphized std instances renamed, since their hash covers their text;
  • one fn_value arrow that no longer returns a unit call (bounds.ts);
  • the new fixture code.

Impact

  • Translator output: a closure, fn pointer, dyn Fn, or function item used as a value returns undefined for () instead of void. This breaks a TS caller passing a void-typed function, so there is a MIGRATE.md entry. ts/README.md's type table gains a row.
  • A function item passed through a generic parameter now translates instead of emitting undefined.

Alternatives

  • Type every () return undefined, declared functions included. That needs no casts and deletes ret_ty's special case, but it churns roughly 770 signatures in 157 snapshot files. Rejected per the quest plan.
  • Wrap function items in an arrow instead of casting them. Rejected: it allocates and breaks fn pointer identity.

Choices made without asking

  • Fixed the function-item-through-a-generic bug here (3 lines), since the fixture's function-item case at T = () hits it.
  • Kept void on trait objects' member types.

Follow-ups

Review

  • Codex gpt-6.1-sol, medium effort, round 1, on head cdb2189138d3482c8c92555576e38729da53d184 against origin/main: 0 findings.
  • Local checks: just ci passed (158 tests), and just ts moq translates and type-checks moq at the pin.

Merge of main at 82fb1fc8 (#294, #299) into cdb21891, then 60cfcefe: took main's generated snapshots and regenerated them (the one interaction is this PR's => undefined cast now applied to #299's bound hash$Hash passed to rt.hashing), and repointed #294's new ts-zero-sized quest from the deleted ts-unit-closures link to this PR. Recorded skip, too trivial to review; just ci passed on 60cfcefe.

🤖 Generated with Claude Code

(written by Claude Opus 5.5)

kixelated and others added 3 commits October 30, 2026 12:21
A closure, fn pointer, dyn Fn, or function item used as a value returns
undefined for (), so a Vec<()> collected from one type-checks. A function
item read back from a generic's parameter names the function.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title A closure's () result as a value feat(ts): type a function value's () result as undefined Oct 30, 2026
@kixelated
kixelated marked this pull request as ready for review October 30, 2026 20:00
@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 20 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e235a1e6-2ee3-4c52-8041-64ae345ea072
Commits

Reviewing files that changed from the base of the PR and between 82fb1fc and 60cfcef.

Files selected for processing: 63
  • quest/m3/README.md
  • quest/m3/ts-deque-debug.md
  • quest/m3/ts-unit-closures.md
  • quest/m3/ts-unread-params.md
  • quest/m3/ts-zero-sized.md
  • ts/MIGRATE.md
  • ts/README.md
  • ts/fixtures/bind/expected/harness.ts
  • ts/fixtures/codec/expected/coding/decode.Decoder.ts
  • ts/fixtures/codec/expected/core/result.ts
  • ts/fixtures/codec/expected/harness.ts
  • ts/fixtures/lang/expected/bounds.ts
  • ts/fixtures/lang/expected/captures.ts
  • ts/fixtures/lang/expected/cells.ts
  • ts/fixtures/lang/expected/collections.ts
  • ts/fixtures/lang/expected/combinators.ts
  • ts/fixtures/lang/expected/core/option.ts
  • ts/fixtures/lang/expected/core/result.ts
  • ts/fixtures/lang/expected/cows.ts
  • ts/fixtures/lang/expected/durations.ts
  • ts/fixtures/lang/expected/dyns.ts
  • ts/fixtures/lang/expected/harness.ts
  • ts/fixtures/lang/expected/hashes.ts
  • ts/fixtures/lang/expected/hints.ts
  • ts/fixtures/lang/expected/ints.ts
  • ts/fixtures/lang/expected/iters.ts
  • ts/fixtures/lang/expected/keys.Horizon.ts
  • ts/fixtures/lang/expected/keys.Seen.ts
  • ts/fixtures/lang/expected/keys.ts
  • ts/fixtures/lang/expected/locals.ts
  • ts/fixtures/lang/expected/nested.Unit.ts
  • ts/fixtures/lang/expected/nested.ts
  • ts/fixtures/lang/expected/objects.ts
  • ts/fixtures/lang/expected/orders.ts
  • ts/fixtures/lang/expected/pointers.Wrapped.ts
  • ts/fixtures/lang/expected/pointers.ts
  • ts/fixtures/lang/expected/refs.ts
  • ts/fixtures/lang/expected/shapes.ts
  • ts/fixtures/lang/expected/slices.Counted.ts
  • ts/fixtures/lang/expected/slices.Item.ts
  • ts/fixtures/lang/expected/slices.Noisy.ts
  • ts/fixtures/lang/expected/slices.ts
  • ts/fixtures/lang/expected/statics.Lock.ts
  • ts/fixtures/lang/expected/statics.ts
  • ts/fixtures/lang/expected/std/thread/local.ts
  • ts/fixtures/lang/expected/wrappers.ts
  • ts/fixtures/lang/src/nested.rs
  • ts/fixtures/stock/expected/buf.Pieces.ts
  • ts/fixtures/stock/expected/buf.Provided.ts
  • ts/fixtures/stock/expected/buf.ts
  • ts/fixtures/stock/expected/core/result.ts
  • ts/fixtures/stock/expected/harness.ts
  • ts/fixtures/task/expected/core/result.ts
  • ts/fixtures/task/expected/drops.ts
  • ts/fixtures/task/expected/drops/closures.ts
  • ts/fixtures/task/expected/drops/folds.ts
  • ts/fixtures/task/expected/drops/iters.ts
  • ts/fixtures/task/expected/futures.ts
  • ts/src/lower/body.rs
  • ts/src/lower/dynamic.rs
  • ts/src/lower/mir/place.rs
  • ts/src/lower/mir/rvalue.rs
  • ts/src/lower/mod.rs
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

# Conflicts:
#	ts/fixtures/lang/expected/durations.ts
#	ts/fixtures/lang/expected/harness.ts
#	ts/fixtures/lang/expected/nested.ts
#	ts/fixtures/lang/expected/orders.ts
#	ts/fixtures/lang/expected/statics.ts
#	ts/fixtures/task/expected/drops/iters.ts

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed commit: cdb2189

No actionable bugs found. The direction is sound: function-value signatures consistently represent () as undefined, while declared functions and trait-object methods retain void. Casting named unit-returning functions preserves identity without allocating wrappers; the fallback arrow correctly discards a unit call's result. Recovering FnDef locals is a small, relevant fix, and the added fixture covers function items, pointers, generic callbacks, dynamic calls, and unit-bearing containers. The migration note documents the TypeScript caller impact.

Verification limits: static review of the lowering changes, relevant surrounding code, fixture source, and generated snapshot diffs only. I did not run the translator, TypeScript checks, or differential tests; the PR's reported test results were not independently reproduced.

Luke Curley and others added 2 commits October 30, 2026 13:04
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s done

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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