Skip to content

fix(hy-emit): resolve and emit a parenthesized receiver like a bare selector (fixes #618) - #741

Open
airdropia wants to merge 1 commit into
limitlessprojects:mainfrom
airdropia:fix/paren-receiver-618
Open

airdropia wants to merge 1 commit into
limitlessprojects:mainfrom
airdropia:fix/paren-receiver-618

Conversation

@airdropia

@airdropia airdropia commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

The defect (from #618)

(hits).add(1) is accepted by hyb check with 0 diagnostics, then hyb build emits it verbatim and go build fails:

(hits).add undefined (type "sync/atomic".Int64 has no field or method add,
                      but does have method Add)

The type is known to the emitter — Go's own diagnostic names sync/atomic.Int64 — so the shape table had the answer and never got asked: every spelling decision pattern-matched the receiver directly, an Expr::Paren wrapper matched none of them, resolution returned None, and each path fell through to "emit the name verbatim".

The fix

peel_parens / unwrap_parens already existed (added for the O8 work) and were simply not applied at these entry points:

where change
hy-emit/src/expr.rs::type_of_expr peels at entry; the user-struct field route peels the base before the var_struct lookup
hy-emit/src/stmt.rs::member_name peels the base before the package-root / foreign decision
hy-emit/src/expr.rs::emit_call the package-qualified, shape/foreign and associated-fn branches peel the base
hy-emit/src/expr.rs Expr::Field arm new selector_base peels a redundant receiver paren, so (x).f emits exactly x.f — byte-identical to the bare form

selector_base drops the parens only when the inner expression is itself a primary expression (Ident / Field / Index), because (*p).f and (a + b).f need them. That is the same whitelist the .pop() lowering already uses, so there is one rule for "is this receiver paren redundant".

Result for the report's three shapes:

source emitted before emitted now
(hits).add(1) (hits).add(1) → Go error hits.Add(1)
(stats).mu.lock() (stats).mu.lock() → Go error stats.mu.Lock()
(s.hits).add(2) (s.hits).add(2) → Go error s.hits.Add(2)

Verification

compiler/crates/hy-emit/tests/shapes_conversions_and_atomics.rs gains one test that pins all three shapes byte-for-byte and asserts no redundant paren survives:

assert!(go.contains("hits.Add(1)"));
assert!(go.contains("s.hits.Add(2)") && go.contains("s.mu.Lock()"));
assert!(!go.contains("(hits)") && !go.contains("(s.hits)") && !go.contains("(s)."));

That is the byte-for-byte check the report asked for instead of "it builds now".

Honest limits. This machine has no Rust or Go toolchain, so I could not compile or run anything locally: CI is the oracle for the build, the new test, and the existing suite. If anything is red, that is real signal and I will fix it rather than argue. Also: no corpus fixture is added here, so the go build leg of the differential loop does not exercise a parenthesized receiver. Happy to add an ACCEPT fixture (plus its VERDICTS.md row and the count updates that go with it) if you want that end-to-end leg rather than the emission assertion.

Credit for the report and its root-cause analysis is #618's.


Duplicate disclosure: #619 claims the same issue

There is a competing open PR for the same issue, #619 (fix(hy-emit): resolve and emit a parenthesized receiver in its own type, opened 2026-10-18T10:26Z, CI green but CONFLICTING against current main). I did not see it before opening this — that is my miss, and per BOUNTY.md the first reporter takes the payout, so the maintainer should treat that as the tie-break, not this PR.

Posting it anyway because the two fixes are not identical and the difference matters for which one is cheaper to land:

#619 #741 (this)
resolves through parens yes (three normalization routes) yes: type_of_expr entry + struct-field route, member_name, three emit_call branches
emitted Go — (x).f emits exactly x.f via a selector_base helper that only peels when the inner expression is a primary expression (Ident/Field/Index), so (*p).f and (a + b).f keep the parens they need — the same whitelist .pop() already uses
merge state CONFLICTING MERGEABLE, rebased on current main (aa4a9d9)
test — one test in shapes_conversions_and_atomics.rs pinning all three shapes byte-for-byte plus a no-redundant-paren assertion

If you prefer #619, close this one; if you prefer this, it needs no rebase. Either way credit is yours to assign.

…elector (limitlessprojects#618)

`(hits).add(1)` was accepted by `hyb check` with 0 diagnostics and then emitted
verbatim, so `go build` failed:

    (hits).add undefined (type "sync/atomic".Int64 has no field or method add,
                          but does have method Add)

The type WAS known — `type_of_expr`, `member_name` and the four `emit_call`
dispatch branches each pattern-matched the receiver directly, so an
`Expr::Paren` wrapper matched none of them, resolution returned `None`, and
every path fell through to "emit the name verbatim". `peel_parens` /
`unwrap_parens` already existed for the O8 work; they were simply not applied
at these entry points.

- `type_of_expr` peels at entry, and the user-struct field route peels the
  base before the `var_struct` lookup
- `member_name` peels its base before the package-root / foreign decision
- the `emit_call` package-qualified, shape/foreign and associated-fn branches
  peel the base
- a new `selector_base` peels a redundant receiver paren in the `Expr::Field`
  emission arm, so `(x).f` emits exactly `x.f` — byte-identical to the bare
  form, which is what makes the fix verifiable rather than "it builds now".
  The parens are kept when the inner expression is not a primary expression,
  because `(*p).f` and `(a + b).f` need them; this is the same whitelist the
  `.pop()` lowering already uses.

Test: `compiler/crates/hy-emit/tests/shapes_conversions_and_atomics.rs` pins
all three shapes from the report — atomic local, atomic struct field, and a
struct receiver — byte-for-byte, and asserts no redundant parens survive.

No Rust/Go toolchain exists on the machine this was written on, so CI is the
oracle for the build (and for the byte-exact assertions above).
@Leimiu-G

Copy link
Copy Markdown
Contributor

I wrote #618 (the report this fixes) and also had my own take on it — closing mine (#619) in favour of
this one. Your selector_base whitelist is the part I got wrong, so this is the stronger fix, and I
verified it rather than taking it on trust.

Built f936449 from the tarball (HYB_ALLOW_TOOLCHAIN_DRIFT=1) and went past hyb build to the Go
toolchain and the running binary:

shape emitted go build runs
(hits).add(1) hits.Add(1) ok hits: 1
(stats).mu.lock() stats.mu.Lock() ok hits: 2
(&e).replicas (&e).replicas — parens kept ok sum: 4

One suggestion, since your own test may not cover it. The body names (*p).f and (a + b).f as
the shapes that need their parens, but neither is reachable from HYB source today — there is no
postfix deref expression, and structs are not addable — so the whitelist's AddrOf case is the one
that actually fires. It is worth pinning explicitly, because it is the case where a naive
"postfix receiver never needs parens" peel silently emits &e.replicas (= &(e.replicas), type
*int) instead of (&e).replicas (type int). That still type-checks in some positions and does not
in others:

struct inner { replicas int }

fn main() {
    e := inner{replicas: 3}
    v := (&e).replicas
    fmt.println("sum: {v + 1}")     // a naive peel: invalid operation: v + 1 (mismatched types *int and untyped int)
}

So a pin on (&e).f → parens kept (and the same for (&e).f.g) would make the whitelist falsifiable
rather than merely present. Your assert!(!go.contains("(hits)")) style would work fine in reverse
here.

Credit note: #618's three reproductions are the report; this PR is the fix, and the whitelist is your
refinement. Good catch on the redundant-paren rule — it is more precise than what I proposed.

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