diff --git a/hosts/ocaml/bin/mcp_tree.ml b/hosts/ocaml/bin/mcp_tree.ml index f91a92ce..0d20799a 100644 --- a/hosts/ocaml/bin/mcp_tree.ml +++ b/hosts/ocaml/bin/mcp_tree.ml @@ -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")]); diff --git a/spec/eval-rules.sx b/spec/eval-rules.sx new file mode 100644 index 00000000..0902da67 --- /dev/null +++ b/spec/eval-rules.sx @@ -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 "+ → , 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)) → "} + + {: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)))