Skip to content

fix(hy-emit): fill an elided literal's head through parentheses (fixes #630) - #631

Merged
limitlessprojects merged 1 commit into
limitlessprojects:mainfrom
Leimiu-G:fix/paren-elided-literal
Oct 18, 2026
Merged

limitlessprojects merged 1 commit into
limitlessprojects:mainfrom
Leimiu-G:fix/paren-elided-literal

Conversation

@Leimiu-G

Copy link
Copy Markdown
Contributor

Fixes #630.

What

hy-emit/src/expr.rs::emit_expr_exp is the only place that fills an elided composite literal's head
from its surrounding type. It matched the expression directly, so a redundant parenthesis put the
literal out of reach of the ty: None / elem: None arms, expected was discarded, and the emit
fell through to the "spell the type" refusal — on a program hyb check passes with 0 diagnostics.

m := map[string]svc{"a": ({replicas: 2, tag: "x"})}   // check: OK   build: error [emit]
m := map[string]svc{"a": {replicas: 2, tag: "x"}}     // check: OK   build: OK

Peel Paren before the expected-type dispatch:

 let mut lit = e;
 while let Expr::Paren { inner, .. } = lit { lit = inner; }
 match lit {
     Expr::StructLit { ty: None, fields, .. } => { /* fill head */ }
     Expr::SliceLit  { elem: None, items, .. } => { /* fill head */ }

Paren is documented as span-only (hy-syntax/src/ast.rs: "kept for faithful spans"), so the
elision has to apply identically through it. Dropping the parens from output is safe at all three
call sites — each is an element slot of a composite literal, where Go gives parentheses no meaning.
The peel is local to emit_expr_exp on purpose: Self::peel_parens is private to stmt.rs today,
and #619 promotes it separately, so this PR keeps to one hunk in one file.

Verification

Emitted Go is byte-identical with and without the parens, and the program output matches:

$ hyb build elided2.hy -o out && ./out      # map[a:{2 x}]
$ hyb build elided1.hy -o out && ./out      # map[a:{2 x}]

Tests — two new pins next to the existing diff_elided_map_value_struct_literal:

  • diff_elided_map_value_struct_literal_under_parens
  • diff_elided_slice_elem_struct_literal_under_parens (also covers double parens)

Falsifiable: with expr.rs reverted and the tests kept, exactly those two fail and the other
171 pass — no collateral, no test that passes without the fix.

with fix:      test result: ok.     173 passed; 0 failed
without fix:   test result: FAILED. 171 passed; 2 failed   # the two new ones

Workspace and differential corpus on 28754ab (patched):

  • cargo test --workspace — all suites ok, 0 failed
  • cargo test -p hy-emit --test '*' — ok
  • tools/differential_loop.py — accept-side 21/23, reject-side 56/56

The 2 accept-side misses are the local environment, not this change: both are a7-* cgo fixtures
that fail assembling under a non-ASCII %TEMP% (can't create C:\Users\????\AppData\Local\Temp\ go-build...\_x006.o). They fail identically on unpatched main.

Related

Same invariant as #618 (parenthesized receiver loses its type) and #609 (line-leading ( swallowed
as an argument list) — the 8th independent locus where a site dispatches on Expr shape without
peeling first.

…tlessprojects#630)

`emit_expr_exp` is the only site that gives a type-elided composite
literal its head from the surrounding type, and it matched the
expression directly. A redundant parenthesis therefore put the literal
out of reach of the `ty: None` / `elem: None` arms, the expected type
was discarded, and the emit fell through to the "spell the type"
refusal - on a program `hyb check` accepts with 0 diagnostics:

    struct svc { replicas int
                 tag      string }

    fn main() {
        m := map[string]svc{"a": ({replicas: 2, tag: "x"})}
        fmt.println("{m}")
    }

    $ hyb check elided.hy   ->  OK (0 diagnostics)
    $ hyb build elided.hy   ->  error [emit]: not yet supported:
                                type-elided struct literal - spell the type

Without the parens the same program builds, runs, and prints
`map[a:{2 x}]`; with the peel it prints the same thing.

`Paren` is documented as span-only and semantically transparent
(`hy-syntax/src/ast.rs`: "kept for faithful spans"), so the elision has
to apply identically through it. Dropping the parens from output is safe
at every call site: `emit_expr_exp` is only reached from the element
slots of composite literals, where Go gives parentheses no meaning.

The peel stays local to `emit_expr_exp` on purpose - `Self::peel_parens`
is private to `stmt.rs` today and limitlessprojects#619 promotes it separately, so this
change is one hunk in one file and does not touch that helper.

Tests: `diff_elided_map_value_struct_literal_under_parens` and
`diff_elided_slice_elem_struct_literal_under_parens` (the latter also
covers double parens) fail without the peel; `diff_elided_map_value_
struct_literal` pins the unparenthesised spelling unchanged.

Verified on 28754ab: `cargo test -p hy-emit --lib` 173 passed / 0 failed
(171 before, +2); with expr.rs reverted and the tests kept, exactly the
two new ones fail and the other 171 pass; `cargo test --workspace` all
suites ok; `tools/differential_loop.py` accept-side 21/23, reject-side
56/56 - the two misses are the local non-ASCII %TEMP% cgo fixtures
(`a7-borrow-by-default`, `a7-own-transfer-at-return`), which fail the
same way on unpatched main.
@limitlessprojects
limitlessprojects merged commit b8e850b into limitlessprojects:main Oct 18, 2026
2 checks passed
@limitlessprojects

Copy link
Copy Markdown
Owner

Landed as two-parent merge b8e850b onto main (worktree merge of pr-631 @ de0a97a, GitHub marks this PR merged). Thank you @Leimiu-G — clean, minimal, and exactly the right fix.

Live-bypass probe on main @ 55cd957 (before landing): confirmed LIVE — check-accepts/emit-rejects gap:

scenario check build
map[string]svc{"a": ({replicas: 2, tag: "x"})} OK (0 diagnostics) REJECT type-elided struct literal — spell the type
[]svc{({replicas: 2}), (({replicas: 3}))} OK REJECT same
controls without parens (map value / slice elem) OK built OK

Post-merge verification on b8e850b: both paren scenarios now emit and build (service{replicas: 2, tag: "prod"} etc.); unparenthesized controls unchanged.

Battery on merged tree: cargo test --workspace 1386/0 (+2 = the PR's two tests), corpus 100/100 verdict agreement (tripwire quiet), differential FULL 34+66.

Closes #630. Credit: Leimiu-G.

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.

[soundness] P1 (hy-emit): a parenthesized type-elided literal is refused — check accepts, build fails

2 participants