Skip to content

feat(vm,checker,backends): application primitives and codegen fixes from the HowlBoard dogfood pass - #37

Merged
howlcipher merged 4 commits into
mainfrom
feat/app-primitives-from-howlboard
Sep 10, 2026
Merged

howlcipher merged 4 commits into
mainfrom
feat/app-primitives-from-howlboard

Conversation

@howlcipher

Copy link
Copy Markdown
Owner

Six changes to HowlFrame, every one forced by building HowlBoard — the mission-control reference application — rather than by a fixture. Each is independently tested.

Added

store_keys / STORE_KEYS returns a store's record keys as a sorted list. The keys already existed in bcMemoryStore.records and were simply unreachable from .howl, so kv_cli, todo_cli, task_api and HowlBoard each maintained a parallel index that could silently diverge from the records it indexed. Sorting is deliberate: Go randomizes map iteration, and a platform selling determinism should not hand back listings in arbitrary order.

time_now in the JavaScript backend. Supported by the bytecode VM and the Go backend, rejected as an unknown statement for web_app, so a browser interface had no way to read the clock.

Changed

Dict values may mix types. The analyzer modelled a dict as a homogeneous map[string]T and rejected (dict ("id" "x") ("count" 7) ("evidence" (list ...))). The runtime never had this restriction — the VM and native store both carry map[string]any, and such a record round-trips through store_put/store_get and res_json correctly, verified before changing anything. Only the checker refused to let you write one down. Heterogeneous dict literals and map_set writes now widen the element type to any through the existing join helper; key checks, target-kind checks and list element homogeneity are unchanged.

This was a hard blocker. Dicts are the language's record literal, and the alternative was building JSON by string concatenation and parsing it back.

Fixed

Route handlers fail closed. A panic inside a handler wrote nothing to the ResponseWriter, so Go emitted 200 with an empty body — making CAPABILITY_DENIED, the platform's core safety mechanism, indistinguishable from a completed request. Handlers that fail before responding now return 500 carrying the structured VMError JSON with its code preserved, logged to the VM error stream rather than process stdout.

for over an expression silently miscompiled in both the JavaScript and Go backends. Each read the iterable's raw node value, empty for anything but a bound symbol, emitting for (let m of ) and for _, m := range {. Invalid output produced with no diagnostic, in a toolchain whose contract is to fail closed. The bytecode target was always correct, which is why it survived: the backends disagreed and nothing compared them.

