Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ go run howlframe.go -run-bc -allow-caps network,filesystem examples/cli_hello.ho

An unrecognized capability name in `-allow-caps` is rejected outright rather than silently granting nothing. See `docs/reference/bytecode_reference.md` for the full opcode-to-capability mapping.

Generated Go and JavaScript mediate `(env "KEY")` and `(exec cmd args...)` the same way. The runner grant is the `HOWLFRAME_ALLOW_CAPS` environment variable, a comma-separated list of the same names. An empty or unset value denies the effect with `CAPABILITY_DENIED` before the variable is fetched or a process is spawned. `environment` returns the value. `process` runs the command. Other generated host effects are not on this gate yet.
Generated Go and JavaScript mediate `(env "KEY")`, `(exec cmd args...)`, and `(read_file path)` the same way. The runner grant is the `HOWLFRAME_ALLOW_CAPS` environment variable, a comma-separated list of the same names. An empty or unset value denies the effect with `CAPABILITY_DENIED` before the variable is fetched, a process is spawned, or a file is read. `environment` returns the value. `process` runs the command. `filesystem` reads the file. Other generated host effects are not on this gate yet.

Dictionary operations such as `map_get` and `map_keys` grant nothing. `map_keys` is pure: `MAP_KEYS` has an empty capability field and runs under an empty grant. `store_keys` stays `database`. A `memory://` store needs that grant alone. A `file://` store additionally requires `filesystem`; `database` alone denies `file://` `store_keys` with `CAPABILITY_DENIED`. The generated opcode table records the opcode field (empty for `MAP_KEYS`, `database` for `STORE_KEYS`). The URI-dependent grant is written in [bytecode capability notes](docs/reference/bytecode_capability_notes.md).

Expand Down
1 change: 1 addition & 0 deletions change_log.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

### Changed
* Generated Go and JavaScript mediate `(read_file)`. An empty, missing, or non-`filesystem` `HOWLFRAME_ALLOW_CAPS` grant fails with `CAPABILITY_DENIED` before any filesystem read, and the denial does not include the path. The `filesystem` grant reads the file. The production `-compile-bc` path is unchanged. Journal: `docs/journals/2026-09-30_lowered_hfir_abi_phase2c_read_file.md`.
* Generated Go and JavaScript mediate `(exec)`. An empty, missing, or non-`process` `HOWLFRAME_ALLOW_CAPS` grant fails with `CAPABILITY_DENIED` before a subprocess starts, and the denial does not include the command. The `process` grant runs it. The production `-compile-bc` path is unchanged. Journal: `docs/journals/2026-09-30_lowered_hfir_abi_phase2b_exec.md`.
* Generated Go and JavaScript mediate `(env)`. An empty or non-`environment` `HOWLFRAME_ALLOW_CAPS` grant fails with `CAPABILITY_DENIED` before the variable is read. The `environment` grant returns the value. The production `-compile-bc` path is unchanged. Journal: `docs/journals/2026-09-30_lowered_hfir_abi_phase2a_env.md`.
* `map_keys` order is UTF-8 byte order on the interpreter, the bytecode VM, the Go backend, and JavaScript. JavaScript no longer uses UTF-16 code-unit sort, so a supplementary-plane key sorts with the same list as Go `sort.Strings`. No new opcode and no capability change. Journal: `docs/journals/2026-09-30_dict_key_sort.md`.
Expand Down
4 changes: 2 additions & 2 deletions docs/hfir_execution_status.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ Improvement #88 has a real but deliberately bounded execution destination: model

`docs/reference/lowered_hfir_abi_v1.md` versions the contract the hosts must share (`lowered-hfir-abi/v1`). The conformance suite compares the interpreter, the bytecode VM, Go, and JavaScript on a pure core and on `env` denial. That comparison runs the production AST paths through `tools/difftest`. It does not switch `-compile-bc` over to `LowerToBytecode`.

Phase 2a mediates `env` in generated Go and JavaScript. An empty `HOWLFRAME_ALLOW_CAPS` grant is `CAPABILITY_DENIED` and does not read the variable. Phase 2b mediates `exec` the same way: an empty grant, or a grant that omits `process`, is `CAPABILITY_DENIED` before any subprocess starts. The production compiler is unchanged.
Phase 2a mediates `env` in generated Go and JavaScript. An empty `HOWLFRAME_ALLOW_CAPS` grant is `CAPABILITY_DENIED` and does not read the variable. Phase 2b mediates `exec` the same way: an empty grant, or a grant that omits `process`, is `CAPABILITY_DENIED` before any subprocess starts. Phase 2c mediates `read_file` the same way: an empty grant, or a grant that omits `filesystem`, is `CAPABILITY_DENIED` before any filesystem read. The production compiler is unchanged.

