Add eval-rules.sx and sx_explain tool

Machine-readable SX semantics reference with 35 evaluation rules
covering literals, lookup, special forms, definitions, higher-order
forms, scopes, continuations, and reactive primitives.

New sx_explain MCP tool: query by form name ("let", "map") or
category ("special-form", "higher-order") to get pattern, rule,
effects, and examples.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 14:26:31 +00:00
parent 3b4156c722
commit 36ba3eb298
2 changed files with 326 additions and 1 deletions

View File

@@ -222,7 +222,10 @@ let setup_env () =
(* Load harness *)
(try load_sx_file e (Filename.concat spec_dir "harness.sx")
with exn -> Printf.eprintf "[mcp] Warning: harness.sx load failed: %s\n%!" (Printexc.to_string exn));
Printf.eprintf "[mcp] SX tree-tools + harness loaded\n%!";
(* Load eval-rules *)
(try load_sx_file e (Filename.concat spec_dir "eval-rules.sx")
with exn -> Printf.eprintf "[mcp] Warning: eval-rules.sx load failed: %s\n%!" (Printexc.to_string exn));
Printf.eprintf "[mcp] SX tree-tools + harness + eval-rules loaded\n%!";
env := e
(* ------------------------------------------------------------------ *)
@@ -1204,6 +1207,53 @@ let rec handle_tool name args =
ignore (Unix.close_process_in ic);
text_result (Buffer.contents buf))
| "sx_explain" ->
let form_name = args |> member "name" |> to_string in
let e = !env in
let result = try
let find_fn = env_get e "find-rule" in
Sx_ref.cek_call find_fn (List [String form_name])
with _ -> Nil in
(match result with
| Dict d ->
let get_str k = match Hashtbl.find_opt d k with
| Some (String s) -> s | Some v -> value_to_string v | None -> "" in
let effects = match Hashtbl.find_opt d "effects" with
| Some (List items) -> String.concat ", " (List.map value_to_string items)
| Some Nil -> "none" | _ -> "none" in
let examples = match Hashtbl.find_opt d "examples" with
| Some (String s) -> " " ^ s
| Some (List items) ->
String.concat "\n" (List.map (fun ex -> " " ^ value_to_string ex) items)
| _ -> " (none)" in
text_result (Printf.sprintf "%s\n Category: %s\n Pattern: %s\n Effects: %s\n\n%s\n\nExamples:\n%s"
(get_str "name") (get_str "category") (get_str "pattern") effects
(get_str "rule") examples)
| _ ->
(* Try listing by category *)
let cats_fn = try env_get e "rules-by-category" with _ -> Nil in
let cat_results = try Sx_ref.cek_call cats_fn (List [String form_name]) with _ -> Nil in
(match cat_results with
| List items when items <> [] ->
let lines = List.map (fun rule ->
match rule with
| Dict rd ->
let name = match Hashtbl.find_opt rd "name" with Some (String s) -> s | _ -> "?" in
let pattern = match Hashtbl.find_opt rd "pattern" with Some (String s) -> s | _ -> "" in
Printf.sprintf " %-16s %s" name pattern
| _ -> " " ^ value_to_string rule
) items in
text_result (Printf.sprintf "Category: %s (%d rules)\n\n%s"
form_name (List.length items) (String.concat "\n" lines))
| _ ->
(* List all categories *)
let all_cats = try Sx_ref.cek_call (env_get e "rule-categories") Nil with _ -> Nil in
let cat_str = match all_cats with
| List items -> String.concat ", " (List.filter_map (fun v ->
match v with String s -> Some s | _ -> None) items)
| _ -> "?" in
error_result (Printf.sprintf "No rule found for '%s'. Categories: %s" form_name cat_str)))
| _ -> error_result ("Unknown tool: " ^ name)
and write_edit file result =
@@ -1276,6 +1326,8 @@ let tool_definitions = `List [
[("expr", `Assoc [("type", `String "string"); ("description", `String "SX expression to trace")]);
("file", `Assoc [("type", `String "string"); ("description", `String "Optional .sx file to load for definitions")]);
("max_steps", `Assoc [("type", `String "integer"); ("description", `String "Max CEK steps to show (default: 200)")])] ["expr"];
tool "sx_explain" "Explain SX evaluation rules. Pass a form name (if, let, map, ...) or category (literal, special-form, higher-order, ...)."
[("name", `Assoc [("type", `String "string"); ("description", `String "Form name or category to explain")])] ["name"];
tool "sx_deps" "Dependency analysis for a component or file. Shows all referenced symbols and where they're defined."
[file_prop;
("name", `Assoc [("type", `String "string"); ("description", `String "Specific define/defcomp/defisland to analyze")]);

273
spec/eval-rules.sx Normal file
View File

@@ -0,0 +1,273 @@
;; Evaluation rules — machine-readable SX semantics reference.
;;
;; Each rule describes one dispatch case in the CEK evaluator.
;; Rules are data — queried by tools (sx_explain), validated against behavior.
;; Examples are strings to avoid evaluation: "expr → result"
(define eval-rules
(list
{:name "number" :category "literal"
:pattern "42"
:rule "Numbers evaluate to themselves."
:effects ()
:examples "42 → 42, -3.14 → -3.14"}
{:name "string" :category "literal"
:pattern "\"hello\""
:rule "Strings evaluate to themselves."
:effects ()
:examples "\"hello\" → \"hello\""}
{:name "boolean" :category "literal"
:pattern "true | false"
:rule "Booleans evaluate to themselves."
:effects ()
:examples "true → true, false → false"}
{:name "nil" :category "literal"
:pattern "nil"
:rule "Nil evaluates to itself. Nil is falsy."
:effects ()
:examples "nil → nil"}
{:name "keyword" :category "literal"
:pattern ":name"
:rule "Keywords evaluate to their string name."
:effects ()
:examples ":foo → \"foo\", :class → \"class\""}
{:name "dict" :category "literal"
:pattern "{:key1 val1 :key2 val2 ...}"
:rule "Create a dictionary. Keys are keywords (evaluated to strings). Values are evaluated."
:effects ()
:examples "{:x 1 :y 2} → {\"x\" 1 \"y\" 2}"}
{:name "symbol" :category "lookup"
:pattern "name"
:rule "Look up in: (1) environment chain, (2) primitives, (3) true/false/nil literals. Error if not found."
:effects ()
:examples "+ → <native:+>, undefined → ERROR"}
{:name "if" :category "special-form"
:pattern "(if test then else?)"
:rule "Evaluate test. If truthy (not false, not nil), evaluate then. Otherwise evaluate else (or nil if absent). Both branches are in tail position."
:effects ()
:examples "(if true 1 2) → 1, (if false 1 2) → 2, (if nil 1) → nil"}
{:name "when" :category "special-form"
:pattern "(when test body ...)"
:rule "Evaluate test. If truthy, evaluate body forms in sequence, return last. If falsy, return nil. Last body form is in tail position."
:effects ()
:examples "(when true 1 2 3) → 3, (when false 1) → nil"}
{:name "cond" :category "special-form"
:pattern "(cond test1 expr1 test2 expr2 ... :else default)"
:rule "Evaluate tests in order. First truthy test: evaluate and return its expr. :else always matches. If no match, return nil."
:effects ()
:examples "(cond false 1 true 2) → 2, (cond false 1 :else 3) → 3"}
{:name "case" :category "special-form"
:pattern "(case expr val1 result1 val2 result2 ... :else default)"
:rule "Evaluate expr once. Compare against each val (by equality). Return the matched result. :else is the fallback."
:effects ()
:examples "(case 2 1 \"one\" 2 \"two\") → \"two\""}
{:name "and" :category "special-form"
:pattern "(and expr ...)"
:rule "Evaluate left to right. Return first falsy value, or last value if all truthy. Short-circuits."
:effects ()
:examples "(and 1 2 3) → 3, (and 1 false 3) → false"}
{:name "or" :category "special-form"
:pattern "(or expr ...)"
:rule "Evaluate left to right. Return first truthy value, or last value if all falsy. Short-circuits."
:effects ()
:examples "(or false 2 3) → 2, (or false nil) → nil"}
{:name "let" :category "special-form"
:pattern "(let ((name val) ...) body ...)"
:rule "Create new scope. Evaluate each val sequentially, bind name. Then evaluate body forms, return last. Values see only earlier bindings. Last body form is in tail position."
:effects ()
:examples "(let ((x 1) (y 2)) (+ x y)) → 3"}
{:name "letrec" :category "special-form"
:pattern "(letrec ((name val) ...) body ...)"
:rule "Like let, but all bindings are visible to all vals (mutual recursion). Bindings exist before vals are evaluated."
:effects ()
:examples "(letrec ((f (fn (n) (if (= n 0) 1 (* n (f (- n 1))))))) (f 5)) → 120"}
{:name "lambda" :category "special-form"
:pattern "(fn (params ...) body ...) | (lambda (params ...) body ...)"
:rule "Create a closure capturing the current environment. Parameters support: positional, &key (keyword args), &rest (variadic), (:as type) annotations. Last body form is in tail position."
:effects ()
:examples "(fn (x) (+ x 1)) → <lambda>"}
{:name "define" :category "special-form"
:pattern "(define name value) | (define name :effects (e ...) value)"
:rule "Evaluate value, bind name in the current environment. Optional :effects annotation declares side effects. Returns the value."
:effects ()
:examples "(define x 42) → 42"}
{:name "set!" :category "special-form"
:pattern "(set! name value)"
:rule "Evaluate value, mutate existing binding of name. Walks the scope chain to find the binding. Error if name is not bound."
:effects "mutation"
:examples "(let ((x 1)) (set! x 2) x) → 2"}
{:name "begin" :category "special-form"
:pattern "(begin expr ...) | (do expr ...)"
:rule "Evaluate expressions in sequence, return last. Last form is in tail position."
:effects ()
:examples "(begin 1 2 3) → 3"}
{:name "quote" :category "special-form"
:pattern "(quote expr)"
:rule "Return expr unevaluated."
:effects ()
:examples "(quote (+ 1 2)) → (+ 1 2)"}
{:name "quasiquote" :category "special-form"
:pattern "`expr with ,x and ,@xs"
:rule "Like quote, but (unquote x) evaluates x, and (splice-unquote x) splices a list."
:effects ()
:examples "(let ((x 1)) `(a ,x b)) → (a 1 b)"}
{:name "thread-first" :category "special-form"
:pattern "(-> val (fn1 args...) (fn2 args...) ...)"
:rule "Thread val through forms. Each form receives the previous result as its first argument. (-> x (f a)) becomes (f x a)."
:effects ()
:examples "(-> 1 (+ 2) (* 3)) → 9"}
{:name "defcomp" :category "definition"
:pattern "(defcomp ~name (params ...) body ...)"
:rule "Define a component. Keyword args via &key, variadic via &rest. Body evaluated in merged env (closure + caller-env + params)."
:effects ()
:examples "(defcomp ~card (&key title) (div (h2 title)))"}
{:name "defisland" :category "definition"
:pattern "(defisland ~name (params ...) body ...)"
:rule "Define an island — a component that hydrates on the client. Server renders a placeholder; client evaluates the body with reactive capabilities."
:effects ()
:examples "(defisland ~counter () (let ((n (signal 0))) (button :on-click (fn (e) (swap! n inc)) (deref n))))"}
{:name "defmacro" :category "definition"
:pattern "(defmacro name (params ...) body ...)"
:rule "Define a macro. At call time, args are passed unevaluated. Body produces a new expression which is then evaluated."
:effects ()
:examples "(defmacro unless (test body) `(if (not ,test) ,body))"}
{:name "map" :category "higher-order"
:pattern "(map fn coll) | (map coll fn)"
:rule "Apply fn to each element of coll, return new list. Argument order is flexible."
:effects ()
:examples "(map (fn (x) (* x 2)) (list 1 2 3)) → (2 4 6)"}
{:name "filter" :category "higher-order"
:pattern "(filter fn coll) | (filter coll fn)"
:rule "Return elements of coll where fn returns truthy. Flexible argument order."
:effects ()
:examples "(filter (fn (x) (> x 2)) (list 1 2 3 4)) → (3 4)"}
{:name "reduce" :category "higher-order"
:pattern "(reduce fn init coll)"
:rule "Fold coll from left. Call (fn acc item) for each element, starting with init."
:effects ()
:examples "(reduce + 0 (list 1 2 3)) → 6"}
{:name "some" :category "higher-order"
:pattern "(some fn coll)"
:rule "Return first truthy result of (fn item), or false if none."
:effects ()
:examples "(some (fn (x) (> x 2)) (list 1 2 3)) → true"}
{:name "every?" :category "higher-order"
:pattern "(every? fn coll)"
:rule "Return true if (fn item) is truthy for all items."
:effects ()
:examples "(every? (fn (x) (> x 0)) (list 1 2 3)) → true"}
{:name "for-each" :category "higher-order"
:pattern "(for-each fn coll)"
:rule "Call (fn item) for each element. Returns nil. Used for side effects."
:effects "mutation"
:examples "(for-each print (list 1 2 3)) → nil (prints 1, 2, 3)"}
{:name "scope" :category "scope"
:pattern "(scope name body ...)"
:rule "Create a named dynamic scope. The unified primitive beneath provide, collect!, and spreads."
:effects "mutation"
:examples "(scope \"my-scope\" (emit! \"my-scope\" 42) (emitted \"my-scope\")) → (42)"}
{:name "provide" :category "scope"
:pattern "(provide name value body ...)"
:rule "Make value available to descendants via (context name)."
:effects "mutation"
:examples "(provide \"theme\" \"dark\" (context \"theme\")) → \"dark\""}
{:name "context" :category "scope"
:pattern "(context name)"
:rule "Retrieve the value from the nearest enclosing (provide name value ...)."
:effects ()
:examples "(provide \"x\" 42 (context \"x\")) → 42"}
{:name "emit!" :category "scope"
:pattern "(emit! name value)"
:rule "Emit a value upward into a named scope."
:effects "mutation"
:examples "see scope example"}
{:name "emitted" :category "scope"
:pattern "(emitted name)"
:rule "Collect all values emitted into the named scope."
:effects ()
:examples "see scope example"}
{:name "reset" :category "continuation"
:pattern "(reset body ...)"
:rule "Delimit a continuation. shift inside body captures up to this point."
:effects ()
:examples "(reset (+ 1 (shift k (k 10)))) → 11"}
{:name "shift" :category "continuation"
:pattern "(shift k body ...)"
:rule "Capture the continuation up to the nearest reset. k is a function that resumes the captured computation."
:effects ()
:examples "(reset (+ 1 (shift k (k (k 10))))) → 12"}
{:name "deref" :category "reactive"
:pattern "(deref signal)"
:rule "Read the current value of a reactive signal. In a reactive context, establishes a dependency."
:effects ()
:examples "(let ((s (signal 42))) (deref s)) → 42"}
{:name "function-call" :category "call"
:pattern "(f arg1 arg2 ...)"
:rule "Evaluate f and all args left to right. Then: native → apply, lambda → bind params + TCO, component → parse kwargs + bind params + TCO, macro → expand unevaluated args + re-eval."
:effects ()
:examples "(+ 1 2) → 3, (list 1 2 3) → (1 2 3)"}
))
;; Lookup helpers
(define find-rule
(fn (name)
(some (fn (rule)
(when (= (get rule "name") name) rule))
eval-rules)))
(define rules-by-category
(fn (category)
(filter (fn (rule) (= (get rule "category") category))
eval-rules)))
(define rule-categories
(fn ()
(let ((seen (dict)) (result (list)))
(for-each (fn (rule)
(let ((cat (get rule "category")))
(when (not (has-key? seen cat))
(dict-set! seen cat true)
(append! result cat))))
eval-rules)
result)))