Files
rose-ash/plans/kernel-on-sx.md
giles 0da39de68a
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 27s
kernel: Phase 3 $vau/$lambda/wrap/unwrap + 34 tests [shapes-reflective]
User-defined operatives via $vau; applicatives via $lambda (wrap ∘ $vau).
wrap/unwrap as Kernel-level applicatives. kernel-call-operative forks
on :impl (primitive) vs :body (user) tag. kernel-base-env wires the
four combiners + operative?/applicative? predicates. Env-param sentinel
`_` / `#ignore` → :knl-ignore (skip dyn-env bind). Flat parameter list
only; destructuring later. Headline test: custom applicative + custom
operative composed from user code.
2026-05-11 07:43:45 +00:00

137 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Kernel-on-SX: first-class everything
The natural successor to SX's recently-completed env-as-value work (sx-improvements Phase 4). Kernel — John Shutt's reformulation of Lisp from his 2010 PhD — pushes *first-class* all the way: environments, evaluators, special forms (operatives), lambda variants are all runtime values, manipulable by programs. SX already has env-as-value; Kernel is what env-as-value looks like *all the way*.
**The chisel:** *reflection*. Every language in the current set treats some part of itself as fixed and ineffable — Common Lisp's special forms, Erlang's process model, OCaml's modules. Kernel reifies more of itself than any other language does. Implementing it stresses the substrate's *self-knowledge*: which parts of evaluation does SX expose to user programs, and which stay opaque?
**What this exposes about the substrate:**
- Whether `eval-expr` can be called as a primitive on user-supplied environments without breaking invariants.
- Whether CEK frames can be reified as values (they currently aren't).
- Whether special-form dispatch can be table-driven and user-extensible at runtime.
- Whether the macro hygiene story extends to Shutt's "hygienic operatives" (operatives that don't capture).
**End-state goal:** Kernel's R-1RK core — `$vau`/`$lambda`/`wrap`/`unwrap`, first-class environments, the applicativeoperative distinction, the standard environment, encapsulations.
## Ground rules
- Scope: `lib/kernel/**` and `plans/kernel-on-sx.md` only. Substrate work belongs to `sx-improvements.md` — if a feature is missing, file it there, don't fix from this plan.
- Consumes from `lib/guest/`: `core/lex.sx`, `core/pratt.sx` (s-expression-shaped, minimal demand), `core/ast.sx`, `core/match.sx`.
- **May propose** a new sub-layer `lib/guest/reflective/` — environment reification helpers, applicative-vs-operative dispatch, evaluator continuation protocols. A second consumer would be needed; candidates are a hypothetical "MetaScheme" or a Common-Lisp port that exposes its evaluator.
- Branch: `loops/kernel`. Standard worktree pattern.
## Architecture sketch
```
Kernel source text (S-expression syntax)
lib/kernel/parser.sx — bog-standard s-expr reader
lib/kernel/eval.sx — kernel-eval: walks the AST, threads first-class env
│ dispatches to operatives via env-bound bindings, not
│ a hardcoded switch
lib/kernel/runtime.sx — applicative/operative tagged values, wrap/unwrap,
│ standard environment construction, encapsulations
SX CEK evaluator
```
## Semantic mappings
| Kernel construct | SX mapping |
|------------------|-----------|
| `($lambda (x) body)` | applicative: `(make-applicative (fn (x) body))` — args evaluated |
| `($vau (x) e body)` | operative: `(make-operative (fn (x e) body))` — args UN-evaluated, dynamic env passed as `e` |
| `(wrap op)` | applicative wrapping an operative: evaluate args, then call op |
| `(unwrap app)` | get the underlying operative of an applicative |
| `($define! x v)` | operative: bind `x` to `v` in dynamic env |
| `(eval expr env)` | call `kernel-eval` on `expr` in `env` — first-class |
| `(make-environment)` | fresh empty env |
| `(get-current-environment)` | reify the calling env (via SX env-as-value) |
| `($if c t e)` | operative: evaluate `c`, then `t` or `e` in dynamic env |
The whole interesting thing: there are no special forms hardcoded in the evaluator. `$if`, `$define!`, `$lambda` are all *operatives* bound in the standard environment. User code can rebind them. The evaluator is just `lookup-and-call`.
## Roadmap
### Phase 1 — Parser
- [x] S-expression reader with the standard atoms (number, string, symbol, boolean, nil) and lists.
- [ ] Reader macros optional; defer to Phase 6.
- [x] Tests in `lib/kernel/tests/parse.sx`.
### Phase 2 — Core evaluator with first-class environments
- [x] `kernel-eval expr env` — primary entry, walks AST, threads env as a value.
- [x] Symbol lookup → environment value (using SX env-as-value primitives).
- [x] List → look up head, dispatch on tag (applicative vs operative).
- [x] No hardcoded special forms — even `if`/`define`/`lambda` are env-bound.
- [x] Tests in `lib/kernel/tests/eval.sx`.
### Phase 3 — `$vau` / `$lambda` / `wrap` / `unwrap`
- [x] Operative tagged value: `{:type :operative :params :env-param :body :static-env}`.
- [x] Applicative tagged value wraps an operative + the "evaluate args first" contract.
- [x] `$vau` builds operatives; `$lambda` is `wrap``$vau`.
- [x] `wrap` / `unwrap` round-trip cleanly.
- [x] Tests: define a custom operative, define a custom applicative on top of it.
### Phase 4 — Standard environment
- [ ] Standard env construction: bind `$if`, `$define!`, `$lambda`, `$vau`, `wrap`, `unwrap`, `eval`, `make-environment`, `get-current-environment`, plus arithmetic and list primitives.
- [ ] Tests: classic Kernel programs (factorial, list operations, environment manipulation).
### Phase 5 — Encapsulations
- [ ] `make-encapsulation-type` returns three operatives: encapsulator, predicate, decapsulator. Standard Kernel idiom for opaque types.
- [ ] Tests: implement promises, streams, or simple modules via encapsulations.
### Phase 6 — Hygienic operatives (Shutt's later work)
- [ ] Operatives that don't capture caller bindings — uses scope sets / frame stamps to track provenance.
- [ ] Bridge to SX's hygienic macro story; possibly extends `lib/guest/reflective/` with hygiene primitives.
- [ ] Tests: write an operative that introduces a binding and verify it doesn't shadow caller's same-named bindings.
### Phase 7 — Propose `lib/guest/reflective/`
- [ ] Once Phase 3 lands and stabilises, identify which env-reification + dispatch primitives are reusable. Candidate API: `make-operative`, `make-applicative`, `with-current-env`, `eval-in-env`.
- [ ] Find a second consumer (Common-Lisp's macro-expansion evaluator? a metacircular Scheme variant? a future plan).
- [ ] Only extract once two consumers exist (per stratification rule).
## lib/guest feedback loop
**Consumes:** `core/lex`, `core/pratt`, `core/ast`, `core/match`.
**Stresses substrate:** env-as-value (Phase 4 of sx-improvements) under heavy use; `eval` as a primitive on user environments; potentially CEK frame reification.
**May propose:** `lib/guest/reflective/` sub-layer — environment manipulation, evaluator-as-value, applicative/operative dispatch protocols.
**Proposed `lib/guest/reflective/combiner.sx` API** (from Phase 3 chiselling — pending second consumer):
- `(refl-make-primitive-operative IMPL)` — IMPL receives `(args dyn-env)`, args unevaluated.
- `(refl-make-user-operative PARAMS EPARAM BODY STATIC-ENV)` — for $vau-like constructors. The EPARAM sentinel for "ignore dyn-env" is a fixed keyword (`:refl-ignore` in the proposal).
- `(refl-wrap OP)` / `(refl-unwrap APP)` — round-trip pair.
- `(refl-operative? V)` / `(refl-applicative? V)` / `(refl-combiner? V)`.
- `(refl-call-combiner COMBINER ARGS DYN-ENV)` — the dispatch fork. Pairs with `refl-eval` from the evaluator kit.
- Representation: `{:refl-tag :operative :impl FN}` or `{:refl-tag :operative :params P :env-param EP :body B :static-env SE}`; applicatives are `{:refl-tag :applicative :underlying OP}`. The dispatch decision lives in one fork: presence of `:impl` is primitive, presence of `:body` is user-defined.
- Driving insight: every reflective Lisp must distinguish "eval my args first" from "hand me the syntax". The tag protocol is identical across Kernel, CL fexprs, vau-style Schemes, possibly Forth's IMMEDIATE words.
**Proposed `lib/guest/reflective/env.sx` API** (from Phase 2 chiselling — pending second consumer per the two-consumer rule):
- `(refl-make-env)` / `(refl-extend-env PARENT)` — fresh / chained envs, plain SX dicts so they're easy to introspect.
- `(refl-env? V)` — predicate.
- `(refl-env-bind! ENV NAME VAL)` — local bind; parent is untouched.
- `(refl-env-has? ENV NAME)` — recursive presence check.
- `(refl-env-lookup ENV NAME)` — recursive lookup, raises on miss.
- Representation: `{:refl-tag :env :bindings DICT :parent ENV-OR-NIL}`. Pure-SX dicts so any guest can serialize, diff, snapshot, or rewind environments without help from the host.
The motivation is that SX's host `make-env` family is registered only in HTTP/site-mode platform setup, so a guest that needs first-class envs in CLI / test contexts has to roll its own anyway. A shared kit means the next reflective consumer (CL macro evaluator? metacircular Scheme?) doesn't need to redo the work.
**What it teaches:** whether SX's recent env-as-value direction generalises to "evaluator-as-value." If Kernel implements cleanly in <2000 lines, env-as-value is real. If it requires substrate fixes at every turn, env-as-value was incomplete and the substrate is telling us what's missing.
## References
- Shutt, "Fexprs as the basis of Lisp function application" (PhD thesis, 2010).
- Kernel Report (R-1RK): https://web.cs.wpi.edu/~jshutt/kernel.html
- Klisp implementation (Andres Navarro) — pragmatic reference.
## Progress log
- 2026-05-11 — Phase 3 operatives landed. `lib/kernel/runtime.sx` adds `$vau` (primitive operative that returns a user operative), `$lambda` (sugar for `wrap ∘ $vau`), `wrap` and `unwrap` (Kernel-level applicatives), plus `operative?` and `applicative?` predicates. `kernel-base-env` wires them all into a fresh env. `kernel-eval.sx` now dispatches in `kernel-call-operative` between primitive ops (carry `:impl`) and user ops (carry `:params :env-param :body :static-env`). Parameter binding is a flat list — destructuring/`&rest` deferred. Env-param sentinel: spell `_` or `#ignore``:knl-ignore`, which skips the dyn-env bind. 34 tests in `tests/vau.sx`, including the headline custom-operative + custom-applicative composition. chisel: shapes-reflective. Two further reflective-API candidates surfaced: (a) the operative/applicative tag protocol — `make-primitive-operative`, `make-user-operative`, `wrap`, `unwrap` are general for any Lisp-of-fexprs; (b) the call-dispatch fork (primitive vs user) is a *single decision* that every reflective evaluator hits. Both shape go into the proposed `lib/guest/reflective/combiner.sx` candidate.
- 2026-05-10 — Phase 2 evaluator landed. `lib/kernel/eval.sx` is `lookup-and-combine`: zero hardcoded special forms. `kernel-eval EXPR ENV` dispatches on shape — literals self-evaluate, Kernel strings unwrap, symbols lookup, lists evaluate head and combine. `kernel-combine` distinguishes operatives (impl receives un-evaluated args + dynamic env) from applicatives (eval args, recurse into underlying op). `kernel-wrap`/`kernel-unwrap` round-trip cleanly. 36 tests verify literal evaluation, symbol lookup with parent-chain shadowing, tagged-value predicates, and the operative-vs-applicative contract (notably `$if` only evaluates the chosen branch, `$quote` returns its arg unevaluated). chisel: shapes-reflective. Substrate gap surfaced: SX's `make-env` / `env-bind!` family is only registered in HTTP/site mode (`http_setup_platform_constructors`), not in CLI epoch mode used for tests. So Kernel envs are modelled in pure SX as `{:knl-tag :env :bindings DICT :parent P}` — a binding-dict + parent-pointer + recursive lookup walk. This is exactly the `lib/guest/reflective/env.sx` candidate API: any reflective language needs first-class env values that can be extended, queried, and walked. Recording the shape (constructor, extend, bind!, has?, lookup) here for the eventual Phase 7 extraction.
- 2026-05-10 — Phase 1 parser landed. `lib/kernel/parser.sx` reads R-1RK lexical syntax: numbers (int/float/exp), strings (with escapes), symbols (permissive — anything non-delimiting), booleans `#t`/`#f`, the empty list `()`, nested lists, and `;` line comments. Reader macros (`'` `,` `,@`) deferred per plan. AST: numbers/booleans/lists pass through; strings are wrapped as `{:knl-string …}` to distinguish from symbols which are bare SX strings. 54 tests in `lib/kernel/tests/parse.sx` pass via `sx_server.exe` epoch protocol. chisel: consumes-lex (uses `lex-digit?` and `lex-whitespace?` from `lib/guest/lex.sx` — pratt deliberately not consumed because Kernel is plain s-expressions, no precedence climbing).
## Blockers
_(none yet — main risk is substrate gap discovery during Phase 2)_