Skip to content

A brace block on a name that is also a local fails to parse inside parentheses #905

Open

Description

@tannevaled

A brace block attached to a name that is also a local variable fails to parse when the call is inside parentheses. MRI calls the method and ignores the local.

def withblock; yield; end
withblock = 5
p(withblock { 7 })
$ /opt/homebrew/opt/ruby@4.0/bin/ruby probe.rb
7

$ rbgo probe.rb
parse error at line 3: expected ), got "{" ({)

Measured through scripts/conformance/rubyspec/cmd/oracle against
ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [arm64-darwin25], with rbgo built from e5619211 (current main).

It needs all three lines

The brace block alone is fine — this is where the defect is not:

def withblock; yield; end
p(withblock { 7 })
=== 3 engines AGREED, byte for byte ===
7

The assignment withblock = 5 is what breaks it. Once the parser knows the name as a local, withblock { 7 } inside (...) stops parsing. A block makes a receiver-less name an FCALL regardless of any local of that name — primary: fcall brace_block (parse.y-ruby_4_0:4458-4463) — so MRI resolves withblock to the method, yields, and prints 7.

Whether the parenthesis is essential or merely how it surfaces here is not established; the three lines above are the smallest form I measured.

Not a regression

rbgo at e5619211 and the branch of #904 fail identically, so this predates #904. I found it while writing that PR's test and nearly asserted the MRI answer as a row from memory — the probe is what caught it.

Where it is likely to matter

Any DSL or test helper whose block-taking method shares a name with a local in
the same scope, written with parentheses. The shape is ordinary Ruby:

config = load_config
p(config { ... })

Scope

Parser-side (go-ruby-parser), not the compiler: the failure is at parse time
and no bytecode is produced. The compiler's own local-vs-method rule for the
parenthesised, blockless case is #904 and is separate from this.

🤖 Generated with Claude Code

Activity

  1. tannevaled commented on Nov 9, 2026

    @tannevaled
    ContributorAuthor

    Prior art in go-ruby-parser/parser, so whoever takes this starts from it

    priorart --repo go-ruby-parser/parser "brace block parenthesised call" — 38 tracker entries. Three matter, and one nearly covers this.

    ⛔ It is NOT one of the nine gaps in parser#27, which is closed

    parser#27 catalogued "Nine syntax gaps that make 12 ruby/spec language files unparseable" and is closed. This case is not among them. Gap 9 is the nearest neighbour, and the error shapes are close to mirror images:

    parser#27 gap 9   p(-> a=a() { a }.call)              expected {, got ")"
    this issue        withblock = 5; p(withblock { 7 })   expected ), got "{"
    

    Gap 9 is a brace body after a default argument that calls a method, inside a lambda's parameter list. This one is a brace block after an identifier the parser already knows as a local, inside a call's parentheses — and it needs the assignment: without withblock = 5, p(withblock { 7 }) parses and three engines agree on 7.

    So: an uncovered case, not a regression of a known one.

    The neighbouring machinery

    parser#32 (closed) give a do block its own CMDARG state, separate from COND — the block/lexer-state interaction, for do rather than braces
    parser#37 (closed) refuse a paren-less command call in an arg position
    parser#41 / #43 (closed) Call cannot distinguish foo from foo() → Call.Paren

    ⭐ #41/#43 are worth noting for a second reason: Call.Paren is what made #904 possible — the compiler could finally tell foobar from foobar(). The same distinction the parser learned to record is the one it mis-parses here when a brace block follows.

    That the identifier must be a known local for the failure to appear points at the lexer's lvar state rather than at the grammar for brace_block, which is the same family #32 worked in.

    Not implemented here

    A grammar/lexer-state change in a parser with 38 prior entries deserves its own pass with a reproduction harness, not an append to an unrelated session. The measurement in the issue above stands: three lines, rbgo at e5619211 and later fail identically, MRI prints 7.

    🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions