Skip to content

Gen/fix wasm src2json - #368

Merged
on-keyday merged 52 commits into
mainfrom
gen/fix-wasm-src2json
May 25, 2026
Merged

Gen/fix wasm src2json#368
on-keyday merged 52 commits into
mainfrom
gen/fix-wasm-src2json

Conversation

@on-keyday

Copy link
Copy Markdown
Owner

WIP

on-keyday and others added 12 commits May 1, 2026 22:34
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>
@devloai

devloai Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor

Unable to trigger custom agent "Code Reviewer". You have run out of credits 😔
Please upgrade your plan or buy additional credits from the subscription page.

@github-advanced-security

Copy link
Copy Markdown
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:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

Comment thread .github/workflows/sisakulint.yaml Fixed
@sisakulint

sisakulint Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor

Workflow Scan

🟡 1 Medium

🟡 Medium - 1 finding

artipacked - Persisted credentials may leak via artifacts.

  47 |       - name: Checkout repository

📍 .github/workflows/codeql.yml:47


Comment /autofix below to apply the suggested fixes automatically.

@on-keyday

Copy link
Copy Markdown
Owner Author

/autofix

@on-keyday

Copy link
Copy Markdown
Owner Author

/autofix

on-keyday and others added 26 commits May 19, 2026 14:47
- 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
on-keyday marked this pull request as ready for review May 25, 2026 04:16
@on-keyday
on-keyday merged commit 75fa9dc into main May 25, 2026
74 of 79 checks passed
@on-keyday
on-keyday deleted the gen/fix-wasm-src2json branch May 25, 2026 15:51
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