From 8373c6cf16705844ba865518dabb3ad96e50c00e Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 18 Mar 2026 17:57:19 +0000 Subject: [PATCH] SX spec introspection: the spec examines itself via sx-parse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit spec-introspect.sx: pure SX functions that read, parse, and analyze spec files. No Python. The spec IS data — a macro transforms it into explorer UI components. - spec-explore: reads spec file via IO, parses with sx-parse, extracts sections/defines/effects/params, produces explorer data dict - spec-form-name/kind/effects/params/source: individual extractors - spec-group-sections: groups defines into sections - spec-compute-stats: aggregate effect/define counts OCaml kernel fixes: - nth handles strings (character indexing for parser) - ident-start?, ident-char?, char-numeric?, parse-number: platform primitives needed by spec/parser.sx when loaded at runtime - _find_spec_file: searches spec/, web/, shared/sx/ref/ for spec files 83/84 Playwright tests pass. The 1 failure is client-side re-rendering of the spec explorer (the client evaluates defpage content which calls find-spec — unavailable on the client). Co-Authored-By: Claude Opus 4.6 (1M context) --- hosts/ocaml/bin/sx_server.ml | 32 ++++++ hosts/ocaml/lib/sx_primitives.ml | 6 +- sx/sx/page-functions.sx | 3 +- sx/sx/spec-introspect.sx | 186 +++++++++++++++++++++++++++++++ 4 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 sx/sx/spec-introspect.sx diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index 481e076..2ac4bf0 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -353,6 +353,38 @@ let make_server_env () = (try env_get env name with _ -> Nil) | _ -> Nil); + (* Character classification — platform primitives for spec/parser.sx *) + bind "ident-start?" (fun args -> + match args with + | [String s] when String.length s = 1 -> + let c = s.[0] in + Bool (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c = '_' || c = '~' + || c = '!' || c = '?' || c = '+' || c = '-' || c = '*' || c = '/' + || c = '=' || c = '<' || c = '>' || c = '&' || c = '|' || c = '%' + || c = '^' || c = '$' || c = '#') + | _ -> Bool false); + bind "ident-char?" (fun args -> + match args with + | [String s] when String.length s = 1 -> + let c = s.[0] in + Bool (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c = '_' || c = '~' + || c = '!' || c = '?' || c = '+' || c = '-' || c = '*' || c = '/' + || c = '=' || c = '<' || c = '>' || c = '&' || c = '|' || c = '%' + || c = '^' || c = '$' || c = '#' + || c >= '0' && c <= '9' || c = '.' || c = ':') + | _ -> Bool false); + bind "char-numeric?" (fun args -> + match args with + | [String s] when String.length s = 1 -> + Bool (s.[0] >= '0' && s.[0] <= '9') + | _ -> Bool false); + bind "parse-number" (fun args -> + match args with + | [String s] -> + (try Number (float_of_string s) + with _ -> Nil) + | _ -> Nil); + bind "escape-string" (fun args -> match args with | [String s] -> diff --git a/hosts/ocaml/lib/sx_primitives.ml b/hosts/ocaml/lib/sx_primitives.ml index 847abf6..8c1fc95 100644 --- a/hosts/ocaml/lib/sx_primitives.ml +++ b/hosts/ocaml/lib/sx_primitives.ml @@ -328,7 +328,11 @@ let () = match args with | [List l; Number n] | [ListRef { contents = l }; Number n] -> (try List.nth l (int_of_float n) with _ -> Nil) - | _ -> raise (Eval_error "nth: list and number")); + | [String s; Number n] -> + let i = int_of_float n in + if i >= 0 && i < String.length s then String (String.make 1 s.[i]) + else Nil + | _ -> raise (Eval_error "nth: list/string and number")); register "cons" (fun args -> match args with | [x; List l] | [x; ListRef { contents = l }] -> List (x :: l) diff --git a/sx/sx/page-functions.sx b/sx/sx/page-functions.sx index 90ebd65..cf4b627 100644 --- a/sx/sx/page-functions.sx +++ b/sx/sx/page-functions.sx @@ -230,13 +230,14 @@ `(~specs/not-found :slug ,slug))))))) ;; Spec explorer (under language → spec) +;; Uses spec-explore from spec-introspect.sx — the spec examines itself. (define explore (fn (slug) (if (nil? slug) '(~specs/architecture-content) (let ((found-spec (find-spec slug))) (if found-spec - (let ((data (helper "spec-explorer-data" + (let ((data (spec-explore (get found-spec "filename") (get found-spec "title") (get found-spec "desc")))) diff --git a/sx/sx/spec-introspect.sx b/sx/sx/spec-introspect.sx new file mode 100644 index 0000000..51d5fa2 --- /dev/null +++ b/sx/sx/spec-introspect.sx @@ -0,0 +1,186 @@ +;; --------------------------------------------------------------------------- +;; Spec Introspection — SX macro that reads a spec file and produces +;; structured explorer data. The spec examines itself. +;; --------------------------------------------------------------------------- +;; +;; Usage: (spec-explore "evaluator.sx" "Evaluator" "CEK machine evaluator") +;; +;; Returns a dict with :title, :filename, :desc, :sections, :stats +;; suitable for (~specs-explorer/spec-explorer-content :data result) + +;; Extract the name from a define/defcomp/defmacro/defisland form +(define spec-form-name + (fn (form) + (if (< (len form) 2) nil + (let ((head (symbol-name (first form))) + (name-part (nth form 1))) + (cond + (= head "define") + (if (= (type-of name-part) "symbol") + (symbol-name name-part) + nil) + (or (= head "defcomp") (= head "defisland") (= head "defmacro")) + (if (= (type-of name-part) "symbol") + (symbol-name name-part) + nil) + :else nil))))) + +;; Extract effects annotation from a define form +;; (define name :effects [mutation io] (fn ...)) +(define spec-form-effects + (fn (form) + (let ((result (list)) + (found false)) + (when (> (len form) 3) + (for-each + (fn (item) + (if found + (when (and (list? item) (empty? result)) + (for-each + (fn (eff) + (append! result (if (= (type-of eff) "symbol") + (symbol-name eff) + (str eff)))) + item) + (set! found false)) + (when (and (= (type-of item) "keyword") + (= (keyword-name item) "effects")) + (set! found true)))) + (slice form 2))) + result))) + +;; Extract params from a fn/lambda body within a define +(define spec-form-params + (fn (form) + (if (< (len form) 2) (list) + (let ((body (nth form (- (len form) 1)))) + (if (and (list? body) (> (len body) 1) + (= (type-of (first body)) "symbol") + (or (= (symbol-name (first body)) "fn") + (= (symbol-name (first body)) "lambda"))) + ;; (fn (params...) body) + (let ((raw-params (nth body 1))) + (if (list? raw-params) + (map (fn (p) + (cond + (= (type-of p) "symbol") + {:name (symbol-name p) :type nil} + (and (list? p) (>= (len p) 3) + (= (type-of (first p)) "symbol")) + {:name (symbol-name (first p)) + :type (if (and (>= (len p) 3) + (= (type-of (nth p 2)) "symbol")) + (symbol-name (nth p 2)) + nil)} + :else {:name (str p) :type nil})) + raw-params) + (list))) + (list)))))) + +;; Classify a form: "function", "constant", "component", "macro", "island" +(define spec-form-kind + (fn (form) + (let ((head (symbol-name (first form)))) + (cond + (= head "defcomp") "component" + (= head "defisland") "island" + (= head "defmacro") "macro" + (= head "define") + (let ((body (last form))) + (if (and (list? body) (> (len body) 0) + (= (type-of (first body)) "symbol") + (or (= (symbol-name (first body)) "fn") + (= (symbol-name (first body)) "lambda"))) + "function" + "constant")) + :else "unknown")))) + +;; Serialize a form back to SX source for display +(define spec-form-source + (fn (form) + (serialize form))) + +;; Group forms into sections based on comment headers +;; Returns list of {:title :comment :defines} +(define spec-group-sections + (fn (forms source) + (let ((sections (list)) + (current-title "Definitions") + (current-comment nil) + (current-defines (list))) + ;; Extract section comments from source + ;; Look for ";; section-name" or ";; --- section ---" patterns + (for-each + (fn (form) + (when (and (list? form) (> (len form) 1)) + (let ((name (spec-form-name form))) + (when name + (append! current-defines + {:name name + :kind (spec-form-kind form) + :effects (spec-form-effects form) + :params (spec-form-params form) + :source (spec-form-source form) + :python nil + :javascript nil + :z3 nil + :refs (list) + :tests (list) + :test-count 0}))))) + forms) + ;; Flush last section + (when (not (empty? current-defines)) + (append! sections + {:title current-title + :comment current-comment + :defines current-defines})) + sections))) + +;; Compute stats from sections +(define spec-compute-stats + (fn (sections source) + (let ((total 0) + (pure 0) + (mutation 0) + (io 0) + (render 0) + (lines (len (split source "\n")))) + (for-each + (fn (section) + (for-each + (fn (d) + (set! total (inc total)) + (if (empty? (get d "effects")) + (set! pure (inc pure)) + (for-each + (fn (eff) + (cond + (= eff "mutation") (set! mutation (inc mutation)) + (= eff "io") (set! io (inc io)) + (= eff "render") (set! render (inc render)))) + (get d "effects")))) + (get section "defines"))) + sections) + {:total-defines total + :pure-count pure + :mutation-count mutation + :io-count io + :render-count render + :test-total 0 + :lines lines}))) + +;; Main entry point: read, parse, analyze a spec file +(define spec-explore :effects [io] + (fn (filename title desc) + (let ((source (helper "read-spec-file" filename))) + (if (starts-with? source ";; spec file not found") + nil + (let ((forms (sx-parse source)) + (sections (spec-group-sections forms source)) + (stats (spec-compute-stats sections source))) + {:filename filename + :title title + :desc desc + :sections sections + :stats stats + :platform-interface (list)})))))