Merge semantics: eval rules, capabilities, modules, geography pages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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")]);
|
||||
|
||||
@@ -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)))
|
||||
|
||||
273
spec/eval-rules.sx
Normal file
273
spec/eval-rules.sx
Normal 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)))
|
||||
52
sx/sx/geography/capabilities.sx
Normal file
52
sx/sx/geography/capabilities.sx
Normal file
@@ -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."))))
|
||||
36
sx/sx/geography/eval-rules.sx
Normal file
36
sx/sx/geography/eval-rules.sx
Normal file
@@ -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."))))
|
||||
30
sx/sx/geography/modules.sx
Normal file
30
sx/sx/geography/modules.sx
Normal file
@@ -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)")))))
|
||||
@@ -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.")))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user