Skip to content

interp: refuse a return the macro cannot place, at the return - #615

Open
notactuallytreyanastasio wants to merge 1 commit into
temperlang:mainfrom
notactuallytreyanastasio:fix/return-from-lambda
Open

notactuallytreyanastasio wants to merge 1 commit into
temperlang:mainfrom
notactuallytreyanastasio:fix/return-from-lambda

Conversation

@notactuallytreyanastasio

Copy link
Copy Markdown
Contributor

A return inside a lambda with a declared result type, (): T => ...,
never desugars, and no error says so. That includes every generator
and async block, which are typed lambdas. The return macro looks only
for a function declared with fn or let, so that a return lands
somewhere predictable rather than at the end of a lambda that some
control flow desugared into. A grammar lambda is not one, so the macro
found nothing and returned a bare Fail that logged nothing. The
return call survived the syntax macro stage,
promoteOutTypesToReturnDecls declared the lambda's result variable
under the parsed name return, and name resolution bound the leftover
call to it. return 1; became a call of the lambda's own result
variable, and the error showed up somewhere else.

At origin/main, with let call(g: fn (): Int): Int { g() }:

let r = call { (): Int =>
  if (true) { return 1; }
  2
};

@T: return__6 is not initialized along branches at [:3+14]
py:   TypeError: 'NotImplementedType' object is not callable
rust: error[E0618]: expected function, found `!`

The same lambda inside let wrap(): Int { call { ... } }:

@T: break/continue not within a matching block
js:   SyntaxError: Undefined label 'fn_5'
py:   NameError: name '_fn' is not defined
rust: error[E0426]: use of undeclared label `'fn__6`

if (i == 1) { return; } inside a
{ (): GeneratorResult<Empty> extends GeneratorFn => ... } block gives
the same "return__6 is not initialized" and the same runtime failures.
A top-level return; gives "@g: Expected function type, but got
Function" and "Cannot translate Invalid", and js prints "before" and
then throws.

After, each is one error at the return, in the syntax macro stage:

[-work/t.temper:3+14-22]@S: `return` inside a lambda with a declared
result type would leave the enclosing function, not the lambda. Make
it a function with `fn` or `let`, or end a generator with `break` or
by reaching its end

[-work/t.temper:2+0-6]@S: Return outside function body

The second is ReturnOutsideFn, which the macro already built and then
dropped unless the stage was past SyntaxMacro. The macro runs only in
that stage, so it never logged.

Returning from a function through a body that control flow desugared
into a lambda is unchanged. These print the same at origin/main and
with this change on js, py, rust and java: a typed fn lambda
returning, a let function nested in another, a typed lambda holding
a fn that returns, a getter, a static method, a return in an
orelse do { ... }, in when arms, in a do block and in a while
loop.

Giving the lambda's result variable a fresh name instead breaks every
typed lambda, because later passes recognize that declaration by its
parsed name, so the fix is in the macro.

New stage test syntax-macro/return-from-lambda: three ReturnFromLambda,
one ReturnOutsideFn, and nothing for a for-of return. It fails with the
ReturnDesugarMacro change reverted. :log:jvmTest 33, :interp:jvmTest
76, :frontend:jvmTest 704 (InterpreterFunctionalTests included),
:be-js:jvmTest 197, the py, rust and java 17 functional suites at 64
each, :build-user-docs:test 76, 0 failures; ktlintCheck and detekt
pass.

What does not work: whether return in a generator or async block
should end that block is a language question this does not answer; it
only says why it is not allowed. A program with these errors still
emits code, as any program with errors does, and that code panics when
it reaches the error.

🤖 Generated with Claude Code

A `return` inside a lambda with a declared result type, `(): T => ...`,
never desugars, and no error says so. That includes every generator
and async block, which are typed lambdas. The return macro looks only
for a function declared with `fn` or `let`, so that a return lands
somewhere predictable rather than at the end of a lambda that some
control flow desugared into. A grammar lambda is not one, so the macro
found nothing and returned a bare Fail that logged nothing. The
`return` call survived the syntax macro stage,
promoteOutTypesToReturnDecls declared the lambda's result variable
under the parsed name `return`, and name resolution bound the leftover
call to it. `return 1;` became a call of the lambda's own result
variable, and the error showed up somewhere else.

At origin/main, with `let call(g: fn (): Int): Int { g() }`:

    let r = call { (): Int =>
      if (true) { return 1; }
      2
    };

    @t: return__6 is not initialized along branches at [:3+14]
    py:   TypeError: 'NotImplementedType' object is not callable
    rust: error[E0618]: expected function, found `!`

The same lambda inside `let wrap(): Int { call { ... } }`:

    @t: break/continue not within a matching block
    js:   SyntaxError: Undefined label 'fn_5'
    py:   NameError: name '_fn' is not defined
    rust: error[E0426]: use of undeclared label `'fn__6`

`if (i == 1) { return; }` inside a
`{ (): GeneratorResult<Empty> extends GeneratorFn => ... }` block gives
the same "return__6 is not initialized" and the same runtime failures.
A top-level `return;` gives "@g: Expected function type, but got
Function" and "Cannot translate Invalid", and js prints "before" and
then throws.

After, each is one error at the `return`, in the syntax macro stage:

    [-work/t.temper:3+14-22]@s: `return` inside a lambda with a declared
    result type would leave the enclosing function, not the lambda. Make
    it a function with `fn` or `let`, or end a generator with `break` or
    by reaching its end

    [-work/t.temper:2+0-6]@s: Return outside function body

The second is ReturnOutsideFn, which the macro already built and then
dropped unless the stage was past SyntaxMacro. The macro runs only in
that stage, so it never logged.

Returning from a function through a body that control flow desugared
into a lambda is unchanged. These print the same at origin/main and
with this change on js, py, rust and java: a typed `fn` lambda
returning, a `let` function nested in another, a typed lambda holding
a `fn` that returns, a getter, a static method, a return in an
`orelse do { ... }`, in `when` arms, in a `do` block and in a `while`
loop.

Giving the lambda's result variable a fresh name instead breaks every
typed lambda, because later passes recognize that declaration by its
parsed name, so the fix is in the macro.

New stage test syntax-macro/return-from-lambda: three ReturnFromLambda,
one ReturnOutsideFn, and nothing for a for-of return. It fails with the
ReturnDesugarMacro change reverted. :log:jvmTest 33, :interp:jvmTest
76, :frontend:jvmTest 704 (InterpreterFunctionalTests included),
:be-js:jvmTest 197, the py, rust and java 17 functional suites at 64
each, :build-user-docs:test 76, 0 failures; ktlintCheck and detekt
pass.

What does not work: whether `return` in a generator or async block
should end that block is a language question this does not answer; it
only says why it is not allowed. A program with these errors still
emits code, as any program with errors does, and that code panics when
it reaches the error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Bobby Grayson <bobbbygrayson+github@gmail.com>
@mikesamuel

Copy link
Copy Markdown
Contributor

Can we have a test that you can still return from a lambda that is inlined, i.e. that this check does not happen too early.

These two are equivalent:

export let f(xs: List<Int32>): Int32 {
  for (let x of [1, 2, 3]) {
    if (x == 2) { return x }
  }
  -1
}

// ----

export let g(xs: List<Int32>): Int32 {
  xs.forEach { x =>
    if (x == 2) { return x }
  }
  -1
}

Some of the iterator style methods are marked with a decoration (@inlineUnrealizedGoal or something like that) so that break, continue, and other transfers of control can bind properly after the InlineToRepairUnrealizedGoals μpass runs.

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.

2 participants