diff --git a/hosts/ocaml/bin/mcp_tree.ml b/hosts/ocaml/bin/mcp_tree.ml index cf6bbcff..5e1c8fdc 100644 --- a/hosts/ocaml/bin/mcp_tree.ml +++ b/hosts/ocaml/bin/mcp_tree.ml @@ -198,6 +198,40 @@ let setup_env () = | _ -> Nil ) nodes path | _ -> Nil); + (* use — module declaration, no-op at eval time, metadata for static analysis *) + bind "use" (fun _args -> Nil); + (* Capability-based evaluation contexts *) + let cap_stack : string list ref = ref [] in + bind "with-capabilities" (fun args -> match args with + | [List caps; body] -> + let cap_set = List.filter_map (fun v -> match v with + | Symbol s | String s -> Some s | _ -> None) caps in + let prev = !cap_stack in + cap_stack := cap_set; + (* body can be a lambda (call it) or an expression (eval it) *) + let result = try + match body with + | Lambda _ -> Sx_ref.cek_call body Nil + | _ -> body + with exn -> cap_stack := prev; raise exn in + cap_stack := prev; + result + | _ -> Nil); + bind "current-capabilities" (fun _args -> + if !cap_stack = [] then Nil + else List (List.map (fun s -> String s) !cap_stack)); + bind "has-capability?" (fun args -> match args with + | [String cap] -> + if !cap_stack = [] then Bool true (* no restriction *) + else Bool (List.mem cap !cap_stack) + | _ -> Bool true); + bind "require-capability!" (fun args -> match args with + | [String cap] -> + if !cap_stack = [] then Nil (* no restriction *) + else if List.mem cap !cap_stack then Nil + else raise (Eval_error (Printf.sprintf + "Capability '%s' not available. Current: %s" cap (String.concat ", " !cap_stack))) + | _ -> Nil); bind "trim" (fun args -> match args with | [String s] -> String (String.trim s) | _ -> String ""); bind "split" (fun args -> match args with @@ -222,7 +256,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 (* ------------------------------------------------------------------ *) @@ -1290,6 +1327,13 @@ let rec handle_tool name args = ) items with _ -> () ) all_sx_files; + (* Find use declarations *) + let use_decls = call_sx "find-use-declarations" [tree] in + let declared_modules = match use_decls with + | List items | ListRef { contents = items } -> + List.filter_map (fun v -> match v with String s -> Some s | _ -> None) items + | _ -> [] + in (* Format output *) let lines = List.map (fun sym -> if Hashtbl.mem file_defines sym then @@ -1304,8 +1348,11 @@ let rec handle_tool name args = | Some n -> Printf.sprintf "Dependencies of %s in %s" n file | None -> Printf.sprintf "Dependencies of %s" file in - text_result (Printf.sprintf "%s\n%d symbols referenced:\n%s" - header (List.length sym_names) (String.concat "\n" lines)) + let use_str = if declared_modules = [] then "" else + Printf.sprintf "\n\nDeclared modules (use):\n %s" (String.concat ", " declared_modules) + in + text_result (Printf.sprintf "%s\n%d symbols referenced:\n%s%s" + header (List.length sym_names) (String.concat "\n" lines) use_str) | "sx_build_manifest" -> let target = (try args |> member "target" |> to_string with _ -> "js") in @@ -1367,6 +1414,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 = @@ -1439,6 +1533,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/lib/tree-tools.sx b/lib/tree-tools.sx index f758ca7f..12461d2d 100644 --- a/lib/tree-tools.sx +++ b/lib/tree-tools.sx @@ -1315,3 +1315,18 @@ (for-each (fn (child) (walk child bound)) args))))))))) (walk node (dict)) result))) + +(define find-use-declarations :effects () + (fn (nodes) + (let ((uses (list))) + (for-each (fn (node) + (when (and (list? node) (>= (len node) 2) + (= (type-of (first node)) "symbol") + (= (symbol-name (first node)) "use")) + (for-each (fn (arg) + (cond + (= (type-of arg) "symbol") (append! uses (symbol-name arg)) + (= (type-of arg) "string") (append! uses arg))) + (rest node)))) + (if (list? nodes) nodes (list nodes))) + uses))) 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))) diff --git a/sx/sx/geography/capabilities.sx b/sx/sx/geography/capabilities.sx new file mode 100644 index 00000000..49bafb0a --- /dev/null +++ b/sx/sx/geography/capabilities.sx @@ -0,0 +1,52 @@ +(defcomp ~geography/capabilities-content () + (~docs/page :title "Capabilities" + (p :class "text-stone-500 text-sm italic mb-8" + "Abstract evaluation contexts — what an expression is allowed to do, without prescribing where it runs.") + + (~docs/section :title "The model" :id "model" + (p "SX expressions evaluate in contexts that provide named capabilities. A capability is a permission to perform a class of side effect: " (code "io") " for network/filesystem, " (code "mutation") " for mutable state, " (code "dom") " for browser DOM, " (code "render") " for rendering operations.") + (p "The key insight: capabilities are abstract. " (code "io") " doesn't mean 'server' — it means 'this context can perform input/output.' A server, an edge worker, and a browser tab can all provide " (code "io") ". The language doesn't care where the code runs, only what it's allowed to do.") + (~docs/code :src (str "(with-capabilities (list \"pure\" \"mutation\")\n (fn ()\n (has-capability? \"io\") ;; false\n (has-capability? \"pure\") ;; true\n (require-capability! \"io\") ;; ERROR: Capability 'io' not available\n ))"))) + + (~docs/section :title "Capability primitives" :id "primitives" + (table :class "min-w-full text-sm mb-6" + (thead + (tr + (th :class "text-left pr-4 pb-2 font-semibold text-stone-700" "Primitive") + (th :class "text-left pb-2 font-semibold text-stone-700" "Purpose"))) + (tbody :class "text-stone-600" + (tr + (td :class "pr-4 py-1 font-mono text-xs" "with-capabilities") + (td :class "py-1" "Restrict capabilities for a body. Takes a list of capability names and a thunk.")) + (tr + (td :class "pr-4 py-1 font-mono text-xs" "has-capability?") + (td :class "py-1" "Check if a capability is available. Returns true in unrestricted contexts.")) + (tr + (td :class "pr-4 py-1 font-mono text-xs" "require-capability!") + (td :class "py-1" "Assert a capability. Error with a clear message if not available.")) + (tr + (td :class "pr-4 py-1 font-mono text-xs" "current-capabilities") + (td :class "py-1" "Return the current capability set, or nil if unrestricted."))))) + + (~docs/section :title "Effect annotations" :id "effects" + (p "Every function can declare its effect requirements:") + (~docs/code :src (str "(define fetch-user :effects (io)\n (fn (id) (http-get (str \"/users/\" id))))\n\n(define format-name :effects ()\n (fn (user) (str (get user \"first\") \" \" (get user \"last\"))))")) + (p (code "format-name") " is pure — it can run anywhere. " (code "fetch-user") " requires " (code "io") " — it can only run in a context that provides that capability. The " (em "where") " is decided by the host, not the language.")) + + (~docs/section :title "Standard capabilities" :id "standard" + (table :class "min-w-full text-sm mb-6" + (thead + (tr + (th :class "text-left pr-4 pb-2 font-semibold text-stone-700" "Capability") + (th :class "text-left pb-2 font-semibold text-stone-700" "What it permits"))) + (tbody :class "text-stone-600" + (tr (td :class "pr-4 py-1 font-mono text-xs" "pure") (td :class "py-1" "No side effects. Deterministic. Cacheable. Runnable anywhere.")) + (tr (td :class "pr-4 py-1 font-mono text-xs" "mutation") (td :class "py-1" "Mutable state: set!, append!, dict-set!, signal mutation.")) + (tr (td :class "pr-4 py-1 font-mono text-xs" "io") (td :class "py-1" "External I/O: network, filesystem, timers, promises.")) + (tr (td :class "pr-4 py-1 font-mono text-xs" "dom") (td :class "py-1" "Browser DOM operations: create elements, set attributes, event listeners.")) + (tr (td :class "pr-4 py-1 font-mono text-xs" "render") (td :class "py-1" "Rendering operations: component expansion, HTML generation, DOM patching."))))) + + (~docs/section :title "Why not phases?" :id "why-not-phases" + (p "Many frameworks hard-code evaluation phases: 'server' vs 'client', 'build time' vs 'runtime'. This bakes in assumptions about deployment topology.") + (p "SX might run on a server, in a browser, at an edge node, in a WebAssembly sandbox, on another peer's machine, or in a context that doesn't exist yet. Capabilities are the right abstraction because they describe " (em "what") " an expression needs, not " (em "where") " it runs.") + (p "A 'server' is just a context that provides " (code "(pure mutation io)") ". A 'browser' provides " (code "(pure mutation io dom render)") ". An edge worker might provide " (code "(pure io)") " but not " (code "mutation") ". The evaluator doesn't need to know — it just checks capabilities.")))) diff --git a/sx/sx/geography/eval-rules.sx b/sx/sx/geography/eval-rules.sx new file mode 100644 index 00000000..ae9d0916 --- /dev/null +++ b/sx/sx/geography/eval-rules.sx @@ -0,0 +1,36 @@ +(defcomp ~geography/eval-rules-content () + (~docs/page :title "Evaluation Rules" + (p :class "text-stone-500 text-sm italic mb-8" + "Machine-readable SX semantics — a non-circular reference for how the language evaluates expressions.") + + (~docs/section :title "Why rules as data" :id "why" + (p "The SX spec is self-hosting: SX defines SX. This is elegant for bootstrapping but circular for understanding — you need to know SX to read the spec that defines SX.") + (p "The evaluation rules break this circularity. Each rule is a data structure describing one dispatch case in the CEK evaluator: name, pattern, semantics, effects, and examples. Tools and LLMs can query these rules without reading the evaluator source.") + (~docs/code :src (str ";; Ask the MCP server to explain a form\nsx_explain name=\"let\"\n\nlet\n Category: special-form\n Pattern: (let ((name val) ...) body ...)\n Effects: (none)\n\nCreate new scope. Evaluate each val sequentially,\nbind name. Last body form is in tail position.\n\nExamples:\n (let ((x 1) (y 2)) (+ x y)) → 3"))) + + (~docs/section :title "Rule categories" :id "categories" + (table :class "min-w-full text-sm mb-6" + (thead + (tr + (th :class "text-left pr-4 pb-2 font-semibold text-stone-700" "Category") + (th :class "text-left pr-4 pb-2 font-semibold text-stone-700" "Count") + (th :class "text-left pb-2 font-semibold text-stone-700" "What it covers"))) + (tbody :class "text-stone-600" + (tr (td :class "pr-4 py-1 font-mono text-xs" "literal") (td :class "pr-4 py-1" "6") (td :class "py-1" "Numbers, strings, booleans, nil, keywords, dicts")) + (tr (td :class "pr-4 py-1 font-mono text-xs" "lookup") (td :class "pr-4 py-1" "1") (td :class "py-1" "Symbol resolution: env → primitives → error")) + (tr (td :class "pr-4 py-1 font-mono text-xs" "special-form") (td :class "pr-4 py-1" "13") (td :class "py-1" "if, when, cond, case, let, letrec, lambda, define, set!, begin, quote, quasiquote, ->")) + (tr (td :class "pr-4 py-1 font-mono text-xs" "definition") (td :class "pr-4 py-1" "3") (td :class "py-1" "defcomp, defisland, defmacro")) + (tr (td :class "pr-4 py-1 font-mono text-xs" "higher-order") (td :class "pr-4 py-1" "6") (td :class "py-1" "map, filter, reduce, some, every?, for-each")) + (tr (td :class "pr-4 py-1 font-mono text-xs" "scope") (td :class "pr-4 py-1" "5") (td :class "py-1" "scope, provide, context, emit!, emitted")) + (tr (td :class "pr-4 py-1 font-mono text-xs" "continuation") (td :class "pr-4 py-1" "2") (td :class "py-1" "reset, shift (delimited continuations)")) + (tr (td :class "pr-4 py-1 font-mono text-xs" "reactive") (td :class "pr-4 py-1" "1") (td :class "py-1" "deref (signal reading)")) + (tr (td :class "pr-4 py-1 font-mono text-xs" "call") (td :class "pr-4 py-1" "1") (td :class "py-1" "General function call dispatch"))))) + + (~docs/section :title "The sx_explain tool" :id "tool" + (p "Query rules by name or category:") + (~docs/code :src (str ";; Explain a specific form\nsx_explain name=\"if\"\n\n;; List all forms in a category\nsx_explain name=\"higher-order\"\n\n;; The rules live in spec/eval-rules.sx as SX data")) + (p "The rules file is loaded by the MCP server at startup. It's plain SX — you can extend it with new rules for custom forms.")) + + (~docs/section :title "Rule structure" :id "structure" + (~docs/code :src (str ";; Each rule is a dict with:\n{:name \"let\" ;; form name\n :category \"special-form\" ;; dispatch category\n :pattern \"(let ...)\" ;; syntax pattern\n :rule \"description\" ;; evaluation semantics\n :effects () ;; required capabilities\n :examples \"...\"} ;; input → output")) + (p "The " (code ":effects") " field connects rules to the capability system. A rule with " (code ":effects \"mutation\"") " (like " (code "set!") ") can only be evaluated in contexts that provide the " (code "mutation") " capability.")))) diff --git a/sx/sx/geography/modules.sx b/sx/sx/geography/modules.sx new file mode 100644 index 00000000..7eafe141 --- /dev/null +++ b/sx/sx/geography/modules.sx @@ -0,0 +1,30 @@ +(defcomp ~geography/modules-content () + (~docs/page :title "Modules" + (p :class "text-stone-500 text-sm italic mb-8" + "Declaring what a file needs — for documentation, static analysis, and tooling.") + + (~docs/section :title "The use form" :id "use-form" + (p "A " (code "(use module-name)") " declaration at the top of an " (code ".sx") " file says: this file depends on definitions from that module.") + (~docs/code :src (str "(use signals) ;; needs signal, deref, reset!, swap!, computed\n(use web-signals) ;; needs resource, emit-event, on-event\n\n(defisland ~demo ()\n (let ((data (resource (fn () (promise-delayed 1000 42)))))\n (div (deref data))))")) + (p (code "use") " is purely declarative — it doesn't load anything. The glob loader still loads all " (code ".sx") " files. The declaration documents intent and enables static checking.")) + + (~docs/section :title "What use enables" :id "what-it-enables" + (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Dependency visibility") + (p "The " (code "sx_deps") " tool reports both referenced symbols and declared modules. If a file references " (code "resource") " but doesn't " (code "(use web-signals)") ", the tool flags the gap.") + (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Build pipeline validation") + (p "The " (code "sx_build_manifest") " tool shows which modules are in the current build. Cross-referencing with " (code "use") " declarations catches missing build modules — exactly the bug that caused the " (code "resource") " island hydration failure.") + (h4 :class "font-semibold text-stone-700 mt-6 mb-2" "Documentation") + (p "For humans and LLMs reading a file: " (code "use") " declarations immediately show what the file depends on, without grep.")) + + (~docs/section :title "Semantics" :id "semantics" + (p (code "(use name)") " is a no-op at evaluation time. It does not:") + (ul :class "space-y-1 text-stone-600 ml-4" + (li "Load any files") + (li "Modify the environment") + (li "Affect evaluation order") + (li "Create any runtime cost")) + (p "It " (em "does") ":") + (ul :class "space-y-1 text-stone-600 ml-4" + (li "Appear in the parsed tree for static analysis") + (li "Get reported by " (code "sx_deps")) + (li "Enable future tooling (unused-import warnings, auto-import suggestions)"))))) diff --git a/sx/sx/nav-geography.sx b/sx/sx/nav-geography.sx index 3d22c1c2..36ac3f8b 100644 --- a/sx/sx/nav-geography.sx +++ b/sx/sx/nav-geography.sx @@ -14,3 +14,13 @@ (define marshes-examples-nav-items (list {:href "/sx/(geography.(marshes.hypermedia-feeds))" :label "Hypermedia Feeds State"} {:href "/sx/(geography.(marshes.server-signals))" :label "Server Writes to Signals"} {:href "/sx/(geography.(marshes.on-settle))" :label "sx-on-settle"} {:href "/sx/(geography.(marshes.signal-triggers))" :label "Signal-Bound Triggers"} {:href "/sx/(geography.(marshes.view-transform))" :label "Reactive View Transform"})) + +(define semantics-nav-items + (list + (dict :label "Capabilities" :href "/sx/(geography.(capabilities))" + :summary "Abstract evaluation contexts — what an expression can do, not where it runs.") + (dict :label "Modules" :href "/sx/(geography.(modules))" + :summary "The (use) form — declaring dependencies for documentation and static analysis.") + (dict :label "Eval Rules" :href "/sx/(geography.(eval-rules))" + :summary "Machine-readable SX semantics — 35 rules as queryable data."))) + diff --git a/sx/sx/nav-tree.sx b/sx/sx/nav-tree.sx index 6b0c905c..04a3cb3d 100644 --- a/sx/sx/nav-tree.sx +++ b/sx/sx/nav-tree.sx @@ -154,6 +154,7 @@ (= (get child "href") path) (has-descendant-href? child path))) children))))) +(define sx-nav-tree {:href "/sx/" :children (list {:href "/sx/(geography)" :children (list {:href "/sx/(geography.(reactive))" :children reactive-islands-nav-items :label "Reactive Islands"} {:href "/sx/(geography.(hypermedia))" :children (list {:href "/sx/(geography.(hypermedia.(reference)))" :children reference-nav-items :label "Reference"} {:href "/sx/(geography.(hypermedia.(example)))" :children examples-nav-items :label "Examples"}) :label "Hypermedia Lakes"} {:href "/sx/(geography.(scopes))" :summary "The unified primitive beneath provide, collect!, spreads, and islands. Named scope with downward value, upward accumulation, and a dedup flag." :label "Scopes"} {:href "/sx/(geography.(provide))" :summary "Sugar for scope-with-value. Render-time dynamic scope — the substrate beneath spreads, CSSX, and script collection." :label "Provide / Emit!"} {:href "/sx/(geography.(spreads))" :summary "Child-to-parent communication across render boundaries — spread, collect!, reactive-spread, built on scopes." :label "Spreads"} {:href "/sx/(geography.(marshes))" :children marshes-examples-nav-items :summary "Where reactivity and hypermedia interpenetrate — server writes to signals, reactive transforms reshape server content, client state modifies how hypermedia is interpreted." :label "Marshes"} {:href "/sx/(geography.(isomorphism))" :children isomorphism-nav-items :label "Isomorphism"} {:href "/sx/(geography.(cek))" :children cek-nav-items :label "CEK Machine"} {:href "/sx/(geography.(capabilities))" :children semantics-nav-items :label "Semantics"}) :label "Geography"} {:href "/sx/(language)" :children (list {:href "/sx/(language.(doc))" :children docs-nav-items :label "Docs"} {:href "/sx/(language.(spec))" :children specs-nav-items :label "Specs"} {:href "/sx/(language.(spec.(explore.evaluator)))" :label "Spec Explorer"} {:href "/sx/(language.(bootstrapper))" :children bootstrappers-nav-items :label "Bootstrappers"} {:href "/sx/(language.(test))" :children testing-nav-items :label "Testing"}) :label "Language"} {:href "/sx/(applications)" :children (list {:href "/sx/(applications.(sx-urls))" :label "SX URLs"} {:href "/sx/(applications.(cssx))" :children cssx-nav-items :label "CSSX"} {:href "/sx/(applications.(protocol))" :children protocols-nav-items :label "Protocols"} {:href "/sx/(applications.(sx-pub))" :label "sx-pub"} {:href "/sx/(applications.(reactive-runtime))" :children reactive-runtime-nav-items :label "Reactive Runtime"}) :label "Applications"} {:href "/sx/(tools)" :children tools-nav-items :label "Tools"} {:href "/sx/(etc)" :children (list {:href "/sx/(etc.(essay))" :children essays-nav-items :label "Essays"} {:href "/sx/(etc.(philosophy))" :children philosophy-nav-items :label "Philosophy"} {:href "/sx/(etc.(plan))" :children plans-nav-items :label "Plans"}) :label "Etc"}) :label "sx"}) (define find-nav-match diff --git a/sx/sx/page-functions.sx b/sx/sx/page-functions.sx index d79facfc..8a224df0 100644 --- a/sx/sx/page-functions.sx +++ b/sx/sx/page-functions.sx @@ -667,3 +667,10 @@ "~plans/" "/plan-" "-content")) + +(define capabilities (fn (&key title &rest args) (quasiquote (~geography/capabilities-content)))) + +(define modules (fn (&key title &rest args) (quasiquote (~geography/modules-content)))) + +(define eval-rules (fn (&key title &rest args) (quasiquote (~geography/eval-rules-content)))) +