docs(architecture): decide pure Go helpers from the bounded client (ADR 0016, #519)#616
Conversation
) Record ADR 0016, which decides the three open design questions from #519: - Execution model: pure Go helpers are compiled to WASM from their Go source and invoked through the existing WASM island ABI (ADR 0004). They are never transpiled/re-expressed in JS, so there is one source of truth per #384. - Purity enforcement: a mandatory static call-graph analysis rejects I/O, global mutable state, concurrency, and non-determinism, with diagnostics. - Type bridging: helper parameters/results reuse the bounded-client JSON value model (scalars, structs, slices/maps of bridgeable values). This turns the bounded->WASM cliff into a ladder. The ADR is the decision (the foundational #519 checklist item); implementation is phased and depends on #384 landing first. Also fills in the stale decisions index (0014-0016).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f1f4342412
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| Allowed: parameters, locals, pure arithmetic/string/slice/struct/map value | ||
| construction, and calls to other functions proven pure (including a curated pure | ||
| standard-library allowlist — e.g. `strings`, `sort`, `math`, `strconv`). Purity |
There was a problem hiding this comment.
Require ownership before allowlisting mutators
When implementing the purity checker from this ADR, treating sort as a pure stdlib allowlist is enough to approve helpers that mutate bridgeable slice/map inputs, such as sorting the Rows parameter inside a computed helper. That violates the pure/computed contract because evaluating a derived value can change its inputs or depend on aliasing; remove mutating packages from the pure allowlist or state that they are only allowed on compiler-proven local copies.
Useful? React with 👍 / 👎.
| ```gwdk | ||
| client { | ||
| // ui.FilterRows is an ordinary, pure Go function in a sibling/imported package. | ||
| computed Visible []Row { return ui.FilterRows(Rows, Query) } |
There was a problem hiding this comment.
Specify a parseable imported-helper call form
The decided syntax uses a package-qualified call, but the bounded client expression grammar currently only permits calls when the callee is a bare identifier (internal/clientlang/expr_parser.go rejects member calls with only helper names can be called). If this ADR is the implementation contract, imported helpers have no parseable/resolvable form, so the documented example would be rejected unless an undocumented syntax change lands; define the qualified-call syntax and resolution here.
Useful? React with 👍 / 👎.
| - generates the marshal/unmarshal glue at the ABI boundary from the resolved Go | ||
| types (reusing the typed-result/struct-field machinery already used for SSR | ||
| load results and contracts); |
There was a problem hiding this comment.
Preserve result field metadata for computed arrays
The bridge contract only calls out type validation and marshal/unmarshal glue, but the motivating []Row computed result also needs entries such as Visible[].Name in the client symbol table. Existing validation gives a computed only its normalized top-level type, so a filtered/sorted row helper could validate as an array while every row field used in g:for, g:key, or interpolations remains unknown; require resolved result-field metadata to be exposed with the computed value.
Useful? React with 👍 / 👎.
| The pure Go helper is **compiled to WebAssembly from the same Go source the | ||
| server uses** and invoked through the existing component WASM island ABI (ADR | ||
| 0004). It is never transpiled or re-expressed in JavaScript. |
There was a problem hiding this comment.
Define how helpers avoid unrelated server files
Because Go compiles all non-tag-excluded files in a package for GOOS=js, compiling a helper from the same package the server uses fails whenever that package also has unrelated server-only files or imports, even if the helper call graph itself is pure. That makes the advertised ordinary sibling/imported-package path unusable for common app packages; require helper packages to be browser-buildable or specify a generated shim/extraction step that only includes the helper dependency set.
Useful? React with 👍 / 👎.
| - **Phase 1:** allow pure Go helper calls only inside components already declared | ||
| `wasm` (ADR 0004), with purity validation, type bridging, and generated glue. |
There was a problem hiding this comment.
Separate generated helpers from hand-authored wasm
Phase 1 says to use components already declared wasm, but the current component-level wasm contract points at a hand-authored browser Go package with required GOWDKMount/Handle/Destroy exports, while this ADR says the compiler owns the reactive shell and generated helper glue. A helper-only module in that lane would either be rejected by existing ABI export validation or create two lifecycle owners; define a distinct generated-helper WASM path or how the current export contract is bypassed.
Useful? React with 👍 / 👎.
| Helper parameters and results must be **bridgeable types**: the same JSON-encoded | ||
| value model the bounded client already uses for `state`, `props`, and the island | ||
| ABI — scalars (`string`, `bool`, integer/float numerics), structs of bridgeable | ||
| fields, and slices/maps of bridgeable values. The compiler: |
There was a problem hiding this comment.
Constrain wide integers across the JSON bridge
Allowing all integer numerics through the JSON value model loses exactness for int64/uint64 values above JavaScript's safe-integer range, so a pure Go/WASM helper can return a correct ID or counter and the bounded client can still observe a rounded value. To preserve the single-source semantics promised here, constrain bridgeable integer ranges or encode wide integers explicitly with diagnostics.
Useful? React with 👍 / 👎.
Fold in Codex review points so the decision is a complete implementation contract: - Specify the qualified-call grammar extension (alias.Name) needed to parse imported helper calls, with import-alias resolution. - Require helper modules to build from an extracted pure-dependency set (or a browser-buildable package), since GOOS=js compiles all package files. - Forbid mutation of bridged inputs; allow stdlib packages only for their non-mutating surface (sort.Slice on a parameter is rejected). - Require resolved result-field metadata to be published with computed results so g:for/g:key/interpolations see row fields. - Constrain bridgeable integers to the safe-integer range (or string-encode) to preserve exactness across the JSON bridge. - Clarify Phase 1 uses a distinct generated-helper WASM path with one lifecycle owner, not the hand-authored island export contract.
Closes #519.
#519 is an
[Architecture]/ design-direction issue whose first and gating checklist item is "Decide the helper execution model, consistent with #384." This PR delivers that decision as ADR 0016, resolving the three open questions in the issue.The decision
Add a
computed-level call to pure, imported Go functions from the bounded client:time.Now,math/rand,unsafe, …) with a diagnostic naming the offending call site. Purity is transitive, with a curated pure-stdlib allowlist.state/props/the island ABI — scalars, structs, slices/maps of bridgeable values — with generated marshal glue from resolved Go types. Unbridgeable signatures are rejected.This turns the bounded→WASM cliff into a ladder:
bounded → bounded + rich expressions → bounded + pure Go helpers → full WASM island.Why a doc and not an implementation
The issue is labelled
architecture("Foundational architecture / design-direction work") and depends on #384 landing first (so the bounded shell has one IR/evaluator before helpers cross the boundary). The full implementation — purity analyzer over the Go call graph, WASM glue, type-bridge codegen — is multi-phase and gated on that dependency. ADR 0016 makes the binding architectural decision now and lays out a phased plan (Phase 1: helpers inside components already declaredwasm; Phase 2: auto-promote; Phase 3: optional JS-shell + WASM-helper sidecar), each shipping diagnostics, an example, and generated-output tests.Checklist (from #519)
Files
docs/engineering/decisions/0016-pure-go-helpers-from-bounded-client.md— the ADR.docs/engineering/decisions/README.md— index updated (also backfills the stale 0014/0015 entries).CHANGELOG.md— note.