syntax: a closure binds a var parameter (CONCURRENCY gap 5, part of slice F) #156

Closed
opened 2026-10-07 15:41:24 +00:00 by orion · 3 comments
Collaborator

Context

Gap 5 of CONCURRENCY.md (ECS.md gap 4), the second part of slice F, split out of #4: a closure may bind a var parameter. The body of windows wants to be a closure that captures a frame time or a scale factor, and a named fn cannot capture.

Source

Goal

var in front of a closure parameter name, with the existing escape rule (BACKEND 5.8) deciding whether the closure may keep the reference. Today points.each { var point => ... } is error: A binding needs a value and two parameters with a var among them is a parse error.

Acceptance criteria

  • { var point => point.x = point.x + 1 } and { (var point, scale) => ... } parse and type check against a parameter type (var value: Point) => Void and (var value: Point, scale: Float) => Void.
  • The write goes to the caller's value, in both back ends (conformance program: a forEach-style function taking body: (var value: Point) => Void, called with a closure that captures a local Float, asserts the mutated values).
  • A closure that tries to keep a var parameter beyond the call (stored in a field, returned, captured by a spawn closure) is refused by the existing escape rule with a diagnostic; probe for each of the three.
  • A mismatch (var closure parameter where the type has none, or the reverse) is a checker error that says which side is var.
  • The formatter (torb format --check), the language server's parameter hover and docs/language (the closure page) know the form; docs check docs green.
  • Checker, lowering and conformance tests for every bullet.

Touches the seed?

  • Yes, a syntax change (syntax/parser, the A binding needs a value site in parser/statements.trb). CLAUDE.md "Seed and breaking changes": the first commit teaches the form; the seed does not know it, so compiler/src and std/ must not use the new form, which keeps this a single commit with no seed refresh and no migration. A later use inside std/ waits for a seed refresh, which is the owner's call (the loop never refreshes the seed).

Size

One PR, one or two Conventional Commits: feat(syntax): a closure binds a var parameter and, if the lowering needs its own change, feat(ir): lower a closure with a var parameter. The type form (var value: Component) => Void and a named fn passed to it already work, so the by-reference convention exists; the new work is the parser, the checker's parameter binding and the closure conversion. If the implementer finds the lowering needs more than that, stop and split instead of widening the PR. Implementer: opus/high (parser, checker, IR).

Dependencies

None.

Part of #4.

## Context Gap 5 of CONCURRENCY.md (ECS.md gap 4), the second part of slice F, split out of #4: a closure may bind a `var` parameter. The body of `windows` wants to be a closure that captures a frame time or a scale factor, and a named `fn` cannot capture. ## Source - [CONCURRENCY.md gap 5](https://git.torb.dev/torbscript/language/src/commit/61d7075c05bd85fd27c71a4c191dd7a9fd5af4c0/docs/design/CONCURRENCY.md#L1143) - [ECS.md gap 4](https://git.torb.dev/torbscript/language/src/commit/61d7075c05bd85fd27c71a4c191dd7a9fd5af4c0/docs/design/ECS.md#L1348) ## Goal `var` in front of a closure parameter name, with the existing escape rule (BACKEND 5.8) deciding whether the closure may keep the reference. Today `points.each { var point => ... }` is `error: A binding needs a value` and two parameters with a `var` among them is a parse error. ## Acceptance criteria - [ ] `{ var point => point.x = point.x + 1 }` and `{ (var point, scale) => ... }` parse and type check against a parameter type `(var value: Point) => Void` and `(var value: Point, scale: Float) => Void`. - [ ] The write goes to the caller's value, in both back ends (conformance program: a `forEach`-style function taking `body: (var value: Point) => Void`, called with a closure that captures a local `Float`, asserts the mutated values). - [ ] A closure that tries to keep a `var` parameter beyond the call (stored in a field, returned, captured by a `spawn` closure) is refused by the existing escape rule with a diagnostic; probe for each of the three. - [ ] A mismatch (`var` closure parameter where the type has none, or the reverse) is a checker error that says which side is `var`. - [ ] The formatter (`torb format --check`), the language server's parameter hover and `docs/language` (the closure page) know the form; `docs check docs` green. - [ ] Checker, lowering and conformance tests for every bullet. ## Touches the seed? - **Yes, a syntax change** (`syntax/parser`, the `A binding needs a value` site in `parser/statements.trb`). CLAUDE.md "Seed and breaking changes": the first commit teaches the form; the seed does not know it, so **`compiler/src` and `std/` must not use the new form**, which keeps this a single commit with no seed refresh and no migration. A later use inside `std/` waits for a seed refresh, which is the owner's call (the loop never refreshes the seed). ## Size One PR, one or two Conventional Commits: `feat(syntax): a closure binds a var parameter` and, if the lowering needs its own change, `feat(ir): lower a closure with a var parameter`. The type form `(var value: Component) => Void` and a named `fn` passed to it already work, so the by-reference convention exists; the new work is the parser, the checker's parameter binding and the closure conversion. If the implementer finds the lowering needs more than that, stop and split instead of widening the PR. Implementer: opus/high (parser, checker, IR). ## Dependencies None. Part of #4.
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261007 until 2026-10-08T00:53:27Z

[loop:torbscript] claim implement by session:loop-torbscript-20261007 until 2026-10-08T00:53:27Z
Author
Collaborator

[loop:torbscript] implement: PR #161 (one commit 5510623 feat(syntax): a closure binds a var parameter, 22 files) is open against main, review requested from argus. The worker timed out before writing this result; gates per the PR body (tier A in a container, check-commits green, tier B left to CI). Forge CI on 5510623 is running. Two points for review are in the PR body: the parenthesized form { (var point, scale) => ... } from the acceptance criteria is a parse error pointing to { var point, scale => ... }, and the reverse var mismatch. Worker token usage is not measurable. Claim 599 released, next: review

[loop:torbscript] implement: PR https://git.torb.dev/torbscript/language/pulls/161 (one commit 5510623 `feat(syntax): a closure binds a var parameter`, 22 files) is open against main, review requested from argus. The worker timed out before writing this result; gates per the PR body (tier A in a container, check-commits green, tier B left to CI). Forge CI on 5510623 is running. Two points for review are in the PR body: the parenthesized form `{ (var point, scale) => ... }` from the acceptance criteria is a parse error pointing to `{ var point, scale => ... }`, and the reverse `var` mismatch. Worker token usage is not measurable. Claim 599 released, next: review
argus closed this issue 2026-10-08 00:09:38 +00:00
Author
Collaborator

[loop:torbscript] PR #161 merged by argus at 2026-10-08T00:09:38Z (merge commit d55c8d9). Issue closed; the PR has no after-merge release or deploy steps. Follow-up (not started): using the form inside std/ waits for a seed refresh (owner call).

[loop:torbscript] PR #161 merged by argus at 2026-10-08T00:09:38Z (merge commit d55c8d9). Issue closed; the PR has no after-merge release or deploy steps. Follow-up (not started): using the form inside `std/` waits for a seed refresh (owner call).
Sign in to join this conversation.
No labels
loop
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#156
No description provided.