Gen/fix wasm src2json - #368
Merged
Merged
Conversation
Adds Scope.loc covering full body extent (begin = first body indent,
end = last element end) since owner.loc is head-only and cannot serve
position->scope lookups. Set in parse_indent_block and Program parse.
gen: handle Loc in gen_ast2{ts,rust} ScopeDef loops; gen_ast2rust now
also copies branch_root from raw_scope (was previously dropped).
lsp: implement analyzeCompletion — keywords, builtin types (bool/void
+ common uN/iN/fN widths as hints), scope-walked visible idents via
prev chain (mirrors C++ Scope::lookup_backward), and type-driven
member access. Member access dispatch:
- enum type name -> enum members
- enum value -> .is_defined builtin
- array -> .length builtin
- struct/format -> fields + cast fns
- struct_union -> fields from all variants
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirrors lsp/server: registers a CompletionItemProvider on the brgen language with `.` trigger, calls analyze.analyzeCompletion with member-access detection (scan back from cursor over ident chars, look for `.`), maps the stub kinds straight to monaco's enum. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Contributor
|
Unable to trigger custom agent "Code Reviewer". You have run out of credits 😔 |
Contributor
|
You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool. What Enabling Code Scanning Means:
For more information about GitHub Code Scanning, check out the documentation. |
…ntain permissions' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Contributor
Workflow Scan🟡 1 Medium 🟡 Medium - 1 finding
47 | - name: Checkout repositoryComment |
Owner
Author
|
/autofix |
Owner
Author
|
/autofix |
…to gen/fix-wasm-src2json
…new test case for TaskID format
…types" This reverts commit 15c4947.
…rty and adjust visit function for error handling in dependency
- expression.cpp: detect struct method with FunctionKind::CAST returning an int-like type when building a TYPE_CAST from STRUCT to INT/UINT/USIZE (e.g. implicit `length.u32()` lowering for `types :[length]ValType` where length is a Uint32 struct). - helper.cpp: set cast_call placeholder in make_cast when cast_kind is FUNCTION_CAST so add_cast_func transform has a slot to fill. - encode.cpp: wrap dynamic-array len_init in EBM_CAST to counter_type, mirroring the decode path; fixes wasm_src2json encode emission. - example/wasm.bgn: drop bogus `return value` inside Uint32.decode whose return type is the opaque DECODER_RETURN; the value was being read as the status code at runtime.
- ebmcodegen LITERAL_STRING: pass EscapeFlag::hex to escape_str so NUL and other control bytes are emitted as \xNN, fixing Python/Zig/Go source parse errors for `magic :"\x00\x61\x73\x6d"` style binary patterns. - ebm2python Statement_FUNCTION_DECL: only inject `self` when the function has a parent_format; top-level fns like `fn checkSum(data :[]u8)` in ip.bgn used to emit `def checkSum(self, data: ...)` which is wrong. - ebm2go function definition wrapper: when a PROPERTY accessor's parent_struct differs from parent_format (inner anon struct generated by if/else field branches), prepend the inner struct identifier to the method name (e.g. tmp318Max). Avoids `Limits.Max already declared` while keeping the receiver as the outer format. - example/wasm.bgn: remove leftover `return value` inside Uint64.decode (Uint32.decode was already fixed); the framework owns the encode/decode return value, so the user expression was meaningless. After these: - ebm2python wasm_src2json: PASS - ebm2go wasm_src2json: remaining failures are Go-strict loop counter type mismatch and a missing receiver prefix inside property accessor bodies (separate visitor bugs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…es differ The cast_call branch previously returned the method invocation result as-is. For STRUCT->INT FUNCTION_CAST (e.g. Uint32.u32() -> u32 used in contexts wanting USIZE), the surrounding code in strict-typed backends saw a uint32 where it expected int, producing Go errors like `invalid operation: tmp < r.Length.U32() (mismatched types int and uint32)`. When the cast_call's result type does not match the destination type of the TYPE_CAST node, wrap it in an outer target-type cast. Backends with func_style_cast=true emit `int(r.Length.U32())`; others emit `(int)r.Length.U32()`. Backends that don't care about strict typing (Python/Ruby/C) are unaffected by the no-op cast in their output. Uses ctx.get() + MAYBE() to fetch the call expression; a missing expression at that point is a bug, not a silent fallthrough. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…eiver Statement_FUNCTION_DECL emits accessors for properties whose parent_struct differs from parent_format with a tmpNNN prefix on the outer format's method set (e.g. tmp318Max declared on *Limits). Match the call site in Expression_MEMBER_ACCESS: - Prefix the member identifier with the inner struct's identifier so the call resolves to the prefixed method (Max -> tmp318Max). - Reset base to the outer self_value so the call is `l.tmp318Max(...)` rather than `l.tmp458.tmp318Max(...)`, which fails because the inner-anon-struct holder type (Variant249) does not carry that method on its method set. After this, the previous `l.tmp458.Max undefined` and the inner-struct chain mismatch errors are gone. Remaining failures are around inner-struct instance variables (`tmp749`) referenced without definition — separate INIT_CHECK / type-assertion lowering issue. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The MEMBER_ACCESS fallback for struct_union member access emitted a bare
`tmp{type_id}` identifier (e.g. `tmp749.tmp444`) that was never declared
in the enclosing scope. The implicit assumption was that some preceding
INIT_CHECK had created it, but the INIT_CHECK in the lowered getter body
defines a differently-tagged variable (tmp{expect_value_type_id}), so
the outer chain entry had no binding and Go failed with
`undefined: tmp749`.
Replace the bare reference with an inline Go type assertion on the
base: `(l.tmp458.(*Tmp749)).tmp444`. This is safe because the enclosing
match/if branch already guarantees the variant tag — the assertion will
not panic in that branch.
With this fix ebm2go wasm_src2json passes 3/3 option sets.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
derive_property's getter/setter body construction emitted a single
INIT_CHECK based on the leaf field's parent_struct. When that field
lived inside an inner anonymous struct nested in another variant (e.g.
Limits.max: optional inside Uint64/Uint32 branch struct, itself a
member of the memory_64 variant), only the inner-most INIT_CHECK was
emitted and the outer variant narrowing was missing. Backends that map
variants to runtime-typed values (Go's `any`) then referenced
undefined intermediate variables like `tmp749`.
Encode/decode lowering already handles this via recursive IndentBlock
processing calling handle_variant_alternative per level. Replicate the
same per-level emission in derive_property by walking from
field.parent_struct outward to property.parent_format, emitting one
INIT_CHECK per variant containment step (outer-most appended first so
intermediate `tmp{type_id}` variables are defined before being
referenced by inner INIT_CHECK target_field expressions).
Robustness: use chained MAYBE on the `expected<optional<pair>>` result
so a missing optional bails with a source-located error instead of
dereferencing UB. Same applies to the related_field FIELD_DECL access.
Revert the ebm2go inline type-assertion workaround in
Expression_MEMBER_ACCESS_before_class.hpp — the original variant_hold
pattern (`tmp{type_id}.member`) now resolves correctly because the
multi-level INIT_CHECK defines each `tmp{type_id}` before use.
After this:
- ebm2go wasm_src2json: still PASS 3/3 (now via clean variant_hold path)
- ebm2c / ebm2python / ebm2ruby / ebm2rmw: PASS unchanged (no regression)
- ebm2cpp / ebm2rust / ebm2zig: still FAIL (separate per-backend issues)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
For pure VARIANT types (common_type=nil), the default Type_VARIANT
visitor returns just "Variant{id}" without declaring the enum, so
references in function signatures (e.g. Option<Variant43>) failed
with "cannot find type". Add a variant_type_custom hook that emits
the enum + impl with get_v0/get_mut_v0/... accessors, mirroring the
existing struct_union enum generation in Statement_FIELD_DECL.
Tracks emitted variants in declared_variants to avoid duplicates.
Expression_TYPE_CAST_class overrode the default visitor without
handling FUNCTION_CAST. Wrap the cast_call result in `(... as T)` when
the method's return type doesn't match the destination, fixing
`Uint32 as usize` non-primitive cast errors for implicit length casts.
Remaining ebm2rust failures: inner property accessors land in
inner-anon-struct impl blocks but their bodies reference outer-format
fields (self.tmp458 etc.) — needs accessor relocation to parent_format
impl + name prefixing (separate commit).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Major rework of how ebm2rust emits inner-anonymous-struct property
accessors so they live on the outer (parent_format) receiver instead
of their anon struct's own impl block where their body's self.* field
references would land on the wrong type.
Changes:
- ebmcodegen/stub/util.hpp: split emit_struct_methods into granular
emit_struct_properties, emit_struct_codec, emit_struct_user_methods
helpers (call-all wrapper kept for source compat). Lets backends
reorder / filter pieces.
- ebm2rust Statement_STRUCT_DECL_class:
* Anon inner structs (name nil) emit only their struct decl, no impl
block — their accessors are pulled up.
* Outer structs walk variant member chains to collect anon inner
descendants and emit their property accessors inside the outer
impl block.
- ebm2rust entry_before_class function_definition_start_wrapper:
prefix the method name with the inner struct identifier when
parent_struct != parent_format so multiple branches don't collide
on the outer impl (tmp318_max vs tmp342_max vs the merged max).
- ebm2rust Expression_MEMBER_ACCESS_before_class: when resolving a
PROPERTY whose parent_struct differs from parent_format, call
`self.<prefix>_<name>(args)` directly on the outer self instead of
letting default MEMBER_ACCESS chain through `self.tmp458.<name>(...)`
which would hit a variant enum that has no such method.
- ebm2rust Expression_TYPE_CAST_class: detect casts to/from pure
VARIANT types (common_type=nil, emitted as Rust enum) and use the
enum constructor or pattern match instead of `as`, which is invalid
for non-primitive types. Order this check before the FUNCTION_CAST
fallback so VARIANT destinations don't get a stray `as`.
Remaining ebm2rust wasm_src2json failures are now borrow/ownership
issues on the relocated accessor results (Option<&Uint64> being
deref'd to move, or written through `&` ref) — handled separately.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…eral for [u8;N] const
Final ebm2rust wasm_src2json fixes:
- Statement_STRUCT_DECL_class: derive Copy when all struct fields are
primitive scalars (UINT/INT/FLOAT/BOOL/ENUM). This lets wrapper
structs like `pub struct Uint32 { value: u32 }` be moved/copied
freely through field access without hitting E0507 on `.field` reads
through `&Struct`. Lifetime parameter doesn't preclude Copy since
scalar fields are still Copy under any lifetime.
- Expression_MEMBER_ACCESS_before_class: STRICT_TYPE property getters
return Option<&T>; emit `.unwrap().clone()` to produce an owned
value instead of `*ref` which would try to move from a shared ref
on non-Copy types (E0507). Applies to both the prefixed (inner-anon)
and main_logic paths.
- entry_before_class assignment_custom hook: when the assignment
target is a MEMBER_ACCESS resolving to a PROPERTY_DECL, rewrite
`*self.x_max().unwrap() = value` (which fails E0594 against a `&`
reference) into `self.x_set_max(value)?` calling the generated
setter. Reuses the same parent_struct prefix logic as the read path.
- entry_before_class variable_decl_custom hook: const with array<u8>
type initialized from a string literal was emitted as
`const NAME: [u8;N] = "..."` (type mismatch `&str` vs `[u8;N]`).
Recognize that shape and emit `const NAME: [u8;N] = *b"..."` with
hex-escaped bytes.
After this, ebm2rust wasm_src2json passes 2/2 option sets with no
regression in ebm2c / ebm2python / ebm2ruby / ebm2rmw / ebm2go.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pure VARIANT types (common_type=nil) were referenced by name in
function signatures (e.g. ?Variant43) but never declared, producing
"use of undeclared identifier" compile errors.
Add a variant_type_custom hook mirroring the ebm2rust one, emitting:
pub const Variant43 = union(enum) {
v0: Uint64,
v1: Uint32,
};
declared_variants set guards against duplicate emission when the
same VARIANT TypeRef is visited from multiple call sites.
After this, ebm2zig wasm_src2json compiles cleanly. Runtime still
fails with `Decode error: error.EndOfStream` — that's a deeper decode
lowering issue separate from variant type declaration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When the generated decode/encode fails at runtime, the harness was
swallowing the error trace ("Decode error: error.EndOfStream") and
exiting without any source location info. Zig's @errorReturnTrace()
returns the chain of `try` sites that propagated the error in debug
builds; dump it via std.debug.dumpStackTrace so the failing line +
file in generated.zig is visible.
This is what surfaced that ebm2zig's WasmModule.decode emits
`while (true) { try section.decode(...); }` for `sections :[..]Type`
without a stream-end check, causing infinite loop → EndOfStream.
That underlying bug is tracked separately.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rray-from-string fix
- Rename composite_field_decl_visitor -> composite_field_decl_custom
on Config + default Statement_COMPOSITE_FIELD_DECL visitor, switch
from MAYBE() to CALL_OR_PASS() so backend hooks can `return pass;`
to fall through to default behavior (per project convention of
*_visitor being deprecated in favor of *_custom).
- ebm2go: matching rename of the hook setter, no logic change.
- ebm2cpp: add variable_decl_custom hook to emit
`constexpr std::array<std::uint8_t, N> NAME = {0xNN, ...};`
for const-array-of-u8 with string-literal initializer, instead of
the default `= "string"` which is a type mismatch.
After this WASM_MAGIC array compile error is gone in ebm2cpp. Other
Limits::max compile errors remain (declaration/definition signature
mismatch + std::variant<T> with T not in the variant) — those are
caused by ebm2cpp emitting the inner anon STRUCT_DECL definition in
two scopes (top-level AND nested inside Limits), which makes type
name resolution ambiguous; tracked separately.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
field_decl_visitor was calling struct_union_members() which visit_Statement
each variant-member STRUCT_DECL and inlines their full `struct X { ... };`
bodies in front of the parent field declaration. Combined with the
top-level sorted_struct emission this duplicated each anon inner struct
into two scopes (top-level and parent-nested), and unqualified type
lookup inside out-of-class method bodies (e.g.
`std::holds_alternative<tmp667>(...)`) picked the wrong copy, breaking
the variant index static_assert.
Drop the inline nested emission. Top-level sorted_struct already emits
each anon inner STRUCT_DECL once at file scope, where unqualified names
resolve uniquely.
Remaining ebm2cpp wasm_src2json compile errors are inner-accessor
relocation issues (merged getter declaration vs inner-prefixed
definition signature mismatch; `tmp458.max()` chain hitting std::variant
without a max method) — same family as the ebm2rust receiver fix,
tracked separately.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Match the ebm2rust receiver-consolidation pattern for inner-anon property accessors that share an outer parent_format: - function_decl_custom + function_definition_start_wrapper: prepend `<inner_struct_identifier>_` to the method name when the property's parent_struct differs from parent_format, so the per-branch accessors don't collide with the merged one as overloads on Limits. - Expression_MEMBER_ACCESS_before_class: for property resolution where parent_struct != parent_format, emit `*(*this).<prefix>_<member>(args)` directly instead of routing main_logic through `(*this).tmp458.<member>(...)` which would call a method on std::variant that doesn't exist. Remaining ebm2cpp wasm_src2json error: declaration of the prefixed accessor lands inside the anon inner struct's class block (because ebm2cpp's Statement_STRUCT_DECL visit ties property emission to the property's nearest struct, not to parent_format), while the out-of-class definition uses `Limits::<prefix>_<member>`. Fixing that requires relocating inner property accessors into the parent_format class declaration — the same change ebm2rust needed in Statement_STRUCT_DECL_class. Tracked separately. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…NT cast + setter assignment - Move collect_anon_inner_descendants from ebm2rust into stub/util.hpp so both ebm2rust and ebm2cpp share the same variant-containment-chain walker, and update ebm2rust's caller to use the qualified name. - ebm2cpp struct_decl_custom: extend to handle DeclarationOnly + FunctionBodyOnly directly so anon inner structs skip emitting their own property accessors and outer (parent_format) structs aggregate the descendants' property accessors into their own class body / out-of-class definitions. Mirrors ebm2rust's pattern. - ebm2cpp type_cast_custom: when casting from a pure VARIANT (common_type=nil, std::variant<monostate, T...>) to a member type, emit std::get<T>(variant) instead of `T(variant)` which is invalid. (Member -> variant works via std::variant's converting constructor.) - ebm2cpp assignment_custom: when the assignment target is a MEMBER_ACCESS to a PROPERTY_DECL, rewrite `*getter() = value` (which fails because the getter returns `const T*`) into an overloaded setter call `(*this).<prefix>_<name>(value);` (or the unprefixed setter when parent_struct == parent_format). After this all testable backends pass wasm_src2json: ebm2c / ebm2ruby / ebm2rmw / ebm2python / ebm2go / ebm2rust / ebm2cpp Only ebm2zig remains failing — compile is clean, runtime `error.EndOfStream` is a `[..]Type` array-decode lowering issue tracked separately. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Five ADRs capturing the cross-cutting design choices that emerged
while bringing wasm_src2json to a passing state on python / go / rust
/ cpp (plus partial zig progress):
- 0027: inner-anon struct property accessors hoist to parent_format
scope (shared between ebm2rust and ebm2cpp via collect_anon_inner_
descendants helper in stub/util.hpp).
- 0028: derive_property emits per-level INIT_CHECK chain so backends
with interface-mapped variants (Go) and tagged-union variants
(Rust/Zig/C++) all get the narrowing information they need, mirroring
the encode/decode IndentBlock recursion pattern.
- 0029: emit_struct_methods split into emit_struct_properties /
emit_struct_codec / emit_struct_user_methods so backends can interleave
/ filter parts (the prerequisite for 0027's hoisting).
- 0030: hook naming convention `*_visitor` (MAYBE-receiving, no pass)
is being phased out in favor of `*_custom` (CALL_OR_PASS, pass falls
through to default). composite_field_decl_visitor was renamed as part
of this session.
- 0031: pure VARIANT (common_type=nil) type declaration is the
backend's responsibility; ebmgen only emits the `Variant{id}` name.
Documents how ebm2go / ebm2rust / ebm2zig / ebm2cpp each chose a
different representation (interface / enum / union(enum) / std::variant).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…d-agnostic CI's unictest log for every ebm2zig case fails with: mise ERROR No version is set for shim: zig Set a global default version with one of the following: mise use -g zig@0.15.2 `mise install` alone configures the tool version only via the local mise.toml (this directory). unictest.py then invokes `zig` from the generated `zig_proj` directory which doesn't have a mise.toml visible, and the shim can't resolve the version. ebm2go / ebm2ruby / ebm2rust don't trip this because they use a single default version per tool — but ebm2zig pins zig@0.15.2 explicitly, and the shim's lookup behaviour differs. Read the [tools] table from mise.toml and replay each pin as `mise use -g <tool>@<version>` so the shim resolves regardless of cwd. Version stays in sync with mise.toml automatically. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
on-keyday
marked this pull request as ready for review
May 25, 2026 04:16
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
WIP