Skip to content

feat: allow yield in fat-arrow functions (#2140)#2143

Merged
STRd6 merged 3 commits into
mainfrom
claude/issue-2140-20260604-1509
Jun 4, 2026
Merged

feat: allow yield in fat-arrow functions (#2140)#2143
STRd6 merged 3 commits into
mainfrom
claude/issue-2140-20260604-1509

Conversation

@STRd6

@STRd6 STRd6 commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Closes #2140.

Summary

Allows yield inside => arrow functions by compiling to a generator function expression bound to the enclosing this:

=> yield @x
###
(function*() {return  yield this.x}).bind(this)

When the arrow body doesn't reference this, the .bind(this) wrapper is omitted.

Implements the feature @edemaine sketched in #2140, matching CoffeeScript 1's => yield @ behavior.

Test plan

  • Full test suite passes (4293 passing)
  • New tests in test/yield.civet cover simple, @-property, parameters, multi-yield, assignment, async, yield*, and class-method cases
  • Existing "yield forbidden in arrow" tests updated to expect the new compiled output

🤖 Generated with Claude Code

Transforms `=> yield X` into `(function*() { return yield X }).bind(this)`,
preserving the lexical `this` semantics of the arrow.

When the body contains no `this` references, the `.bind(this)` wrapper is
omitted. Works with `async`, `yield*`, parameters, and multi-statement
arrow bodies.

This implements the long-requested generator-arrow feature referenced in
TC39 — CoffeeScript 1 supported the same shape via `=> yield @`.

Co-authored-by: Daniel X Moore <STRd6@users.noreply.github.com>
@STRd6

STRd6 commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author

@claude make code comments much more succinct

@claude

claude Bot commented Jun 4, 2026

Copy link
Copy Markdown

Claude finished @STRd6's task in 5m 30s —— View job


Making code comments more succinct

  • Reviewed comments in source/parser/function.civet
  • Shortened verbose comments (dropped doc block + 3 inline blocks)
  • Verified build + tests pass (4273 passing)
  • Commit pushed (3aea3c8)

Summary

Trimmed the verbose comments around transformArrowFunctionWithYield:

  • Dropped the multi-line JSDoc header in favor of a single-line one-liner showing the transform
  • Removed restating-the-code comments around insertReturn, brace-wrapping, and signature setup — the code reads clearly without them
  • Kept the one non-obvious WHY: the originalParent capture comment explaining the self-cycle hazard, condensed to two lines

Net: −30 lines comment, +3 lines comment.
Branch

Co-authored-by: Daniel X Moore <STRd6@users.noreply.github.com>
@greptile-apps

greptile-apps Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR implements yield support inside Civet => fat-arrow functions by compiling them to function* generator expressions, wrapping with .bind(this) when this is referenced in the body. It mirrors the CoffeeScript 1 => yield @ behaviour.

  • transformArrowFunctionWithYield in source/parser/function.civet mutates the arrow node in-place (type, children, signature), wraps it with makeLeftHandSideExpression, and conditionally appends .bind(this) based on whether a this token is found inside the block.
  • this detection uses gatherRecursiveWithinFunction, which stops at ArrowFunction node boundaries; this references that live exclusively inside a nested arrow in the outer body are invisible to the search, so the outer generator can miss a required .bind(this).
  • Test files in test/function.civet, test/infra/worker-pool.civet, and test/integration.civet are updated to replace the formerly-erroring => yield snippet with a getter-with-yield to keep error-path coverage intact.

Confidence Score: 3/5

Safe for direct this references but produces silently wrong runtime this binding when a nested arrow inside the generator body is the only place @x appears.

The transformation correctly handles the common case where the outer arrow body directly references @x or this. However, because gatherRecursiveWithinFunction halts at ArrowFunction boundaries, any @x that lives only inside a nested arrow within the outer generator body is invisible to the this-detection logic. In that pattern the outer generator is emitted without .bind(this), so the nested arrow inherits the wrong this at runtime.

source/parser/function.civet — specifically the gatherRecursiveWithinFunction call in transformArrowFunctionWithYield; test/yield.civet needs a corresponding test for the nested-arrow this case.

Important Files Changed

Filename Overview
source/parser/function.civet Adds transformArrowFunctionWithYield; the this-detection uses gatherRecursiveWithinFunction which stops at nested ArrowFunction boundaries, so a nested arrow using @x inside the outer => generator won't trigger .bind(this) on the outer generator.
test/yield.civet New test suite for yield-in-arrow; covers direct this references, parameters, multi-yield, async, and yield*, but misses the nested-arrow indirect this case.
test/function.civet Converted three throws tests to testCase assertions matching the new generator-expression output; straightforward and correct.
test/infra/worker-pool.civet Swapped the now-valid yield-in-arrow snippet for a getter-with-yield snippet to keep the errors-should-be-populated assertion passing.
test/integration.civet Same swap as worker-pool: uses a getter-with-yield to exercise the error-via-option path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[processSignature called on function node] --> B{f.generator?# AND hasYield?}
    B -- No --> Z[Done]
    B -- Yes --> C{f.generator? slot exists?}
    C -- Yes --> D[addGenerator = true]
    C -- No --> E{f.type is ArrowFunction?}
    E -- Yes --> F[transformArrowFunctionWithYield]
    E -- No --> G[Inject Error node into yield]
    F --> H{block.implicitlyReturned?}
    H -- Yes --> I[insertReturn block]
    H -- No --> J{block.bare?}
    I --> J
    J -- Yes --> K[Add braces to block]
    J -- No --> L[Build function* children]
    K --> L
    L --> M[f.type = FunctionExpression, mark generator]
    M --> N[makeLeftHandSideExpression f]
    N --> O{this token in block via gatherRecursiveWithinFunction?}
    O -- Yes --> P[Wrap: lhs.bind this]
    O -- No --> Q[lhs only]
    P --> R{newNode !== f AND originalParent?}
    Q --> R
    R -- Yes --> S[replaceNode + updateParentPointers]
    R -- No --> Z
    S --> Z
Loading

Comments Outside Diff (1)

  1. source/parser/function.civet, line 1288-1294 (link)

    P1 this detection stops at nested arrow-function boundaries

    gatherRecursiveWithinFunction uses isFunction as its skipPredicate, and isFunction returns true for ArrowFunction nodes (see traversal.civet line 23 / util.civet lines 139-146). That means any this (or @x) reference inside a nested => inside the outer generator block is invisible to this search.

    Concrete failure: in a class method, if the outer => generator's body contains inner := => @x but no direct @x in the outer body itself, the outer generator is emitted WITHOUT .bind(this). Calling the generator gives it this = undefined (strict) or global (sloppy), so inner's this.x is wrong at runtime. The fix is to replace gatherRecursiveWithinFunction here with a variant that descends into nested ArrowFunction nodes (since arrow functions share the enclosing this) while still stopping at FunctionExpression / MethodDefinition boundaries.

Reviews (1): Last reviewed commit: "refactor: shorten comments in transformA..." | Re-trigger Greptile

Comment thread test/yield.civet
Comment on lines +45 to +127
describe "yield in arrow function (#2140)", ->
testCase """
simple thick arrow with yield
---
=> yield 5
---
(function*() {return yield 5})
"""

testCase """
thick arrow with yield and this
---
=> yield @
---
(function*() {return yield this}).bind(this)
"""

testCase """
thick arrow with yield and @-property
---
=> yield @x
---
(function*() {return yield this.x}).bind(this)
"""

testCase """
thick arrow with parameter and yield
---
(x) => yield x
---
(function*(x) {return yield x})
"""

testCase """
thick arrow with multiple yields
---
=>
yield 1
yield 2
---
(function*() {
yield 1
yield 2
})
"""

testCase """
assigned thick arrow generator
---
gen := => yield @x
---
const gen =(function*() {return yield this.x}).bind(this)
"""

testCase """
async thick arrow with yield becomes async generator
---
async => yield 42
---
(async function*() {return yield 42})
"""

testCase """
yield* in thick arrow
---
=> yield* arr
---
(function*() {return yield* arr})
"""

testCase """
yield in arrow inside class method preserves this
---
class Foo
method()
gen := => yield @x
---
class Foo {
method() {
const gen =(function*() {return yield this.x}).bind(this);return gen
}
}
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Missing test: indirect this via nested arrow inside generator

The suite covers direct @x in the outer body, but not the case where @x lives exclusively inside a nested arrow within the outer => generator (e.g. => (inner := => @x; yield 5)). That pattern is the one affected by the gatherRecursiveWithinFunction boundary issue noted in function.civet, and adding a failing test here would lock in the expected .bind(this) behavior once the fix lands.

Comment thread test/yield.civet
---
=> yield 5
---
(function*() {return yield 5})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Double space in return yield output

The expected output (function*() {return yield 5}) contains two spaces between return and yield. This appears in every single-expression arrow-generator test. It comes from insertReturn appending "return " (with trailing space) when the existing yield token already carries a leading space. The generated JS is still valid, but the redundant whitespace is unexpected and would look odd in source maps or formatted output. It is worth tracking down where the extra space originates and trimming it.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@codecov

codecov Bot commented Jun 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (e7e5223) to head (c734bb3).

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #2143   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           57        57           
  Lines        27649     27691   +42     
  Branches      4590      4599    +9     
=========================================
+ Hits         27649     27691   +42     
Files with missing lines Coverage Δ
source/parser/function.civet 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@STRd6

STRd6 commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author

@claude ensure typecheck passes

@claude

claude Bot commented Jun 4, 2026

Copy link
Copy Markdown

Claude finished @STRd6's task in 10m 56s —— View job


Ensuring typecheck passes

  • Read CLAUDE.md and understand repo setup
  • Install dependencies and build
  • Run typecheck and identify failures
  • Fix the 2 regressions in this PR
  • Verify tests still pass (4273 passing)
  • Commit and push (c734bb3)

Summary

The PR introduced 2 typecheck regressions in source/parser/function.civet:

1272:29 TS2345 Argument of type 'ASTNodeObject' is not assignable...
1273:35 TS2345 Argument of type 'ASTNodeObject' is not assignable...

replaceNode and updateParentPointers both expect ASTNodeParent (= ASTNodeObject & { children: Children }), not bare ASTNodeObject. The cast on originalParent at function.civet:1261 was too loose:

-  originalParent := f.parent as ASTNodeObject?
+  originalParent := f.parent as ASTNodeParent?

A parent always has children, so ASTNodeParent is the correct type — narrowing here also gives the two call sites below the type they need.

Verification

  • pnpm typecheck:diff against origin/main: Net +0, Broke 0, Fixed 0 (was Net +2, Broke 2 before the fix)
  • pnpm test: 4273 passing, 19 pending
    Branch

The originalParent cast was ASTNodeObject?, but replaceNode and
updateParentPointers expect ASTNodeParent? (which adds the children
constraint). A parent node always has children, so ASTNodeParent is the
right type.

Co-authored-by: Daniel X Moore <STRd6@users.noreply.github.com>
@STRd6 STRd6 merged commit 05f5b37 into main Jun 4, 2026
8 checks passed
@STRd6 STRd6 deleted the claude/issue-2140-20260604-1509 branch June 4, 2026 21:32
@edemaine

edemaine commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Awesome, thanks!

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.

Suggestion: allow yield in arrow functions

2 participants