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:
2026-04-01 23:46:13 +00:00
parent b3718c06d0
commit 6e885f49b6
9 changed files with 447 additions and 217 deletions

View File

@@ -65,6 +65,8 @@ let rec to_string = function
| Symbol s -> s | Symbol s -> s
| Keyword k -> k | Keyword k -> k
| Thunk _ as t -> to_string (!trampoline_hook t) | Thunk _ as t -> to_string (!trampoline_hook t)
| SxExpr s -> s
| RawHTML s -> s
| v -> inspect v | v -> inspect v
let () = let () =
@@ -126,10 +128,23 @@ let () =
Number (Float.max lo (Float.min hi x)) Number (Float.max lo (Float.min hi x))
| _ -> raise (Eval_error "clamp: 3 args")); | _ -> raise (Eval_error "clamp: 3 args"));
register "parse-int" (fun 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 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] -> | [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)) | [Number n] | [Number n; _] -> Number (float_of_int (int_of_float n))
| [_; default_val] -> default_val | [_; default_val] -> default_val
| _ -> Nil); | _ -> Nil);
@@ -276,7 +291,17 @@ let () =
else if String.sub haystack i nl = needle then Number (float_of_int i) else if String.sub haystack i nl = needle then Number (float_of_int i)
else find (i + 1) else find (i + 1)
in find 0 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 -> register "substring" (fun args ->
match args with match args with
| [String s; Number start; Number end_] -> | [String s; Number start; Number end_] ->
@@ -655,6 +680,8 @@ let () =
match args with [a] -> String (inspect a) | _ -> raise (Eval_error "inspect: 1 arg")); match args with [a] -> String (inspect a) | _ -> raise (Eval_error "inspect: 1 arg"));
register "serialize" (fun args -> register "serialize" (fun args ->
match args with match args with
| [SxExpr s] -> String s
| [RawHTML s] -> String s
| [a] -> String (inspect a) (* used for dedup keys in compiler *) | [a] -> String (inspect a) (* used for dedup keys in compiler *)
| _ -> raise (Eval_error "serialize: 1 arg")); | _ -> raise (Eval_error "serialize: 1 arg"));
register "make-symbol" (fun args -> register "make-symbol" (fun args ->

View File

@@ -201,6 +201,7 @@ let rec do_render_to_html (expr : value) (env : env) : string =
| String s -> escape_html s | String s -> escape_html s
| Keyword k -> escape_html k | Keyword k -> escape_html k
| RawHTML s -> s | RawHTML s -> s
| SxExpr s -> s
| Symbol s -> | Symbol s ->
let v = Sx_ref.eval_expr (Symbol s) (Env env) in let v = Sx_ref.eval_expr (Symbol s) (Env env) in
do_render_to_html v env 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 let result = Sx_ref.eval_expr (List (head :: args)) (Env env) in
do_render_to_html result env) 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 let result = Sx_ref.eval_expr (List (head :: args)) (Env env) in
do_render_to_html result env 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 | String s -> escape_html_buf buf s
| Keyword k -> escape_html_buf buf k | Keyword k -> escape_html_buf buf k
| RawHTML s -> Buffer.add_string buf s | RawHTML s -> Buffer.add_string buf s
| SxExpr s -> Buffer.add_string buf s
| Symbol s -> | Symbol s ->
let v = Sx_ref.eval_expr (Symbol s) (Env env) in let v = Sx_ref.eval_expr (Symbol s) (Env env) in
render_to_buf buf v env render_to_buf buf v env

View File

@@ -531,9 +531,9 @@ let rec inspect = function
| Continuation (_, _) -> "<continuation>" | Continuation (_, _) -> "<continuation>"
| NativeFn (name, _) -> Printf.sprintf "<native:%s>" name | NativeFn (name, _) -> Printf.sprintf "<native:%s>" name
| Signal _ -> "<signal>" | 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>" | 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>" | Env _ -> "<env>"
| CekState _ -> "<cek-state>" | CekState _ -> "<cek-state>"
| CekFrame f -> Printf.sprintf "<frame:%s>" f.cf_type | CekFrame f -> Printf.sprintf "<frame:%s>" f.cf_type

View 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)))))))

View File

