Spec explorer: fix SxExpr rendering bugs, add drill-in UX, Playwright tests
Fix 3 OCaml bugs that caused spec explorer to hang: - sx_types: inspect outputs quoted string for SxExpr (not bare symbol) - sx_primitives: serialize/to_string extract SxExpr/RawHTML content - sx_render: handle SxExpr in both render-to-html paths Restructure spec explorer for performance: - Lightweight overview: name + kind only (was full source for 141 defs) - Drill-in detail: click definition → params, effects, signature - explore() page function accepts optional second arg for drill-in - spec() passes through non-string slugs from nested routing Fix aser map result wrapping: - aser-special map now wraps results in fragment (<> ...) via aser-fragment - Prevents ((div ...) (div ...)) nested lists that caused client "Not callable" 5 Playwright tests: overview load, no errors, SPA nav, drill-in detail+params Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -65,6 +65,8 @@ let rec to_string = function
|
||||
| Symbol s -> s
|
||||
| Keyword k -> k
|
||||
| Thunk _ as t -> to_string (!trampoline_hook t)
|
||||
| SxExpr s -> s
|
||||
| RawHTML s -> s
|
||||
| v -> inspect v
|
||||
|
||||
let () =
|
||||
@@ -126,10 +128,23 @@ let () =
|
||||
Number (Float.max lo (Float.min hi x))
|
||||
| _ -> raise (Eval_error "clamp: 3 args"));
|
||||
register "parse-int" (fun args ->
|
||||
let parse_leading_int s =
|
||||
let len = String.length s in
|
||||
let start = ref 0 in
|
||||
let neg = len > 0 && s.[0] = '-' in
|
||||
if neg then start := 1
|
||||
else if len > 0 && s.[0] = '+' then start := 1;
|
||||
let j = ref !start in
|
||||
while !j < len && s.[!j] >= '0' && s.[!j] <= '9' do incr j done;
|
||||
if !j > !start then
|
||||
let n = int_of_string (String.sub s !start (!j - !start)) in
|
||||
Some (if neg then -n else n)
|
||||
else None
|
||||
in
|
||||
match args with
|
||||
| [String s] -> (match int_of_string_opt s with Some n -> Number (float_of_int n) | None -> Nil)
|
||||
| [String s] -> (match parse_leading_int s with Some n -> Number (float_of_int n) | None -> Nil)
|
||||
| [String s; default_val] ->
|
||||
(match int_of_string_opt s with Some n -> Number (float_of_int n) | None -> default_val)
|
||||
(match parse_leading_int s with Some n -> Number (float_of_int n) | None -> default_val)
|
||||
| [Number n] | [Number n; _] -> Number (float_of_int (int_of_float n))
|
||||
| [_; default_val] -> default_val
|
||||
| _ -> Nil);
|
||||
@@ -276,7 +291,17 @@ let () =
|
||||
else if String.sub haystack i nl = needle then Number (float_of_int i)
|
||||
else find (i + 1)
|
||||
in find 0
|
||||
| _ -> raise (Eval_error "index-of: 2 string args"));
|
||||
| [List items; target] | [ListRef { contents = items }; target] ->
|
||||
let eq a b = match a, b with
|
||||
| String x, String y -> x = y | Number x, Number y -> x = y
|
||||
| Symbol x, Symbol y -> x = y | Keyword x, Keyword y -> x = y
|
||||
| Bool x, Bool y -> x = y | Nil, Nil -> true | _ -> a == b in
|
||||
let rec find i = function
|
||||
| [] -> Nil
|
||||
| h :: _ when eq h target -> Number (float_of_int i)
|
||||
| _ :: tl -> find (i + 1) tl
|
||||
in find 0 items
|
||||
| _ -> raise (Eval_error "index-of: 2 string args or list+target"));
|
||||
register "substring" (fun args ->
|
||||
match args with
|
||||
| [String s; Number start; Number end_] ->
|
||||
@@ -655,6 +680,8 @@ let () =
|
||||
match args with [a] -> String (inspect a) | _ -> raise (Eval_error "inspect: 1 arg"));
|
||||
register "serialize" (fun args ->
|
||||
match args with
|
||||
| [SxExpr s] -> String s
|
||||
| [RawHTML s] -> String s
|
||||
| [a] -> String (inspect a) (* used for dedup keys in compiler *)
|
||||
| _ -> raise (Eval_error "serialize: 1 arg"));
|
||||
register "make-symbol" (fun args ->
|
||||
|
||||
@@ -201,6 +201,7 @@ let rec do_render_to_html (expr : value) (env : env) : string =
|
||||
| String s -> escape_html s
|
||||
| Keyword k -> escape_html k
|
||||
| RawHTML s -> s
|
||||
| SxExpr s -> s
|
||||
| Symbol s ->
|
||||
let v = Sx_ref.eval_expr (Symbol s) (Env env) in
|
||||
do_render_to_html v env
|
||||
@@ -280,7 +281,12 @@ and render_list_to_html head args env =
|
||||
| _ ->
|
||||
let result = Sx_ref.eval_expr (List (head :: args)) (Env env) in
|
||||
do_render_to_html result env)
|
||||
with Eval_error _ -> "")
|
||||
with Eval_error _ ->
|
||||
(* Primitive or special form — not in env, delegate to CEK *)
|
||||
(try
|
||||
let result = Sx_ref.eval_expr (List (head :: args)) (Env env) in
|
||||
do_render_to_html result env
|
||||
with Eval_error _ -> ""))
|
||||
| _ ->
|
||||
let result = Sx_ref.eval_expr (List (head :: args)) (Env env) in
|
||||
do_render_to_html result env
|
||||
@@ -456,6 +462,7 @@ let rec render_to_buf buf (expr : value) (env : env) : unit =
|
||||
| String s -> escape_html_buf buf s
|
||||
| Keyword k -> escape_html_buf buf k
|
||||
| RawHTML s -> Buffer.add_string buf s
|
||||
| SxExpr s -> Buffer.add_string buf s
|
||||
| Symbol s ->
|
||||
let v = Sx_ref.eval_expr (Symbol s) (Env env) in
|
||||
render_to_buf buf v env
|
||||
|
||||
@@ -531,9 +531,9 @@ let rec inspect = function
|
||||
| Continuation (_, _) -> "<continuation>"
|
||||
| NativeFn (name, _) -> Printf.sprintf "<native:%s>" name
|
||||
| Signal _ -> "<signal>"
|
||||
| RawHTML s -> Printf.sprintf "<raw-html:%d chars>" (String.length s)
|
||||
| RawHTML s -> Printf.sprintf "\"<raw-html:%d>\"" (String.length s)
|
||||
| Spread _ -> "<spread>"
|
||||
| SxExpr s -> Printf.sprintf "<sx-expr:%d chars>" (String.length s)
|
||||
| SxExpr s -> Printf.sprintf "\"<sx-expr:%d>\"" (String.length s)
|
||||
| Env _ -> "<env>"
|
||||
| CekState _ -> "<cek-state>"
|
||||
| CekFrame f -> Printf.sprintf "<frame:%s>" f.cf_type
|
||||
|
||||
22
sx/sx/handlers/spec-detail.sx
Normal file
22
sx/sx/handlers/spec-detail.sx
Normal file
@@ -0,0 +1,22 @@
|
||||
(defhandler
|
||||
spec-detail
|
||||
:path "/sx/api/spec-detail"
|
||||
:method :get
|
||||
:returns "element"
|
||||
(&key)
|
||||
(let
|
||||
((filename (helper "request-arg" "file" ""))
|
||||
(def-name (helper "request-arg" "name" "")))
|
||||
(if
|
||||
(or (= filename "") (= def-name ""))
|
||||
(div
|
||||
:class "text-sm text-stone-400 p-2"
|
||||
"Missing file or name parameter")
|
||||
(let
|
||||
((d (spec-explore-define filename def-name)))
|
||||
(if
|
||||
d
|
||||
(~specs-explorer/spec-explorer-define-detail :d d :filename filename)
|
||||
(div
|
||||
:class "text-sm text-stone-400 p-2"
|
||||
(str "Definition '" def-name "' not found in " filename)))))))
|
||||
@@ -213,10 +213,12 @@
|
||||
spec
|
||||
(fn
|
||||
(slug)
|
||||
(if
|
||||
(cond
|
||||
(nil? slug)
|
||||
(quote (~specs/architecture-content))
|
||||
(case
|
||||
(not (= (type-of slug) "string"))
|
||||
slug
|
||||
:else (case
|
||||
slug
|
||||
"core"
|
||||
(let
|
||||
@@ -278,22 +280,34 @@
|
||||
(define
|
||||
explore
|
||||
(fn
|
||||
(slug)
|
||||
(slug defname)
|
||||
(if
|
||||
(nil? slug)
|
||||
(quote (~specs/architecture-content))
|
||||
(let
|
||||
((found-spec (find-spec slug)))
|
||||
(if
|
||||
found-spec
|
||||
(let
|
||||
((data (spec-explore (get found-spec "filename") (get found-spec "title") (get found-spec "desc"))))
|
||||
(if
|
||||
data
|
||||
(quasiquote
|
||||
(~specs-explorer/spec-explorer-content :data (unquote data)))
|
||||
(quasiquote (~specs/not-found :slug (unquote slug)))))
|
||||
(quasiquote (~specs/not-found :slug (unquote slug))))))))
|
||||
(not found-spec)
|
||||
(quasiquote (~specs/not-found :slug (unquote slug)))
|
||||
(if
|
||||
defname
|
||||
(let
|
||||
((d (spec-explore-define (get found-spec "filename") defname)))
|
||||
(if
|
||||
d
|
||||
(quasiquote
|
||||
(~specs-explorer/spec-explorer-define-detail
|
||||
:d (unquote d)
|
||||
:filename (unquote (get found-spec "filename"))))
|
||||
(quasiquote
|
||||
(~specs/not-found :slug (unquote (str slug "." defname))))))
|
||||
(let
|
||||
((data (spec-explore (get found-spec "filename") (get found-spec "title") (get found-spec "desc"))))
|
||||
(if
|
||||
data
|
||||
(quasiquote
|
||||
(~specs-explorer/spec-explorer-content :data (unquote data)))
|
||||
(quasiquote (~specs/not-found :slug (unquote slug)))))))))))
|
||||
|
||||
(define
|
||||
make-spec-files
|
||||
|
||||
@@ -1,186 +1,238 @@
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 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)))
|
||||
(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)
|
||||
(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)
|
||||
(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)
|
||||
(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))
|
||||
(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))))
|
||||
(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"))
|
||||
(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}))
|
||||
(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")))
|
||||
(let
|
||||
((raw-params (nth body 1)))
|
||||
(if
|
||||
(list? raw-params)
|
||||
(map
|
||||
(fn
|
||||
(p)
|
||||
(cond
|
||||
(= (type-of p) "symbol")
|
||||
{:type nil :name (symbol-name p)}
|
||||
(and
|
||||
(list? p)
|
||||
(>= (len p) 3)
|
||||
(= (type-of (first p)) "symbol"))
|
||||
{:type (if (and (>= (len p) 3) (= (type-of (nth p 2)) "symbol")) (symbol-name (nth p 2)) nil) :name (symbol-name (first p))}
|
||||
:else {:type nil :name (str p)}))
|
||||
raw-params)
|
||||
(list)))
|
||||
(list))))))
|
||||
|
||||
;; Classify a form: "function", "constant", "component", "macro", "island"
|
||||
(define spec-form-kind
|
||||
(fn (form)
|
||||
(let ((head (symbol-name (first form))))
|
||||
(define
|
||||
spec-form-kind
|
||||
(fn
|
||||
(form)
|
||||
(let
|
||||
((head (symbol-name (first form))))
|
||||
(cond
|
||||
(= head "defcomp") "component"
|
||||
(= head "defisland") "island"
|
||||
(= head "defmacro") "macro"
|
||||
(= 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"))
|
||||
(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)))
|
||||
(define
|
||||
spec-form-signature
|
||||
(fn
|
||||
(form)
|
||||
(let
|
||||
((head (symbol-name (first form))) (name (spec-form-name form)))
|
||||
(cond
|
||||
(or
|
||||
(= head "define")
|
||||
(= head "defcomp")
|
||||
(= head "defisland")
|
||||
(= head "defmacro"))
|
||||
(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")))
|
||||
(let
|
||||
((params (nth body 1)))
|
||||
(str "(" head " " name " (fn " (serialize params) " …))"))
|
||||
(str "(" head " " name " …)")))
|
||||
:else (str "(" head " " name " …)")))))
|
||||
|
||||
;; 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
|
||||
(define
|
||||
spec-group-sections
|
||||
(fn
|
||||
(forms source)
|
||||
(let
|
||||
((sections (list))
|
||||
(current-title "Definitions")
|
||||
(current-comment nil)
|
||||
(current-defines (list)))
|
||||
(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})))))
|
||||
(fn
|
||||
(form)
|
||||
(when
|
||||
(and (list? form) (> (len form) 1))
|
||||
(let
|
||||
((name (spec-form-name form)))
|
||||
(when name (append! current-defines {:kind (spec-form-kind form) :name name})))))
|
||||
forms)
|
||||
;; Flush last section
|
||||
(when (not (empty? current-defines))
|
||||
(append! sections
|
||||
{:title current-title
|
||||
:comment current-comment
|
||||
:defines current-defines}))
|
||||
(when (not (empty? current-defines)) (append! sections {:defines current-defines :title current-title :comment current-comment}))
|
||||
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"))))
|
||||
(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)
|
||||
(fn
|
||||
(section)
|
||||
(for-each
|
||||
(fn (d)
|
||||
(fn
|
||||
(d)
|
||||
(set! total (inc total))
|
||||
(if (empty? (get d "effects"))
|
||||
(if
|
||||
(empty? (get d "effects"))
|
||||
(set! pure (inc pure))
|
||||
(for-each
|
||||
(fn (eff)
|
||||
(fn
|
||||
(eff)
|
||||
(cond
|
||||
(= eff "mutation") (set! mutation (inc mutation))
|
||||
(= eff "io") (set! io (inc io))
|
||||
(= eff "render") (set! render (inc render))))
|
||||
(= 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})))
|
||||
{:lines lines :io-count io :render-count render :pure-count pure :mutation-count mutation :test-total 0 :total-defines total})))
|
||||
|
||||
;; 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")
|
||||
(define
|
||||
spec-explore-define
|
||||
:effects (io)
|
||||
(fn
|
||||
(filename def-name)
|
||||
(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)})))))
|
||||
(let
|
||||
((forms (sx-parse source)) (found nil))
|
||||
(for-each
|
||||
(fn
|
||||
(form)
|
||||
(when
|
||||
(and (not found) (list? form) (> (len form) 1))
|
||||
(let
|
||||
((name (spec-form-name form)))
|
||||
(when (= name def-name) (set! found {:kind (spec-form-kind form) :effects (spec-form-effects form) :params (spec-form-params form) :source (spec-form-signature form) :name name})))))
|
||||
forms)
|
||||
found)))))
|
||||
|
||||
(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)))
|
||||
{:stats stats :desc desc :title title :filename filename :platform-interface (list) :sections sections})))))
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
:slug (replace (get data "filename") ".sx" ""))
|
||||
(~specs-explorer/spec-explorer-stats :stats (get data "stats"))
|
||||
(map
|
||||
(fn (section) (~specs-explorer/spec-explorer-section :section section))
|
||||
(fn
|
||||
(section)
|
||||
(~specs-explorer/spec-explorer-section
|
||||
:section section
|
||||
:filename (get data "filename")))
|
||||
(get data "sections"))
|
||||
(when
|
||||
(not (empty? (get data "platform-interface")))
|
||||
@@ -78,56 +82,95 @@
|
||||
|
||||
(defcomp
|
||||
~specs-explorer/spec-explorer-section
|
||||
(&key section)
|
||||
(&key section filename)
|
||||
(div
|
||||
:class "mb-8"
|
||||
:class "mb-6"
|
||||
(h2
|
||||
:class "text-lg font-semibold text-stone-700 border-b border-stone-200 pb-1 mb-3"
|
||||
:class "text-base font-semibold text-stone-600 mb-2 border-b border-stone-200 pb-1"
|
||||
:id (replace (lower (get section "title")) " " "-")
|
||||
(get section "title"))
|
||||
(when
|
||||
(get section "comment")
|
||||
(p :class "text-sm text-stone-500 mb-3" (get section "comment")))
|
||||
(p :class "text-sm text-stone-500 mb-2" (get section "comment")))
|
||||
(div
|
||||
:class "space-y-4"
|
||||
:class "space-y-0.5"
|
||||
(map
|
||||
(fn (d) (~specs-explorer/spec-explorer-define :d d))
|
||||
(fn
|
||||
(d)
|
||||
(~specs-explorer/spec-explorer-define :d d :filename filename))
|
||||
(get section "defines")))))
|
||||
|
||||
(defcomp
|
||||
~specs-explorer/spec-explorer-define
|
||||
(&key d)
|
||||
(&key d filename)
|
||||
(div
|
||||
:class "rounded border border-stone-200 p-4"
|
||||
:class "flex items-center gap-2 py-1.5 px-2 rounded hover:bg-stone-50 cursor-pointer group"
|
||||
:id (str "fn-" (get d "name"))
|
||||
:sx-get (str
|
||||
"/sx/(language.(spec.(explore."
|
||||
(replace filename ".sx" "")
|
||||
"."
|
||||
(get d "name")
|
||||
")))")
|
||||
:sx-target "#sx-content"
|
||||
:sx-select "#sx-content"
|
||||
:sx-swap "innerHTML"
|
||||
:sx-push-url "true"
|
||||
(span
|
||||
:class "font-mono text-sm font-medium text-stone-800 group-hover:text-violet-700"
|
||||
(get d "name"))
|
||||
(span :class "text-xs text-stone-400" (get d "kind"))))
|
||||
|
||||
(defcomp
|
||||
~specs-explorer/spec-explorer-define-detail
|
||||
(&key d filename)
|
||||
:affinity :server
|
||||
(div
|
||||
:class "max-w-3xl mx-auto"
|
||||
(div
|
||||
:class "flex items-center gap-2 flex-wrap"
|
||||
(span :class "font-mono font-semibold text-stone-800" (get d "name"))
|
||||
(span :class "text-xs text-stone-400" (get d "kind"))
|
||||
(if
|
||||
(empty? (get d "effects"))
|
||||
:class "mb-4"
|
||||
(a
|
||||
:class "text-sm text-violet-600 hover:text-violet-800"
|
||||
:href (str
|
||||
"/sx/(language.(spec.(explore."
|
||||
(replace filename ".sx" "")
|
||||
")))")
|
||||
:sx-get (str
|
||||
"/sx/(language.(spec.(explore."
|
||||
(replace filename ".sx" "")
|
||||
")))")
|
||||
:sx-target "#sx-content"
|
||||
:sx-select "#sx-content"
|
||||
:sx-swap "innerHTML"
|
||||
:sx-push-url "true"
|
||||
(str "← Back to " (replace filename ".sx" ""))))
|
||||
(div
|
||||
:class "rounded border border-violet-200 bg-violet-50/30 p-4"
|
||||
(div
|
||||
:class "flex items-center gap-2 flex-wrap mb-3"
|
||||
(span
|
||||
:class "text-xs px-1.5 py-0.5 rounded bg-green-100 text-green-700"
|
||||
"pure")
|
||||
(map
|
||||
(fn (eff) (~specs-explorer/spec-effect-badge :effect eff))
|
||||
(get d "effects"))))
|
||||
(when
|
||||
(not (empty? (get d "params")))
|
||||
(~specs-explorer/spec-param-list :params (get d "params")))
|
||||
(~specs-explorer/spec-ring-translations
|
||||
:source (get d "source")
|
||||
:python (get d "python")
|
||||
:javascript (get d "javascript")
|
||||
:z3 (get d "z3"))
|
||||
(when
|
||||
(not (empty? (get d "refs")))
|
||||
(~specs-explorer/spec-ring-bridge :refs (get d "refs")))
|
||||
(when
|
||||
(> (get d "test-count") 0)
|
||||
(~specs-explorer/spec-ring-runtime
|
||||
:tests (get d "tests")
|
||||
:test-count (get d "test-count")))))
|
||||
:class "font-mono text-lg font-semibold text-stone-800"
|
||||
(get d "name"))
|
||||
(span :class "text-xs text-stone-400" (get d "kind"))
|
||||
(if
|
||||
(empty? (get d "effects"))
|
||||
(span
|
||||
:class "text-xs px-1.5 py-0.5 rounded bg-green-100 text-green-700"
|
||||
"pure")
|
||||
(map
|
||||
(fn (eff) (~specs-explorer/spec-effect-badge :effect eff))
|
||||
(get d "effects"))))
|
||||
(when
|
||||
(not (empty? (get d "params")))
|
||||
(~specs-explorer/spec-param-list :params (get d "params")))
|
||||
(details
|
||||
:open "true"
|
||||
(summary
|
||||
:class "px-3 py-1.5 bg-stone-50 text-xs font-medium text-stone-600 cursor-pointer select-none mt-3 rounded"
|
||||
"SX Source")
|
||||
(pre
|
||||
:class "text-xs p-3 overflow-x-auto bg-white rounded mt-1 border border-stone-200"
|
||||
(code :class "language-sx" (get d "source")))))))
|
||||
|
||||
(defcomp
|
||||
~specs-explorer/spec-effect-badge
|
||||
@@ -178,13 +221,12 @@
|
||||
(div
|
||||
:class "mt-3 border border-stone-200 rounded-lg overflow-hidden"
|
||||
(details
|
||||
:open "true"
|
||||
(summary
|
||||
:class "px-3 py-1.5 bg-stone-50 text-xs font-medium text-stone-600 cursor-pointer"
|
||||
"SX")
|
||||
:class "px-3 py-1.5 bg-stone-50 text-xs font-medium text-stone-600 cursor-pointer select-none"
|
||||
"SX Source")
|
||||
(pre
|
||||
:class "text-xs p-3 overflow-x-auto bg-white"
|
||||
(code (highlight source "sx"))))
|
||||
(code :class "language-sx" source)))
|
||||
(when
|
||||
python
|
||||
(details
|
||||
|
||||
74
tests/playwright/spec-explorer.spec.js
Normal file
74
tests/playwright/spec-explorer.spec.js
Normal file
@@ -0,0 +1,74 @@
|
||||
const { test, expect } = require('playwright/test');
|
||||
const { BASE_URL, trackErrors } = require('./helpers');
|
||||
|
||||
const LOAD_TIMEOUT = 30000;
|
||||
|
||||
async function loadExplorer(page, path) {
|
||||
await page.goto(BASE_URL + '/sx/' + path, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: LOAD_TIMEOUT,
|
||||
});
|
||||
await page.waitForSelector('h1, .sx-render-error, [data-sx-boundary]', {
|
||||
timeout: LOAD_TIMEOUT,
|
||||
});
|
||||
}
|
||||
|
||||
test.describe('Spec Explorer', () => {
|
||||
test('overview loads with evaluator definitions', async ({ page }) => {
|
||||
await loadExplorer(page, '(language.(spec.(explore.evaluator)))');
|
||||
|
||||
await expect(page.locator('h1')).toHaveText('Evaluator');
|
||||
await expect(page.locator('text=141 defines')).toBeVisible();
|
||||
await expect(page.locator('text=2501 lines')).toBeVisible();
|
||||
await expect(page.locator('h2:has-text("Definitions")')).toBeVisible();
|
||||
await expect(page.locator('#fn-make-cek-state')).toBeVisible();
|
||||
await expect(page.locator('#fn-cek-step')).toBeVisible();
|
||||
// eval-expr has 2 entries (forward decl + real), just check at least 1
|
||||
await expect(page.locator('#fn-eval-expr').first()).toBeVisible();
|
||||
await expect(page.locator('.sx-render-error')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('no errors on initial load', async ({ page }) => {
|
||||
const tracker = trackErrors(page);
|
||||
await loadExplorer(page, '(language.(spec.(explore.evaluator)))');
|
||||
await expect(page.locator('h1')).toHaveText('Evaluator');
|
||||
expect(tracker.errors()).toEqual([]);
|
||||
});
|
||||
|
||||
test('SPA nav to explorer has no render error', async ({ page }) => {
|
||||
await page.goto(BASE_URL + '/sx/(language.(spec))', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: LOAD_TIMEOUT,
|
||||
});
|
||||
await page.waitForSelector('h1, h2', { timeout: LOAD_TIMEOUT });
|
||||
|
||||
const link = page.locator('a[href*="explore.evaluator"]');
|
||||
if (await link.count() > 0) {
|
||||
await link.click();
|
||||
await page.waitForSelector('#fn-cek-step', { timeout: LOAD_TIMEOUT });
|
||||
await expect(page.locator('h1')).toHaveText('Evaluator');
|
||||
|
||||
const content = await page.locator('#sx-content').textContent();
|
||||
expect(content).not.toContain('Render error');
|
||||
expect(content).not.toContain('Not callable');
|
||||
}
|
||||
});
|
||||
|
||||
test('drill-in shows definition detail', async ({ page }) => {
|
||||
await loadExplorer(page, '(language.(spec.(explore.evaluator.cek-step)))');
|
||||
|
||||
await expect(page.locator('text=cek-step').first()).toBeVisible();
|
||||
await expect(page.locator('text=function').first()).toBeVisible();
|
||||
await expect(page.locator('a:has-text("Back to evaluator")')).toBeVisible();
|
||||
await expect(page.locator('text=define cek-step')).toBeVisible();
|
||||
});
|
||||
|
||||
test('drill-in shows params and pure badge', async ({ page }) => {
|
||||
await loadExplorer(page, '(language.(spec.(explore.evaluator.make-cek-state)))');
|
||||
|
||||
await expect(page.locator('text=control').first()).toBeVisible();
|
||||
await expect(page.locator('text=env').first()).toBeVisible();
|
||||
await expect(page.locator('text=kont').first()).toBeVisible();
|
||||
await expect(page.locator('text=pure').first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -437,17 +437,9 @@
|
||||
(let
|
||||
((f (trampoline (eval-expr (first args) env)))
|
||||
(coll (trampoline (eval-expr (nth args 1) env))))
|
||||
(map
|
||||
(fn
|
||||
(item)
|
||||
(if
|
||||
(lambda? f)
|
||||
(let
|
||||
((local (env-merge (lambda-closure f) env)))
|
||||
(env-bind! local (first (lambda-params f)) item)
|
||||
(aser (lambda-body f) local))
|
||||
(cek-call f (list item))))
|
||||
coll))
|
||||
(let
|
||||
((results (map (fn (item) (if (lambda? f) (let ((local (env-extend (lambda-closure f)))) (env-bind! local (first (lambda-params f)) item) (aser (lambda-body f) local)) (cek-call f (list item)))) coll)))
|
||||
(aser-fragment results env)))
|
||||
(= name "map-indexed")
|
||||
(let
|
||||
((f (trampoline (eval-expr (first args) env)))
|
||||
|
||||
Reference in New Issue
Block a user