web_app output could not run in a browser. on_event emitted no trailing semicolon, and ASI does not apply before (, so the next statement was parsed as a call of the addEventListener result. Separately, top-level statements routinely contain awaited calls and a classic <script> has no top-level await. They are now wrapped in an async IIFE, with function declarations left at top level so inline handlers can still reach them as globals.

These two together mean no web_app had been executed end to end before now. Successful compilation was being treated as success.

Verification

gofmt clean, go mod tidy, go build ./..., go vet ./..., go test ./... — 32/32 packages. Plus -validate, -run, -compile-bc, -run-bc, -compile-wasm and TestRepoAnalystStandaloneBytecode. The generated frontend/app.js now parses and executes as a classic script.

Not attempted

Module support in the bytecode target. It is the largest gap — backend/server.howl is one 665-line file because module/use/import/export are all unsupported there — but it is a substantial compiler change with real regression risk, and the right response to finding it in a dogfood pass is to report it with evidence rather than attempt it opportunistically.

Full findings, including fourteen gaps left open and three corrections to earlier claims in this repository's own notes: HowlBoard's dogfooding journal.

Ordering

This should merge before HowlBoard's companion PR, which builds its compiler from this repository's main.

🤖 Generated with Claude Code

https://claude.ai/code/session_01X427iD1w9dAGLML8wKn9aJ

howlcipher and others added 3 commits September 10, 2026 09:01
…handler panic

Three changes forced by building HowlBoard, the reference application, on
standalone bytecode. Each is independently testable and none is specific to
that application.

store_keys / STORE_KEYS enumerates a native store's record keys as a sorted
list. The keys already existed in bcMemoryStore.records but were unreachable
from .howl, so kv_cli, todo_cli, task_api, and HowlBoard each maintained a
parallel index record that could silently diverge from the records it indexed.
Enumeration is sorted because Go randomizes map iteration and callers list
records for display.

Dict values may now mix types. Dicts are the language's record literal, and
both the VM and the native store carry map[string]any, so a record combining
strings, ints, lists, and nested dicts already round-tripped correctly through
store_put/store_get; only the analyzer rejected constructing one. Heterogeneous
dict literals and map_set writes now widen the element type to any via the
existing join helper. Key checks, target-kind checks, and list element
homogeneity are unchanged. Two test assertions that encoded the old
homogeneity rule are removed, with positive coverage added in their place.

Route handlers now fail closed. A panic inside a handler wrote nothing to the
ResponseWriter, so Go emitted 200 with an empty body, making a denied
capability indistinguishable from a completed request. Handlers that fail
before responding now return 500 carrying the structured VMError JSON, and the
failure is logged to the VM error stream rather than process stdout. Handlers
that already committed a response are left untouched.

Verified: go build ./..., go vet ./..., go test ./... (32/32 packages),
gofmt clean, plus -validate, -run, -compile-bc, -run-bc, -compile-wasm and
TestRepoAnalystStandaloneBytecode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X427iD1w9dAGLML8wKn9aJ
…nterface

Three defects and one gap, all surfaced by compiling a real web_app rather
than a fixture.

for over an expression silently miscompiled in both the JavaScript and Go
backends. Each read ir.Kids[1].Value, which is empty for anything but a bound
symbol, so (for m (map_get d "missions") ...) emitted "for (let m of )" and
"for _, m := range {". Invalid output was produced with no diagnostic at all,
which is the opposite of the fail-closed target contract.

on_event emitted no trailing semicolon. Automatic semicolon insertion does not
apply before "(", so a following top-level statement was parsed as a call of
the addEventListener result, taking down the whole script.

A web_app's top-level statements are now wrapped in an async IIFE. They
routinely contain awaited calls and a classic <script> has no top-level await,
so generated interfaces failed to parse in the browser. Function declarations
stay at top level, keeping them reachable as globals for inline handlers.

time_now is now lowered by the JavaScript backend. The bytecode VM and Go
backend both support it; web_app programs were rejecting it as an unknown
statement, leaving a browser interface unable to render relative times.

Verified: go build ./..., go vet ./..., go test ./... (32/32 packages),
gofmt clean, and the generated frontend/app.js now parses as a classic script.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X427iD1w9dAGLML8wKn9aJ
HowlBoard is now the largest program built on this toolchain and the one that
most exercises it. The README and the Pages ecosystem drawer name it as the
canonical reference application and human-facing operations interface, and
point at its dogfooding journal as the most detailed record of where this
language helps and where it gets in the way. Repo Analyst remains the reference
for the in-repository bytecode path.

Also corrects three stale claims in apps/task_api/DEVELOPMENT_NOTES.md, stated
as corrections rather than silently edited into the original text: req_method
does exist, a defun can return a dict through the type_hint annotation rather
than a positional type symbol, and route handler panics no longer reach the
client as an empty 200. The note that store key enumeration is adequate
friction no longer applies now that store_keys exists.

Verified: go build ./..., go vet ./..., go test ./... (32/32 packages).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X427iD1w9dAGLML8wKn9aJ
TestTaskAPICapabilities/network_only_starts_but_store_access_is_denied
asserted the old fail-open behaviour as "real, current behavior": a
capability-denied panic inside a route handler reaching the client as a 200
with an empty body. That is the defect this branch fixes, so the assertion is
inverted to require a 500 carrying CAPABILITY_DENIED.

This strengthens the test rather than relaxing it. It previously proved the
denial was invisible; it now proves the denial is reported with its code
intact. The unit-level regression test is TestHTTPHandlerFailuresFailClosed in
internal/vm.

Caught by CI, not locally. `go test ./...` reported this package as cached and
passing because its tests drive a compiled bytecode binary through a
subprocess rather than importing the packages that changed, so Go's test cache
did not invalidate. Verification for this branch now uses -count=1.

Verified: gofmt clean, go vet ./..., go build ./..., go test -count=1 ./... —
32/32 packages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X427iD1w9dAGLML8wKn9aJ
@howlcipher
howlcipher merged commit 97b8830 into main Sep 10, 2026
1 check passed
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.

1 participant