@@ -213,10 +213,12 @@
spec spec
(fn (fn
(slug) (slug)
(if (cond
(nil? slug) (nil? slug)
(quote (~specs/architecture-content)) (quote (~specs/architecture-content))
(case (not (= (type-of slug) "string"))
slug
:else (case
slug slug
"core" "core"
(let (let
@@ -278,22 +280,34 @@
(define (define
explore explore
(fn (fn
(slug) (slug defname)
(if (if
(nil? slug) (nil? slug)
(quote (~specs/architecture-content)) (quote (~specs/architecture-content))
(let (let
((found-spec (find-spec slug))) ((found-spec (find-spec slug)))
(if (if
found-spec (not found-spec)
(let (quasiquote (~specs/not-found :slug (unquote slug)))
((data (spec-explore (get found-spec "filename") (get found-spec "title") (get found-spec "desc")))) (if
(if defname
data (let
(quasiquote ((d (spec-explore-define (get found-spec "filename") defname)))
(~specs-explorer/spec-explorer-content :data (unquote data))) (if
(quasiquote (~specs/not-found :slug (unquote slug))))) d
(quasiquote (~specs/not-found :slug (unquote slug)))))))) (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 (define
make-spec-files make-spec-files

View File

@@ -1,186 +1,238 @@
;; --------------------------------------------------------------------------- (define
;; Spec Introspection — SX macro that reads a spec file and produces spec-form-name
;; structured explorer data. The spec examines itself. (fn
;; --------------------------------------------------------------------------- (form)
;; (if
;; Usage: (spec-explore "evaluator.sx" "Evaluator" "CEK machine evaluator") (< (len form) 2)
;; nil
;; Returns a dict with :title, :filename, :desc, :sections, :stats (let
;; suitable for (~specs-explorer/spec-explorer-content :data result) ((head (symbol-name (first form))) (name-part (nth form 1)))
;; 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 (cond
(= head "define") (= head "define")
(if (= (type-of name-part) "symbol") (if (= (type-of name-part) "symbol") (symbol-name name-part) nil)
(symbol-name name-part)
nil)
(or (= head "defcomp") (= head "defisland") (= head "defmacro")) (or (= head "defcomp") (= head "defisland") (= head "defmacro"))
(if (= (type-of name-part) "symbol") (if (= (type-of name-part) "symbol") (symbol-name name-part) nil)
(symbol-name name-part)
nil)
:else nil))))) :else nil)))))
;; Extract effects annotation from a define form (define
;; (define name :effects [mutation io] (fn ...)) spec-form-effects
(define spec-form-effects (fn
(fn (form) (form)
(let ((result (list)) (let
(found false)) ((result (list)) (found false))
(when (> (len form) 3) (when
(> (len form) 3)
(for-each (for-each
(fn (item) (fn
(if found (item)
(when (and (list? item) (empty? result)) (if
found
(when
(and (list? item) (empty? result))
(for-each (for-each
(fn (eff) (fn
(append! result (if (= (type-of eff) "symbol") (eff)
(symbol-name eff) (append!
(str eff)))) result
(if
(= (type-of eff) "symbol")
(symbol-name eff)
(str eff))))
item) item)
(set! found false)) (set! found false))
(when (and (= (type-of item) "keyword") (when
(= (keyword-name item) "effects")) (and
(= (type-of item) "keyword")
(= (keyword-name item) "effects"))
(set! found true)))) (set! found true))))
(slice form 2))) (slice form 2)))
result))) result)))
;; Extract params from a fn/lambda body within a define (define
(define spec-form-params spec-form-params
(fn (form) (fn
(if (< (len form) 2) (list) (form)
(let ((body (nth form (- (len form) 1)))) (if
(if (and (list? body) (> (len body) 1) (< (len form) 2)
(= (type-of (first body)) "symbol") (list)
(or (= (symbol-name (first body)) "fn") (let
(= (symbol-name (first body)) "lambda"))) ((body (nth form (- (len form) 1))))
;; (fn (params...) body) (if
(let ((raw-params (nth body 1))) (and
(if (list? raw-params) (list? body)
(map (fn (p) (> (len body) 1)
(cond (= (type-of (first body)) "symbol")
(= (type-of p) "symbol") (or
{:name (symbol-name p) :type nil} (= (symbol-name (first body)) "fn")
(and (list? p) (>= (len p) 3) (= (symbol-name (first body)) "lambda")))
(= (type-of (first p)) "symbol")) (let
{:name (symbol-name (first p)) ((raw-params (nth body 1)))
:type (if (and (>= (len p) 3) (if
(= (type-of (nth p 2)) "symbol")) (list? raw-params)
(symbol-name (nth p 2)) (map
nil)} (fn
:else {:name (str p) :type nil})) (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) raw-params)
(list))) (list)))
(list)))))) (list))))))
;; Classify a form: "function", "constant", "component", "macro", "island" (define
(define spec-form-kind spec-form-kind
(fn (form) (fn
(let ((head (symbol-name (first form)))) (form)
(let
((head (symbol-name (first form))))
(cond (cond
(= head "defcomp") "component" (= head "defcomp")
(= head "defisland") "island" "component"
(= head "defmacro") "macro" (= head "defisland")
"island"
(= head "defmacro")
"macro"
(= head "define") (= head "define")
(let ((body (last form))) (let
(if (and (list? body) (> (len body) 0) ((body (last form)))
(= (type-of (first body)) "symbol") (if
(or (= (symbol-name (first body)) "fn") (and
(= (symbol-name (first body)) "lambda"))) (list? body)
"function" (> (len body) 0)
"constant")) (= (type-of (first body)) "symbol")
(or
(= (symbol-name (first body)) "fn")
(= (symbol-name (first body)) "lambda")))
"function"
"constant"))
:else "unknown")))) :else "unknown"))))
;; Serialize a form back to SX source for display (define
(define spec-form-source spec-form-signature
(fn (form) (fn
(serialize form))) (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 (define
;; Returns list of {:title :comment :defines} spec-group-sections
(define spec-group-sections (fn
(fn (forms source) (forms source)
(let ((sections (list)) (let
(current-title "Definitions") ((sections (list))
(current-comment nil) (current-title "Definitions")
(current-defines (list))) (current-comment nil)
;; Extract section comments from source (current-defines (list)))
;; Look for ";; section-name" or ";; --- section ---" patterns
(for-each (for-each
(fn (form) (fn
(when (and (list? form) (> (len form) 1)) (form)
(let ((name (spec-form-name form))) (when
(when name (and (list? form) (> (len form) 1))
(append! current-defines (let
{:name name ((name (spec-form-name form)))
:kind (spec-form-kind form) (when name (append! current-defines {:kind (spec-form-kind form) :name name})))))
: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) forms)
;; Flush last section (when (not (empty? current-defines)) (append! sections {:defines current-defines :title current-title :comment current-comment}))
(when (not (empty? current-defines))
(append! sections
{:title current-title
:comment current-comment
:defines current-defines}))
sections))) sections)))
;; Compute stats from sections (define
(define spec-compute-stats spec-compute-stats
(fn (sections source) (fn
(let ((total 0) (sections source)
(pure 0) (let
(mutation 0) ((total 0)
(io 0) (pure 0)
(render 0) (mutation 0)
(lines (len (split source "\n")))) (io 0)
(render 0)
(lines (len (split source "\n"))))
(for-each (for-each
(fn (section) (fn
(section)
(for-each (for-each
(fn (d) (fn
(d)
(set! total (inc total)) (set! total (inc total))
(if (empty? (get d "effects")) (if
(empty? (get d "effects"))
(set! pure (inc pure)) (set! pure (inc pure))
(for-each (for-each
(fn (eff) (fn
(eff)
(cond (cond
(= eff "mutation") (set! mutation (inc mutation)) (= eff "mutation")
(= eff "io") (set! io (inc io)) (set! mutation (inc mutation))
(= eff "render") (set! render (inc render)))) (= eff "io")
(set! io (inc io))
(= eff "render")
(set! render (inc render))))
(get d "effects")))) (get d "effects"))))
(get section "defines"))) (get section "defines")))
sections) sections)
{:total-defines total {:lines lines :io-count io :render-count render :pure-count pure :mutation-count mutation :test-total 0 :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
(define spec-explore :effects [io] spec-explore-define
(fn (filename title desc) :effects (io)
(let ((source (helper "read-spec-file" filename))) (fn
(if (starts-with? source ";; spec file not found") (filename def-name)
(let
((source (helper "read-spec-file" filename)))
(if
(starts-with? source ";; spec file not found")
nil nil
(let ((forms (sx-parse source)) (let
(sections (spec-group-sections forms source)) ((forms (sx-parse source)) (found nil))
(stats (spec-compute-stats sections source))) (for-each
{:filename filename (fn
:title title (form)
:desc desc (when
:sections sections (and (not found) (list? form) (> (len form) 1))
:stats stats (let
:platform-interface (list)}))))) ((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})))))

View File

@@ -11,7 +11,11 @@
:slug (replace (get data "filename") ".sx" "")) :slug (replace (get data "filename") ".sx" ""))
(~specs-explorer/spec-explorer-stats :stats (get data "stats")) (~specs-explorer/spec-explorer-stats :stats (get data "stats"))
(map (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")) (get data "sections"))
(when (when
(not (empty? (get data "platform-interface"))) (not (empty? (get data "platform-interface")))
@@ -78,56 +82,95 @@
(defcomp (defcomp
~specs-explorer/spec-explorer-section ~specs-explorer/spec-explorer-section
(&key section) (&key section filename)
(div (div
:class "mb-8" :class "mb-6"
(h2 (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")) " " "-") :id (replace (lower (get section "title")) " " "-")
(get section "title")) (get section "title"))
(when (when
(get section "comment") (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 (div
:class "space-y-4" :class "space-y-0.5"
(map (map
(fn (d) (~specs-explorer/spec-explorer-define :d d)) (fn
(d)
(~specs-explorer/spec-explorer-define :d d :filename filename))
(get section "defines"))))) (get section "defines")))))
(defcomp (defcomp
~specs-explorer/spec-explorer-define ~specs-explorer/spec-explorer-define
(&key d) (&key d filename)
(div (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")) :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 (div
:class "flex items-center gap-2 flex-wrap" :class "mb-4"
(span :class "font-mono font-semibold text-stone-800" (get d "name")) (a
(span :class "text-xs text-stone-400" (get d "kind")) :class "text-sm text-violet-600 hover:text-violet-800"
(if :href (str
(empty? (get d "effects")) "/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 (span
:class "text-xs px-1.5 py-0.5 rounded bg-green-100 text-green-700" :class "font-mono text-lg font-semibold text-stone-800"
"pure") (get d "name"))
(map (span :class "text-xs text-stone-400" (get d "kind"))
(fn (eff) (~specs-explorer/spec-effect-badge :effect eff)) (if
(get d "effects")))) (empty? (get d "effects"))
(when (span
(not (empty? (get d "params"))) :class "text-xs px-1.5 py-0.5 rounded bg-green-100 text-green-700"
(~specs-explorer/spec-param-list :params (get d "params"))) "pure")
(~specs-explorer/spec-ring-translations (map
:source (get d "source") (fn (eff) (~specs-explorer/spec-effect-badge :effect eff))
:python (get d "python") (get d "effects"))))
:javascript (get d "javascript") (when
:z3 (get d "z3")) (not (empty? (get d "params")))
(when (~specs-explorer/spec-param-list :params (get d "params")))
(not (empty? (get d "refs"))) (details
(~specs-explorer/spec-ring-bridge :refs (get d "refs"))) :open "true"
(when (summary
(> (get d "test-count") 0) :class "px-3 py-1.5 bg-stone-50 text-xs font-medium text-stone-600 cursor-pointer select-none mt-3 rounded"
(~specs-explorer/spec-ring-runtime "SX Source")
:tests (get d "tests") (pre
:test-count (get d "test-count"))))) :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 (defcomp
~specs-explorer/spec-effect-badge ~specs-explorer/spec-effect-badge
@@ -178,13 +221,12 @@
(div (div
:class "mt-3 border border-stone-200 rounded-lg overflow-hidden" :class "mt-3 border border-stone-200 rounded-lg overflow-hidden"
(details (details
:open "true"
(summary (summary
:class "px-3 py-1.5 bg-stone-50 text-xs font-medium text-stone-600 cursor-pointer" :class "px-3 py-1.5 bg-stone-50 text-xs font-medium text-stone-600 cursor-pointer select-none"
"SX") "SX Source")
(pre (pre
:class "text-xs p-3 overflow-x-auto bg-white" :class "text-xs p-3 overflow-x-auto bg-white"
(code (highlight source "sx")))) (code :class "language-sx" source)))
(when (when
python python
(details (details

View 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();
});
});

View File

@@ -437,17 +437,9 @@
(let (let
((f (trampoline (eval-expr (first args) env))) ((f (trampoline (eval-expr (first args) env)))
(coll (trampoline (eval-expr (nth args 1) env)))) (coll (trampoline (eval-expr (nth args 1) env))))
(map (let
(fn ((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)))
(item) (aser-fragment results env)))
(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))
(= name "map-indexed") (= name "map-indexed")
(let (let
((f (trampoline (eval-expr (first args) env))) ((f (trampoline (eval-expr (first args) env)))