Wasm feasibility in this revision is the closed set `exec`, `spawn_agent`, and `http_server_start` (`HFIR_TARGET_INFEASIBLE`). Control edges are still unpopulated. Calls are still not executable HFIR. The rest of Phase 2 is the execution-path move. Journals: `docs/journals/2026-09-30_lowered_hfir_abi_phase1.md`, `docs/journals/2026-09-30_lowered_hfir_abi_phase2a_env.md`, `docs/journals/2026-09-30_lowered_hfir_abi_phase2b_exec.md`.
Wasm feasibility in this revision is the closed set `exec`, `spawn_agent`, and `http_server_start` (`HFIR_TARGET_INFEASIBLE`). Control edges are still unpopulated. Calls are still not executable HFIR. The rest of Phase 2 is the execution-path move. Journals: `docs/journals/2026-09-30_lowered_hfir_abi_phase1.md`, `docs/journals/2026-09-30_lowered_hfir_abi_phase2a_env.md`, `docs/journals/2026-09-30_lowered_hfir_abi_phase2b_exec.md`, `docs/journals/2026-09-30_lowered_hfir_abi_phase2c_read_file.md`.

## Provenance limitation

Expand Down
30 changes: 30 additions & 0 deletions docs/journals/2026-09-30_lowered_hfir_abi_phase2c_read_file.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Lowered HFIR ABI, phase 2c: read_file grants on Go and JavaScript

## Why this slice

Phase 2a put `(env)` behind `HOWLFRAME_ALLOW_CAPS`. Phase 2b did the same for `(exec)`. `(read_file path)` on generated Go was still a direct `os.ReadFile`, and JavaScript had no `read_file` form. The bytecode VM already denies `OpReadFile` before `os.ReadFile`. The interpreter already denied a missing `filesystem` grant, then rejected the form as unsupported, so a granted read there was not an honest file read. This slice makes the generated hosts check the grant, and makes the interpreter read only after that same check.

Phase 2 as a whole is still one lowered graph for every host. This change does not take that step. #90 stays Partial.

## What landed

`(read_file path)` in generated Go and JavaScript goes through `howlFrameReadFile`. The helper reads the runner grant `HOWLFRAME_ALLOW_CAPS` (comma-separated, the same names as `-allow-caps`). The grant name is `filesystem`, matching `capability.ForConstruct("read_file")` and `OpReadFile`. It panics or throws `CAPABILITY_DENIED: capability denied: filesystem` when `filesystem` is absent, and only then would it read. An empty grant, an unset grant, and a grant of some other capability all deny. The denial does not include the path, and the file is not opened.

A `filesystem` grant reads the file. Go's `try_let` still binds `(bytes, error)` from `howlFrameReadFile`, so a granted miss stays an IO error instead of a capability denial. `let` and `bytes_to_string` use `howlFrameReadFileBytes`, which returns one `[]byte` after that same check. JavaScript returns the UTF-8 text from `readFileSync` after the check. The interpreter now reads with `os.ReadFile` after its existing `filesystem` check and returns those bytes, so the conformance case can include it. The bytecode VM is unchanged: `OpReadFile` is already `filesystem`, and the gate runs before the read.

`tools/difftest` already passes `HOWLFRAME_ALLOW_CAPS` to generated Go and `node`. `tests/conformance/lowered_hfir_abi_v1.json` runs `read_file_denied` and `read_file_granted` on the interpreter, the bytecode VM, Go, and JavaScript. The fixture reads `/tmp/howlframe-abi-v1-phase2c.txt`. The conformance test writes `phase2c-read-marker` there first, because generated Go does not share the caller's working directory. The marker is absent on denial and printed when `filesystem` is granted.

The JavaScript checker accepts `read_file` so a `web_app` body can host the same fixture. Production `-compile-bc` is still `runHFIRGate` and then `bytecode.CompileToBytecode` on the AST.

## What is still Phase 2

* `-compile-bc` still compiles the AST. `-compile-hfir-bc` is still the experimental lowerer.
* `defun`, `call`, and `while` are still not executable HFIR.
* `ControlEdges` are still empty, so the graph is not SSA.
* Generated `fetch`, `write_file`, `mkdir`, and the other host effects still do not consult a grant. `env`, `exec`, and `read_file` do. `write_file` and `mkdir` stayed out: JavaScript and the interpreter do not implement them, and folding them into this helper would be a second host effect.
* Feasibility is still a Wasm-only set. This slice adds no Wasm opcode and does not execute Wasm.
* No module linker, no HFIR module graph, and no VM module opcode.

## What this does not do

