Skip to content

docs(architecture): decide pure Go helpers from the bounded client (ADR 0016, #519)#616

Merged
cssbruno merged 3 commits into
mainfrom
design/go-helpers-from-client-519
Jun 22, 2026
Merged

docs(architecture): decide pure Go helpers from the bounded client (ADR 0016, #519)#616
cssbruno merged 3 commits into
mainfrom
design/go-helpers-from-client-519

Conversation

@cssbruno

Copy link
Copy Markdown
Owner

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:

client {
  computed Visible []Row { return ui.FilterRows(Rows, Query) }   // ui.FilterRows is pure Go
}
  1. Execution model — compile the helper to WASM from its Go source, invoked through the existing WASM island ABI (ADR 0004). The helper is never transpiled or re-expressed in JavaScript. This is the only model that satisfies [Client] Client expression DSL has two independent, diverging implementations (Go vs hand-written JS) #384 (single-source client semantics) by construction: the helper has one implementation (the Go source); WASM is just its compiled form. A Go→JS transpiler would be a second implementation of Go semantics — exactly the divergence [Client] Client expression DSL has two independent, diverging implementations (Go vs hand-written JS) #384 forbids — and is rejected.
  2. Purity is enforced statically and mandatorily: a call-graph analysis rejects I/O, package-level mutable state, concurrency, and non-determinism (time.Now, math/rand, unsafe, …) with a diagnostic naming the offending call site. Purity is transitive, with a curated pure-stdlib allowlist.
  3. Type bridging reuses the bounded-client JSON value model already used for 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 declared wasm; 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.

)

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).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +97 to +99
- 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +46 to +48
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +157 to +158
- **Phase 1:** allow pure Go helper calls only inside components already declared
`wasm` (ADR 0004), with purity validation, type bridging, and generated glue.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +90 to +93
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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

cssbruno added 2 commits June 21, 2026 23:34
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.
@cssbruno cssbruno merged commit 1567577 into main Jun 22, 2026
16 checks passed
@cssbruno cssbruno deleted the design/go-helpers-from-client-519 branch June 22, 2026 02:55
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.

[Architecture] Callable pure Go helpers from the bounded client (WASM-cliff middle rung)

1 participant