Skip to content

Commit eb8aa4e

Browse files
feat(core): LambdaDelta L0 kernel — reader, value model, evaluator, budget (#35)
* feat(core): LambdaDelta L0 kernel — reader, value model, evaluator, budget Phase L0 of ADR-0003: the notebook-agnostic λδ language kernel, built to the confirmed spec v0.1. Headless and fully tested; no UI change. This is the piece everything else (formulas, agents-as-programs, REPL, plugins) will sit on. The kernel / host seam (spec §7), from day one: - `core/src/lambdadelta/` is a self-contained Clojure-flavoured Lisp that KNOWS NOTHING ABOUT NOTES. Reader · value model · evaluator · budget · pure builtins. - `Interp::register_builtin` is the seam: a host (the notebook — the first of possibly many) registers its own builtins as native functions. Nexia-List is simply the first host. This is what makes an SDK/embedding/plugin ecosystem (#33) cheap rather than a later rewrite. A doc-test exercises the seam. What's in: - Reader (reader.rs): total, panic-free. Literals, `()` `[]` `{}` `#{}`, `#uuid`/`#inst` tagged literals (generic `Value::Tagged` — kernel keeps the form, hosts assign meaning), reader sugar `' ` `~` `~@`, `#(… % …)`, `;` comments, commas-as-whitespace. Guards so `nan`/`inf`/`->` stay symbols. - Value model (value.rs): nil/bool/int/float/string/symbol/keyword/list/vector/ set/map/tagged/fn/builtin; truthiness (only nil/false falsy); a reader- compatible printer; a lexical `Scope` with parent chain for closures. - Evaluator (eval.rs): special forms `quote if do let fn def` + quasiquote/ unquote/unquote-splicing exactly per spec §3; closures with lexical capture and `& rest`; keyword-as-function (`(:k m)`) powering multimethod dispatch and `sort-by`; collection literals evaluate their elements. - Sandbox budget (spec §6): per-eval reduction-step ceiling and recursion-depth limit; exhaustion aborts cleanly with an error value — never hangs. - Errors as values (error.rs): unbound/not-callable/arity/type/syntax/ divide-by-zero/budget — structured, never panics. - Pure builtins (builtins.rs, spec §4): arithmetic, comparison, predicates, sequence ops incl. map/filter/reduce/sort/sort-by, sets, strings, maps, and a little reflection (`eval`/`read`/`type`). No notebook I/O — that's a host's job. - WASM: `lambdadeltaEval` browser entrypoint proves the kernel runs in the tab. Deliberate resolutions the spec left open (flagged, not silent): - `=` compares numbers across int/float (`(= 1 1.0)` → true) — JSON attributes make strict variant-equality a footgun. - Collection transformers (map/filter/rest/sort/…) return vectors (the spec's "data" form, §9.4); `conj`/`cons` preserve the natural structure. Deferred to follow-up L0/L1 slices (kept out to keep this reviewable): hygienic `defmacro` + `gensym`/`macroexpand`, multimethods (`defmulti`/`defmethod`), `let`/`fn` destructuring, and the notebook host bindings (`notes`, `set-attr!`, …) that a host registers through the seam. Verification: `cargo test` 66 green (34 kernel unit + 2 property [reader never panics; print→read round-trip] + existing core suite + the seam doc-test); `cargo clippy --all-targets -- -D warnings` clean; `cargo fmt --check` clean; `cargo check --features wasm` clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PAEqVP7Y3bZDhtAaPUTbps * fix(lambdadelta): remove reader unwrap; scope test-only panic exemption Hypatia code_safety flagged unwrap/panic sites on the L0 kernel: - reader.rs `read_one` used `forms.pop().unwrap()` on a length-1 vec. Replaced with `forms.remove(0)` (total) so the reader stays strictly panic-free — a real strengthening of the "reader never panics" guarantee, not a suppression. - tests.rs `unwrap()`/`panic!` are the idiomatic assertion mechanism for unit tests (a failed unwrap IS the test failure), not a runtime DoS surface. Added a scoped, documented .hypatia-ignore carve-out for the test file ALONE; the production kernel deliberately stays under the code_safety scanner. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PAEqVP7Y3bZDhtAaPUTbps * test(lambdadelta): assert on Results — remove unwrap/panic from the test module Rewrites the kernel test module to compare whole `Result`s (`assert_eq!(run(x), Ok(y))`) instead of unwrapping. Two wins: - Clears the Hypatia code_safety alerts (unwrap_without_check, panic_macro) on the test file — the gating SARIF check does not consult .hypatia-ignore, so the fix belongs in the code, not the exemption list. Reverts the speculative .hypatia-ignore carve-out; the governance file is back to its prior state. - Assertions now cover the error variant too, and the whole kernel (production AND tests) is free of unwrap/panic. No behaviour change; 32 kernel tests still green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PAEqVP7Y3bZDhtAaPUTbps --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent dd7d442 commit eb8aa4e

9 files changed

Lines changed: 2913 additions & 0 deletions

File tree

core/src/lambdadelta/builtins.rs

Lines changed: 1108 additions & 0 deletions
Large diffs are not rendered by default.

core/src/lambdadelta/error.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
//! λδ error values.
3+
//!
4+
//! Per the sandbox contract (spec §6) every failure is a *structured value*,
5+
//! never a panic: unbound symbol, arity mismatch, type error, budget-exceeded,
6+
//! and so on. The evaluator surfaces these as `Err(LdError)`; a later layer maps
7+
//! them onto in-notebook error values the UI can show against the offending
8+
//! form. The kernel itself is total — no `unwrap`/`panic` on user input.
9+
10+
use thiserror::Error;
11+
12+
/// A λδ diagnostic. Cheap to clone; carries enough context to point a user at
13+
/// what went wrong.
14+
#[derive(Debug, Clone, PartialEq, Eq, Error)]
15+
pub enum LdError {
16+
/// The reader could not make sense of the source text.
17+
#[error("read error at position {pos}: {msg}")]
18+
Read { msg: String, pos: usize },
19+
20+
/// A symbol was evaluated but is not bound in any enclosing scope.
21+
#[error("unbound symbol: {0}")]
22+
Unbound(String),
23+
24+
/// The head of a call form is not something that can be applied.
25+
#[error("not callable: {0}")]
26+
NotCallable(String),
27+
28+
/// A function or special form received the wrong number of arguments.
29+
#[error("{name}: wrong arity — expected {expected}, got {got}")]
30+
Arity {
31+
name: String,
32+
expected: String,
33+
got: usize,
34+
},
35+
36+
/// A value had the wrong type for the operation.
37+
#[error("{op}: type error — expected {expected}, got {got}")]
38+
Type {
39+
op: String,
40+
expected: String,
41+
got: String,
42+
},
43+
44+
/// Special-form syntax was malformed (e.g. an odd-length `let` binding).
45+
#[error("bad syntax in {form}: {msg}")]
46+
Syntax { form: String, msg: String },
47+
48+
/// Division (or `mod`) by zero.
49+
#[error("division by zero")]
50+
DivideByZero,
51+
52+
/// The evaluation budget (steps or recursion depth) was exhausted.
53+
#[error("budget exceeded: {0}")]
54+
Budget(String),
55+
56+
/// An error raised deliberately from λδ code.
57+
#[error("{0}")]
58+
User(String),
59+
}
60+
61+
/// Convenience alias for kernel results.
62+
pub type LdResult<T> = Result<T, LdError>;

0 commit comments

Comments
 (0)