No new capability kind. No new opcode. #102–#105 and #108 stay Done and are not reopened. #90 stays Partial.
8 changes: 5 additions & 3 deletions docs/reference/lowered_hfir_abi_v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ v1 defines no linear memory and no Wasm import table.

Observability imports `print`, `stderr`, and `exit` grant nothing.

Host effects are named by `capability.ForConstruct`. The v1 suite uses two of them. `(env "KEY")` requires `environment`. An empty grant denies it before the variable is read. The grant `environment` returns the value. `(exec cmd args...)` requires `process`, the same name as `OpExec`. An empty grant denies it before any subprocess starts. The grant `process` runs the command and returns its output. File, network, and database imports stay on that same table and are not given new opcodes here.
Host effects are named by `capability.ForConstruct`. The v1 suite uses three of them. `(env "KEY")` requires `environment`. An empty grant denies it before the variable is read. The grant `environment` returns the value. `(exec cmd args...)` requires `process`, the same name as `OpExec`. An empty grant denies it before any subprocess starts. The grant `process` runs the command and returns its output. `(read_file path)` requires `filesystem`, the same name as `OpReadFile`. An empty grant denies it before any filesystem read. The grant `filesystem` returns the file bytes. Network and database imports stay on that same table and are not given new opcodes here.

### Errors

Expand All @@ -93,7 +93,9 @@ Pure operations declare no capability. `map_keys` and `map_get` stay pure (#107)

`exec` is the process case. The suite binds `(exec "printf" "phase2b-exec-marker")` and prints that output as text. With no grant, the same four hosts reject with `CAPABILITY_DENIED`, exit nonzero, write no stdout, and do not include the marker. With the `process` grant, those hosts print the marker. The command is not a shell pipeline.

The interpreter and the bytecode VM consult `-allow-caps`. Generated Go and JavaScript do the same check in `howlFrameEnv` and `howlFrameExec`. Their runner grant is `HOWLFRAME_ALLOW_CAPS`, a comma-separated list of the same names as `-allow-caps`. An empty or unset value denies. A grant that omits the required name denies. `howlFrameEnv` may read that grant variable. It does not read the requested key until `environment` is present. `howlFrameExec` does not spawn until `process` is present, and the denial text does not contain the command. Other generated host effects, including `read_file` and `fetch`, are still not mediated.
`read_file` is the filesystem case. The suite binds `(read_file "/tmp/howlframe-abi-v1-phase2c.txt")` and prints those bytes as text. The conformance test writes `phase2c-read-marker` to that absolute path before the case, because generated Go runs in its own directory. With no grant, the same four hosts reject with `CAPABILITY_DENIED`, exit nonzero, write no stdout, and do not include the marker. With the `filesystem` grant, those hosts print the marker.

The interpreter and the bytecode VM consult `-allow-caps`. Generated Go and JavaScript do the same check in `howlFrameEnv`, `howlFrameExec`, and `howlFrameReadFile`. Their runner grant is `HOWLFRAME_ALLOW_CAPS`, a comma-separated list of the same names as `-allow-caps`. An empty or unset value denies. A grant that omits the required name denies. `howlFrameEnv` may read that grant variable. It does not read the requested key until `environment` is present. `howlFrameExec` does not spawn until `process` is present, and the denial text does not contain the command. `howlFrameReadFile` does not call `os.ReadFile` or `readFileSync` until `filesystem` is present, and the denial text does not contain the path. Other generated host effects, including `fetch`, `write_file`, and `mkdir`, are still not mediated.

### Feasibility

Expand Down Expand Up @@ -128,7 +130,7 @@ Phase 2 is one lowered graph consumed by every host, with identical outcomes or
* Production `-compile-bc` still compiles the AST. Flipping that path is Phase 2.
* `defun`, `call`, and `while` become executable HFIR, or every host rejects them with one code. Today the hosts run them and the experimental lowerer rejects them.
* `ControlEdges` are populated and the graph is SSA.
* Go and JavaScript mediate `env` (Phase 2a) and `exec` (Phase 2b). Other generated host effects, including `read_file` and `fetch`, still do not. One lowered graph for every host is still the rest of Phase 2.
* Go and JavaScript mediate `env` (Phase 2a), `exec` (Phase 2b), and `read_file` (Phase 2c). Other generated host effects, including `fetch`, `write_file`, and `mkdir`, still do not. One lowered graph for every host is still the rest of Phase 2.
* One feasibility table covers every target, not only the three Wasm host effects.
* Non-exact integer division picks one rule.
* Wasm collections (#73) and `for` / `match` / `try_let` / `spawn` SSA lowering (#84) target this ABI. They are not part of v1, and this revision does not grow them.
Expand Down
Loading
Loading