Compare commits
3 Commits
83dbb5958a
...
loops/ruby
| Author | SHA1 | Date | |
|---|---|---|---|
| fa3274c394 | |||
| 15eb133311 | |||
| 96019e9fe8 |
@@ -688,11 +688,6 @@ let setup_evaluator_bridge env =
|
||||
| [expr; e] -> Sx_ref.eval_expr expr (Env (Sx_runtime.unwrap_env e))
|
||||
| [expr] -> Sx_ref.eval_expr expr (Env env)
|
||||
| _ -> raise (Eval_error "eval-expr: expected (expr env?)"));
|
||||
(* eval-in-env: (env expr) → result. Evaluates expr in the given env. *)
|
||||
Sx_primitives.register "eval-in-env" (fun args ->
|
||||
match args with
|
||||
| [e; expr] -> Sx_ref.eval_expr expr e
|
||||
| _ -> raise (Eval_error "eval-in-env: (env expr)"));
|
||||
bind "trampoline" (fun args ->
|
||||
match args with
|
||||
| [v] ->
|
||||
@@ -754,13 +749,7 @@ let setup_evaluator_bridge env =
|
||||
| _ -> raise (Eval_error "register-special-form!: expected (name handler)"));
|
||||
ignore (env_bind env "*custom-special-forms*" Sx_ref.custom_special_forms);
|
||||
ignore (Sx_ref.register_special_form (String "<>") (NativeFn ("<>", fun args ->
|
||||
List (List.map (fun a -> Sx_ref.eval_expr a (Env env)) args))));
|
||||
(* current-env: special form — returns current lexical env as a first-class value *)
|
||||
ignore (Sx_ref.register_special_form (String "current-env")
|
||||
(NativeFn ("current-env", fun args ->
|
||||
match args with
|
||||
| [_arg_list; env_val] -> env_val
|
||||
| _ -> Nil)))
|
||||
List (List.map (fun a -> Sx_ref.eval_expr a (Env env)) args))))
|
||||
|
||||
(* ---- Type predicates and introspection ---- *)
|
||||
let setup_introspection env =
|
||||
@@ -946,24 +935,7 @@ let setup_env_operations env =
|
||||
bind "env-has?" (fun args -> match args with [e; String k] -> Bool (Sx_types.env_has (uw e) k) | [e; Keyword k] -> Bool (Sx_types.env_has (uw e) k) | _ -> raise (Eval_error "env-has?: expected env and string"));
|
||||
bind "env-bind!" (fun args -> match args with [e; String k; v] -> Sx_types.env_bind (uw e) k v | [e; Keyword k; v] -> Sx_types.env_bind (uw e) k v | _ -> raise (Eval_error "env-bind!: expected env, key, value"));
|
||||
bind "env-set!" (fun args -> match args with [e; String k; v] -> Sx_types.env_set (uw e) k v | [e; Keyword k; v] -> Sx_types.env_set (uw e) k v | _ -> raise (Eval_error "env-set!: expected env, key, value"));
|
||||
bind "env-extend" (fun args ->
|
||||
match args with
|
||||
| e :: pairs ->
|
||||
let child = Sx_types.env_extend (uw e) in
|
||||
let rec go = function
|
||||
| [] -> ()
|
||||
| k :: v :: rest ->
|
||||
ignore (Sx_types.env_bind child (Sx_runtime.value_to_str k) v); go rest
|
||||
| [_] -> raise (Eval_error "env-extend: odd number of key-val pairs") in
|
||||
go pairs; Env child
|
||||
| _ -> raise (Eval_error "env-extend: expected env"));
|
||||
bind "env-lookup" (fun args ->
|
||||
match args with
|
||||
| [e; key] ->
|
||||
let k = Sx_runtime.value_to_str key in
|
||||
let raw = uw e in
|
||||
if Sx_types.env_has raw k then Sx_types.env_get raw k else Nil
|
||||
| _ -> raise (Eval_error "env-lookup: (env key)"));
|
||||
bind "env-extend" (fun args -> match args with [e] -> Env (Sx_types.env_extend (uw e)) | _ -> raise (Eval_error "env-extend: expected env"));
|
||||
bind "env-merge" (fun args -> match args with [a; b] -> Sx_runtime.env_merge a b | _ -> raise (Eval_error "env-merge: expected 2 envs"))
|
||||
|
||||
(* ---- Strict mode (gradual type system support) ---- *)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
(library
|
||||
(name sx)
|
||||
(wrapped false)
|
||||
(libraries re re.pcre unix))
|
||||
(libraries re re.pcre))
|
||||
|
||||
@@ -1871,175 +1871,4 @@ let () =
|
||||
| [rx] ->
|
||||
let (_, _, flags) = regex_of_value rx in
|
||||
String flags
|
||||
| _ -> raise (Eval_error "regex-flags: (regex)"));
|
||||
|
||||
(* === File I/O === *)
|
||||
register "file-read" (fun args ->
|
||||
match args with
|
||||
| [String path] ->
|
||||
(try
|
||||
let ic = open_in path in
|
||||
let n = in_channel_length ic in
|
||||
let s = Bytes.create n in
|
||||
really_input ic s 0 n;
|
||||
close_in ic;
|
||||
String (Bytes.to_string s)
|
||||
with Sys_error msg -> raise (Eval_error ("file-read: " ^ msg)))
|
||||
| _ -> raise (Eval_error "file-read: (path)"));
|
||||
|
||||
register "file-write" (fun args ->
|
||||
match args with
|
||||
| [String path; String content] ->
|
||||
(try
|
||||
let oc = open_out path in
|
||||
output_string oc content;
|
||||
close_out oc;
|
||||
Nil
|
||||
with Sys_error msg -> raise (Eval_error ("file-write: " ^ msg)))
|
||||
| _ -> raise (Eval_error "file-write: (path content)"));
|
||||
|
||||
register "file-append" (fun args ->
|
||||
match args with
|
||||
| [String path; String content] ->
|
||||
(try
|
||||
let oc = open_out_gen [Open_append; Open_creat; Open_wronly; Open_text] 0o644 path in
|
||||
output_string oc content;
|
||||
close_out oc;
|
||||
Nil
|
||||
with Sys_error msg -> raise (Eval_error ("file-append: " ^ msg)))
|
||||
| _ -> raise (Eval_error "file-append: (path content)"));
|
||||
|
||||
register "file-exists?" (fun args ->
|
||||
match args with
|
||||
| [String path] -> Bool (Sys.file_exists path)
|
||||
| _ -> raise (Eval_error "file-exists?: (path)"));
|
||||
|
||||
register "file-glob" (fun args ->
|
||||
let glob_match pat str =
|
||||
let pn = String.length pat and sn = String.length str in
|
||||
let rec go pi si =
|
||||
if pi = pn then si = sn
|
||||
else match pat.[pi] with
|
||||
| '*' ->
|
||||
let rec try_from i = i <= sn && (go (pi+1) i || try_from (i+1)) in
|
||||
try_from si
|
||||
| '?' -> si < sn && go (pi+1) (si+1)
|
||||
| '[' ->
|
||||
let pi' = ref (pi+1) in
|
||||
let negate = !pi' < pn && pat.[!pi'] = '^' in
|
||||
if negate then incr pi';
|
||||
let matched = ref false in
|
||||
while !pi' < pn && pat.[!pi'] <> ']' do
|
||||
let c1 = pat.[!pi'] in
|
||||
incr pi';
|
||||
if !pi' + 1 < pn && pat.[!pi'] = '-' then begin
|
||||
let c2 = pat.[!pi' + 1] in
|
||||
pi' := !pi' + 2;
|
||||
if si < sn && str.[si] >= c1 && str.[si] <= c2 then matched := true
|
||||
end else if si < sn && str.[si] = c1 then matched := true
|
||||
done;
|
||||
if !pi' < pn then incr pi';
|
||||
((!matched && not negate) || (not !matched && negate)) && go !pi' (si+1)
|
||||
| c -> si < sn && str.[si] = c && go (pi+1) (si+1)
|
||||
in go 0 0
|
||||
in
|
||||
let glob_paths pat =
|
||||
let dir = Filename.dirname pat in
|
||||
let base_pat = Filename.basename pat in
|
||||
let dir' = if dir = "." && not (String.length pat > 1 && pat.[0] = '.') then "." else dir in
|
||||
(try
|
||||
let entries = Sys.readdir dir' in
|
||||
Array.fold_left (fun acc entry ->
|
||||
if glob_match base_pat entry then
|
||||
let full = if dir' = "." then entry else Filename.concat dir' entry in
|
||||
full :: acc
|
||||
else acc
|
||||
) [] entries
|
||||
|> List.sort String.compare
|
||||
with Sys_error _ -> [])
|
||||
in
|
||||
match args with
|
||||
| [String pat] -> List (List.map (fun s -> String s) (glob_paths pat))
|
||||
| _ -> raise (Eval_error "file-glob: (pattern)"));
|
||||
|
||||
(* === Clock === *)
|
||||
register "clock-seconds" (fun args ->
|
||||
match args with
|
||||
| [] -> Number (Float.round (Unix.gettimeofday ()))
|
||||
| _ -> raise (Eval_error "clock-seconds: no args"));
|
||||
|
||||
register "clock-milliseconds" (fun args ->
|
||||
match args with
|
||||
| [] -> Number (Float.round (Unix.gettimeofday () *. 1000.0))
|
||||
| _ -> raise (Eval_error "clock-milliseconds: no args"));
|
||||
|
||||
register "clock-format" (fun args ->
|
||||
match args with
|
||||
| [Number t_f] | [Number t_f; String _] ->
|
||||
let t = int_of_float t_f in
|
||||
let fmt = (match args with [_; String f] -> f | _ -> "%a %b %e %H:%M:%S %Z %Y") in
|
||||
let tm = Unix.gmtime (float_of_int t) in
|
||||
let buf = Buffer.create 32 in
|
||||
let n = String.length fmt in
|
||||
let i = ref 0 in
|
||||
while !i < n do
|
||||
if fmt.[!i] = '%' && !i + 1 < n then begin
|
||||
(match fmt.[!i + 1] with
|
||||
| 'Y' -> Buffer.add_string buf (Printf.sprintf "%04d" (1900 + tm.Unix.tm_year))
|
||||
| 'm' -> Buffer.add_string buf (Printf.sprintf "%02d" (tm.Unix.tm_mon + 1))
|
||||
| 'd' -> Buffer.add_string buf (Printf.sprintf "%02d" tm.Unix.tm_mday)
|
||||
| 'e' -> Buffer.add_string buf (Printf.sprintf "%2d" tm.Unix.tm_mday)
|
||||
| 'H' -> Buffer.add_string buf (Printf.sprintf "%02d" tm.Unix.tm_hour)
|
||||
| 'M' -> Buffer.add_string buf (Printf.sprintf "%02d" tm.Unix.tm_min)
|
||||
| 'S' -> Buffer.add_string buf (Printf.sprintf "%02d" tm.Unix.tm_sec)
|
||||
| 'j' -> Buffer.add_string buf (Printf.sprintf "%03d" (tm.Unix.tm_yday + 1))
|
||||
| 'Z' -> Buffer.add_string buf "UTC"
|
||||
| 'a' -> let days = [|"Sun";"Mon";"Tue";"Wed";"Thu";"Fri";"Sat"|] in
|
||||
Buffer.add_string buf days.(tm.Unix.tm_wday)
|
||||
| 'A' -> let days = [|"Sunday";"Monday";"Tuesday";"Wednesday";"Thursday";"Friday";"Saturday"|] in
|
||||
Buffer.add_string buf days.(tm.Unix.tm_wday)
|
||||
| 'b' | 'h' -> let mons = [|"Jan";"Feb";"Mar";"Apr";"May";"Jun";"Jul";"Aug";"Sep";"Oct";"Nov";"Dec"|] in
|
||||
Buffer.add_string buf mons.(tm.Unix.tm_mon)
|
||||
| 'B' -> let mons = [|"January";"February";"March";"April";"May";"June";"July";"August";"September";"October";"November";"December"|] in
|
||||
Buffer.add_string buf mons.(tm.Unix.tm_mon)
|
||||
| c -> Buffer.add_char buf '%'; Buffer.add_char buf c);
|
||||
i := !i + 2
|
||||
end else begin
|
||||
Buffer.add_char buf fmt.[!i];
|
||||
incr i
|
||||
end
|
||||
done;
|
||||
String (Buffer.contents buf)
|
||||
| _ -> raise (Eval_error "clock-format: (seconds [format])"));
|
||||
|
||||
(* === Env-as-value (Phase 4) === *)
|
||||
|
||||
(* env-lookup: (env key) → value or nil. Works on Env, Dict, or Nil. *)
|
||||
register "env-lookup" (fun args ->
|
||||
let unwrap = function
|
||||
| Env e -> e
|
||||
| Nil -> make_env ()
|
||||
| _ -> raise (Eval_error "env-lookup: first arg must be an environment") in
|
||||
match args with
|
||||
| [env_val; key] ->
|
||||
let e = unwrap env_val in
|
||||
let k = value_to_string key in
|
||||
if env_has e k then env_get e k else Nil
|
||||
| _ -> raise (Eval_error "env-lookup: (env key)"));
|
||||
|
||||
(* env-extend: (env [key val ...]) → new child env with optional bindings. *)
|
||||
register "env-extend" (fun args ->
|
||||
match args with
|
||||
| [] -> raise (Eval_error "env-extend: requires at least one arg")
|
||||
| env_val :: pairs ->
|
||||
let parent_env = match env_val with
|
||||
| Env e -> e
|
||||
| Nil -> make_env ()
|
||||
| _ -> raise (Eval_error "env-extend: first arg must be an environment") in
|
||||
let child = env_extend parent_env in
|
||||
let rec add_bindings = function
|
||||
| [] -> ()
|
||||
| k :: v :: rest -> ignore (env_bind child (value_to_string k) v); add_bindings rest
|
||||
| [_] -> raise (Eval_error "env-extend: odd number of key-val pairs") in
|
||||
add_bindings pairs;
|
||||
Env child)
|
||||
| _ -> raise (Eval_error "regex-flags: (regex)"))
|
||||
|
||||
@@ -529,4 +529,3 @@ let jit_try_call f args =
|
||||
(match hook f arg_list with Some result -> incr _jit_hit; result | None -> incr _jit_miss; _jit_skip_sentinel)
|
||||
| _ -> incr _jit_skip; _jit_skip_sentinel
|
||||
|
||||
|
||||
|
||||
44
lib/fiber.sx
44
lib/fiber.sx
@@ -1,44 +0,0 @@
|
||||
; lib/fiber.sx — pure SX fiber library using call/cc
|
||||
;
|
||||
; A fiber is a cooperative coroutine with true suspension (no eager
|
||||
; pre-execution). Each fiber is a dict {:resume fn :done? fn}.
|
||||
;
|
||||
; make-fiber body → fiber dict
|
||||
; body = (fn (yield init-val) ...) — body receives yield + first resume val
|
||||
; yield = (fn (val) ...) — suspends fiber, returns val to resumer
|
||||
;
|
||||
; fiber-resume f v → next yielded value, or nil when body returns
|
||||
; fiber-done? f → true after body has returned
|
||||
|
||||
(define make-fiber
|
||||
(fn (body)
|
||||
(let
|
||||
((resume-k nil)
|
||||
(caller-k nil)
|
||||
(done false))
|
||||
(let
|
||||
((yield
|
||||
(fn (val)
|
||||
(call/cc
|
||||
(fn (k)
|
||||
(set! resume-k k)
|
||||
(caller-k val))))))
|
||||
{:resume
|
||||
(fn (val)
|
||||
(if
|
||||
done
|
||||
nil
|
||||
(call/cc
|
||||
(fn (k)
|
||||
(set! caller-k k)
|
||||
(if
|
||||
(nil? resume-k)
|
||||
(begin
|
||||
(body yield val)
|
||||
(set! done true)
|
||||
(k nil))
|
||||
(resume-k val))))))
|
||||
:done? (fn () done)}))))
|
||||
|
||||
(define fiber-resume (fn (f v) ((get f :resume) v)))
|
||||
(define fiber-done? (fn (f) ((get f :done?))))
|
||||
831
lib/ruby/parser.sx
Normal file
831
lib/ruby/parser.sx
Normal file
@@ -0,0 +1,831 @@
|
||||
;; Ruby parser: token list → AST.
|
||||
;; Entry: (rb-parse tokens) or (rb-parse-str src)
|
||||
;; AST nodes: dicts with :type plus type-specific fields.
|
||||
|
||||
(define rb-parse
|
||||
(fn (tokens)
|
||||
(let ((pos 0) (tok-count (len tokens)))
|
||||
|
||||
(define rb-p-cur
|
||||
(fn () (nth tokens pos)))
|
||||
(define rb-p-peek
|
||||
(fn (n)
|
||||
(if (< (+ pos n) tok-count)
|
||||
(nth tokens (+ pos n))
|
||||
{:type "eof" :value nil :line 0 :col 0})))
|
||||
(define rb-p-advance!
|
||||
(fn () (set! pos (+ pos 1))))
|
||||
(define rb-p-type
|
||||
(fn () (get (rb-p-cur) :type)))
|
||||
(define rb-p-val
|
||||
(fn () (get (rb-p-cur) :value)))
|
||||
(define rb-p-sep?
|
||||
(fn () (or (= (rb-p-type) "newline") (= (rb-p-type) "semi"))))
|
||||
(define rb-p-skip-seps!
|
||||
(fn ()
|
||||
(when (rb-p-sep?)
|
||||
(do (rb-p-advance!) (rb-p-skip-seps!)))))
|
||||
(define rb-p-skip-newlines!
|
||||
(fn ()
|
||||
(when (= (rb-p-type) "newline")
|
||||
(do (rb-p-advance!) (rb-p-skip-newlines!)))))
|
||||
(define rb-p-expect!
|
||||
(fn (type)
|
||||
(if (= (rb-p-type) type)
|
||||
(let ((tok (rb-p-cur)))
|
||||
(rb-p-advance!)
|
||||
tok)
|
||||
{:type "error"
|
||||
:msg (join "" (list "expected " type " got " (rb-p-type)))})))
|
||||
(define rb-p-expect-kw!
|
||||
(fn (kw)
|
||||
(when (and (= (rb-p-type) "keyword") (= (rb-p-val) kw))
|
||||
(rb-p-advance!))))
|
||||
|
||||
;; Block: do |params| body end or { |params| body }
|
||||
(define rb-p-parse-block-params
|
||||
(fn ()
|
||||
(if (= (rb-p-type) "pipe")
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(let ((params (list)))
|
||||
(define rb-p-bp-loop
|
||||
(fn ()
|
||||
(when (not (or (= (rb-p-type) "pipe") (= (rb-p-type) "eof")))
|
||||
(do
|
||||
(cond
|
||||
((and (= (rb-p-type) "op") (= (rb-p-val) "**"))
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(append! params {:type "param-kwrest" :name (rb-p-val)})
|
||||
(rb-p-advance!)))
|
||||
((and (= (rb-p-type) "op") (= (rb-p-val) "*"))
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(if (= (rb-p-type) "ident")
|
||||
(do
|
||||
(append! params {:type "param-rest" :name (rb-p-val)})
|
||||
(rb-p-advance!))
|
||||
(append! params {:type "param-rest" :name nil}))))
|
||||
(:else
|
||||
(do
|
||||
(append! params {:type "param-req" :name (rb-p-val)})
|
||||
(rb-p-advance!))))
|
||||
(when (= (rb-p-type) "comma") (rb-p-advance!))
|
||||
(rb-p-bp-loop)))))
|
||||
(rb-p-bp-loop)
|
||||
(rb-p-expect! "pipe")
|
||||
params))
|
||||
(list))))
|
||||
|
||||
(define rb-p-parse-block
|
||||
(fn ()
|
||||
(cond
|
||||
((and (= (rb-p-type) "keyword") (= (rb-p-val) "do"))
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(let ((params (rb-p-parse-block-params)))
|
||||
(rb-p-skip-seps!)
|
||||
(let ((body (rb-p-parse-stmts (list "end"))))
|
||||
(rb-p-expect-kw! "end")
|
||||
{:type "block" :params params :body body}))))
|
||||
((= (rb-p-type) "lbrace")
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(let ((params (rb-p-parse-block-params)))
|
||||
(rb-p-skip-seps!)
|
||||
(let ((body (rb-p-parse-stmts (list "rbrace"))))
|
||||
(rb-p-expect! "rbrace")
|
||||
{:type "block" :params params :body body}))))
|
||||
(:else nil))))
|
||||
|
||||
;; Method def params
|
||||
(define rb-p-parse-def-params
|
||||
(fn ()
|
||||
(let ((params (list)))
|
||||
(define rb-p-dp-one
|
||||
(fn ()
|
||||
(cond
|
||||
((and (= (rb-p-type) "op") (= (rb-p-val) "&"))
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(append! params {:type "param-block" :name (rb-p-val)})
|
||||
(rb-p-advance!)))
|
||||
((and (= (rb-p-type) "op") (= (rb-p-val) "**"))
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(append! params {:type "param-kwrest" :name (rb-p-val)})
|
||||
(rb-p-advance!)))
|
||||
((and (= (rb-p-type) "op") (= (rb-p-val) "*"))
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(if (= (rb-p-type) "ident")
|
||||
(do
|
||||
(append! params {:type "param-rest" :name (rb-p-val)})
|
||||
(rb-p-advance!))
|
||||
(append! params {:type "param-rest" :name nil}))))
|
||||
((and (= (rb-p-type) "ident")
|
||||
(= (get (rb-p-peek 1) :type) "colon"))
|
||||
(do
|
||||
(let ((name (rb-p-val)))
|
||||
(rb-p-advance!)
|
||||
(rb-p-advance!)
|
||||
(if (or (rb-p-sep?) (= (rb-p-type) "comma")
|
||||
(= (rb-p-type) "rparen") (= (rb-p-type) "eof"))
|
||||
(append! params {:type "param-kw" :name name :default nil})
|
||||
(append! params {:type "param-kw" :name name
|
||||
:default (rb-p-parse-assign)})))))
|
||||
(:else
|
||||
(let ((name (rb-p-val)))
|
||||
(rb-p-advance!)
|
||||
(if (and (= (rb-p-type) "op") (= (rb-p-val) "="))
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(append! params {:type "param-opt" :name name
|
||||
:default (rb-p-parse-assign)}))
|
||||
(append! params {:type "param-req" :name name})))))))
|
||||
(define rb-p-dp-loop
|
||||
(fn ()
|
||||
(when (not (or (= (rb-p-type) "rparen") (rb-p-sep?)
|
||||
(= (rb-p-type) "eof")))
|
||||
(do
|
||||
(rb-p-dp-one)
|
||||
(when (= (rb-p-type) "comma")
|
||||
(do (rb-p-advance!) (rb-p-skip-newlines!)))
|
||||
(rb-p-dp-loop)))))
|
||||
(rb-p-dp-loop)
|
||||
params)))
|
||||
|
||||
;; def [recv.] name [(params)] body end
|
||||
(define rb-p-parse-def
|
||||
(fn ()
|
||||
(rb-p-advance!)
|
||||
(let ((recv nil) (name nil))
|
||||
(cond
|
||||
((and (= (rb-p-type) "keyword") (= (rb-p-val) "self")
|
||||
(= (get (rb-p-peek 1) :type) "dot"))
|
||||
(do
|
||||
(set! recv {:type "self"})
|
||||
(rb-p-advance!)
|
||||
(rb-p-advance!)
|
||||
(set! name (rb-p-val))
|
||||
(rb-p-advance!)))
|
||||
((and (= (rb-p-type) "ident")
|
||||
(= (get (rb-p-peek 1) :type) "dot"))
|
||||
(do
|
||||
(set! recv {:type "lvar" :name (rb-p-val)})
|
||||
(rb-p-advance!)
|
||||
(rb-p-advance!)
|
||||
(set! name (rb-p-val))
|
||||
(rb-p-advance!)))
|
||||
(:else
|
||||
(do
|
||||
(set! name (rb-p-val))
|
||||
(rb-p-advance!))))
|
||||
(let ((params (list)))
|
||||
(cond
|
||||
((= (rb-p-type) "lparen")
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(rb-p-skip-newlines!)
|
||||
(set! params (rb-p-parse-def-params))
|
||||
(rb-p-expect! "rparen")))
|
||||
((not (or (rb-p-sep?) (= (rb-p-type) "eof")))
|
||||
(set! params (rb-p-parse-def-params)))
|
||||
(:else nil))
|
||||
(rb-p-skip-seps!)
|
||||
(let ((body (rb-p-parse-stmts (list "end"))))
|
||||
(rb-p-expect-kw! "end")
|
||||
{:type "method-def" :recv recv :name name
|
||||
:params params :body body})))))
|
||||
|
||||
;; class [<<obj | Name [<Super]] body end
|
||||
(define rb-p-parse-class
|
||||
(fn ()
|
||||
(rb-p-advance!)
|
||||
(if (and (= (rb-p-type) "op") (= (rb-p-val) "<<"))
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(let ((obj (rb-p-parse-primary)))
|
||||
(rb-p-skip-seps!)
|
||||
(let ((body (rb-p-parse-stmts (list "end"))))
|
||||
(rb-p-expect-kw! "end")
|
||||
{:type "sclass" :obj obj :body body})))
|
||||
(let ((name (rb-p-parse-const-path)))
|
||||
(let ((super nil))
|
||||
(when (and (= (rb-p-type) "op") (= (rb-p-val) "<"))
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(set! super (rb-p-parse-const-path))))
|
||||
(rb-p-skip-seps!)
|
||||
(let ((body (rb-p-parse-stmts (list "end"))))
|
||||
(rb-p-expect-kw! "end")
|
||||
{:type "class-def" :name name :super super :body body}))))))
|
||||
|
||||
;; module Name body end
|
||||
(define rb-p-parse-module
|
||||
(fn ()
|
||||
(rb-p-advance!)
|
||||
(let ((name (rb-p-parse-const-path)))
|
||||
(rb-p-skip-seps!)
|
||||
(let ((body (rb-p-parse-stmts (list "end"))))
|
||||
(rb-p-expect-kw! "end")
|
||||
{:type "module-def" :name name :body body}))))
|
||||
|
||||
;; Const or Const::Const::...
|
||||
(define rb-p-parse-const-path
|
||||
(fn ()
|
||||
(let ((node {:type "const" :name (rb-p-val)}))
|
||||
(rb-p-advance!)
|
||||
(define rb-p-cp-loop
|
||||
(fn ()
|
||||
(when (= (rb-p-type) "dcolon")
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(let ((name (rb-p-val)))
|
||||
(rb-p-advance!)
|
||||
(set! node {:type "const-path" :left node :name name})
|
||||
(rb-p-cp-loop))))))
|
||||
(rb-p-cp-loop)
|
||||
node)))
|
||||
|
||||
;; [e, *e, ...]
|
||||
(define rb-p-parse-array
|
||||
(fn ()
|
||||
(rb-p-advance!)
|
||||
(rb-p-skip-newlines!)
|
||||
(let ((elems (list)))
|
||||
(define rb-p-arr-loop
|
||||
(fn ()
|
||||
(when (not (or (= (rb-p-type) "rbracket") (= (rb-p-type) "eof")))
|
||||
(do
|
||||
(if (and (= (rb-p-type) "op") (= (rb-p-val) "*"))
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(append! elems {:type "splat" :value (rb-p-parse-assign)}))
|
||||
(append! elems (rb-p-parse-assign)))
|
||||
(rb-p-skip-newlines!)
|
||||
(when (= (rb-p-type) "comma")
|
||||
(do (rb-p-advance!) (rb-p-skip-newlines!)))
|
||||
(rb-p-arr-loop)))))
|
||||
(rb-p-arr-loop)
|
||||
(rb-p-expect! "rbracket")
|
||||
{:type "array" :elems elems})))
|
||||
|
||||
;; {k: v, k => v, ...}
|
||||
(define rb-p-parse-hash
|
||||
(fn ()
|
||||
(rb-p-advance!)
|
||||
(rb-p-skip-newlines!)
|
||||
(let ((pairs (list)))
|
||||
(define rb-p-hash-loop
|
||||
(fn ()
|
||||
(when (not (or (= (rb-p-type) "rbrace") (= (rb-p-type) "eof")))
|
||||
(do
|
||||
(let ((key nil) (val nil) (style nil))
|
||||
(cond
|
||||
((and (or (= (rb-p-type) "ident") (= (rb-p-type) "const"))
|
||||
(= (get (rb-p-peek 1) :type) "colon"))
|
||||
(do
|
||||
(set! key {:type "lit-sym" :value (rb-p-val)})
|
||||
(set! style "colon")
|
||||
(rb-p-advance!)
|
||||
(rb-p-advance!)))
|
||||
(:else
|
||||
(do
|
||||
(set! key (rb-p-parse-assign))
|
||||
(set! style "rocket")
|
||||
(when (and (= (rb-p-type) "op") (= (rb-p-val) "=>"))
|
||||
(rb-p-advance!)))))
|
||||
(rb-p-skip-newlines!)
|
||||
(set! val (rb-p-parse-assign))
|
||||
(append! pairs {:key key :val val :style style}))
|
||||
(rb-p-skip-newlines!)
|
||||
(when (= (rb-p-type) "comma")
|
||||
(do (rb-p-advance!) (rb-p-skip-newlines!)))
|
||||
(rb-p-hash-loop)))))
|
||||
(rb-p-hash-loop)
|
||||
(rb-p-expect! "rbrace")
|
||||
{:type "hash" :pairs pairs})))
|
||||
|
||||
;; (a, *b, **c, &d)
|
||||
(define rb-p-parse-args-parens
|
||||
(fn ()
|
||||
(rb-p-advance!)
|
||||
(rb-p-skip-newlines!)
|
||||
(let ((args (list)))
|
||||
(define rb-p-ap-loop
|
||||
(fn ()
|
||||
(when (not (or (= (rb-p-type) "rparen") (= (rb-p-type) "eof")))
|
||||
(do
|
||||
(cond
|
||||
((and (= (rb-p-type) "op") (= (rb-p-val) "**"))
|
||||
(do (rb-p-advance!)
|
||||
(append! args {:type "dsplat" :value (rb-p-parse-assign)})))
|
||||
((and (= (rb-p-type) "op") (= (rb-p-val) "*"))
|
||||
(do (rb-p-advance!)
|
||||
(append! args {:type "splat" :value (rb-p-parse-assign)})))
|
||||
((and (= (rb-p-type) "op") (= (rb-p-val) "&"))
|
||||
(do (rb-p-advance!)
|
||||
(append! args {:type "block-pass" :value (rb-p-parse-assign)})))
|
||||
(:else (append! args (rb-p-parse-assign))))
|
||||
(rb-p-skip-newlines!)
|
||||
(when (= (rb-p-type) "comma")
|
||||
(do (rb-p-advance!) (rb-p-skip-newlines!)))
|
||||
(rb-p-ap-loop)))))
|
||||
(rb-p-ap-loop)
|
||||
(rb-p-expect! "rparen")
|
||||
args)))
|
||||
|
||||
;; No-paren arg list up to sep/end-keyword
|
||||
(define rb-p-parse-args-bare
|
||||
(fn ()
|
||||
(let ((args (list)) (going true))
|
||||
(define rb-p-ab-loop
|
||||
(fn ()
|
||||
(when (and going
|
||||
(not (rb-p-sep?))
|
||||
(not (= (rb-p-type) "eof"))
|
||||
(not (= (rb-p-type) "rparen"))
|
||||
(not (= (rb-p-type) "rbracket"))
|
||||
(not (= (rb-p-type) "rbrace"))
|
||||
(not (and (= (rb-p-type) "keyword")
|
||||
(contains? (list "end" "else" "elsif" "when"
|
||||
"rescue" "ensure" "then" "do")
|
||||
(rb-p-val)))))
|
||||
(do
|
||||
(cond
|
||||
((and (= (rb-p-type) "op") (= (rb-p-val) "*"))
|
||||
(do (rb-p-advance!)
|
||||
(append! args {:type "splat" :value (rb-p-parse-assign)})))
|
||||
((and (= (rb-p-type) "op") (= (rb-p-val) "**"))
|
||||
(do (rb-p-advance!)
|
||||
(append! args {:type "dsplat" :value (rb-p-parse-assign)})))
|
||||
((and (= (rb-p-type) "op") (= (rb-p-val) "&"))
|
||||
(do (rb-p-advance!)
|
||||
(append! args {:type "block-pass" :value (rb-p-parse-assign)})))
|
||||
(:else (append! args (rb-p-parse-assign))))
|
||||
(if (= (rb-p-type) "comma")
|
||||
(do (rb-p-advance!) (rb-p-skip-newlines!) (rb-p-ab-loop))
|
||||
(set! going false))))))
|
||||
(rb-p-ab-loop)
|
||||
args)))
|
||||
|
||||
;; Primary expression
|
||||
(define rb-p-parse-primary
|
||||
(fn ()
|
||||
(cond
|
||||
((= (rb-p-type) "int")
|
||||
(let ((v (rb-p-val))) (rb-p-advance!) {:type "lit-int" :value v}))
|
||||
((= (rb-p-type) "float")
|
||||
(let ((v (rb-p-val))) (rb-p-advance!) {:type "lit-float" :value v}))
|
||||
((= (rb-p-type) "string")
|
||||
(let ((v (rb-p-val))) (rb-p-advance!) {:type "lit-str" :value v}))
|
||||
((= (rb-p-type) "symbol")
|
||||
(let ((v (rb-p-val))) (rb-p-advance!) {:type "lit-sym" :value v}))
|
||||
((= (rb-p-type) "words")
|
||||
(let ((v (rb-p-val))) (rb-p-advance!) {:type "lit-words" :elems v}))
|
||||
((= (rb-p-type) "isymbols")
|
||||
(let ((v (rb-p-val))) (rb-p-advance!) {:type "lit-isyms" :elems v}))
|
||||
((= (rb-p-type) "ivar")
|
||||
(let ((v (rb-p-val))) (rb-p-advance!) {:type "ivar" :name v}))
|
||||
((= (rb-p-type) "cvar")
|
||||
(let ((v (rb-p-val))) (rb-p-advance!) {:type "cvar" :name v}))
|
||||
((= (rb-p-type) "gvar")
|
||||
(let ((v (rb-p-val))) (rb-p-advance!) {:type "gvar" :name v}))
|
||||
((= (rb-p-type) "const")
|
||||
(rb-p-parse-const-path))
|
||||
((= (rb-p-type) "ident")
|
||||
(let ((name (rb-p-val)))
|
||||
(rb-p-advance!)
|
||||
(if (= (rb-p-type) "lparen")
|
||||
(let ((args (rb-p-parse-args-parens))
|
||||
(blk (rb-p-parse-block)))
|
||||
{:type "send" :name name :args args :block blk})
|
||||
{:type "send" :name name :args (list) :block nil})))
|
||||
((= (rb-p-type) "keyword")
|
||||
(cond
|
||||
((= (rb-p-val) "nil")
|
||||
(do (rb-p-advance!) {:type "lit-nil"}))
|
||||
((= (rb-p-val) "true")
|
||||
(do (rb-p-advance!) {:type "lit-bool" :value true}))
|
||||
((= (rb-p-val) "false")
|
||||
(do (rb-p-advance!) {:type "lit-bool" :value false}))
|
||||
((= (rb-p-val) "self")
|
||||
(do (rb-p-advance!) {:type "self"}))
|
||||
((= (rb-p-val) "super")
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(let ((args (if (= (rb-p-type) "lparen")
|
||||
(rb-p-parse-args-parens) (list)))
|
||||
(blk (rb-p-parse-block)))
|
||||
{:type "send" :name "super" :args args :block blk})))
|
||||
(:else
|
||||
{:type "error"
|
||||
:msg (join "" (list "unexpected kw " (rb-p-val)))})))
|
||||
((= (rb-p-type) "lbracket")
|
||||
(rb-p-parse-array))
|
||||
((= (rb-p-type) "lbrace")
|
||||
(rb-p-parse-hash))
|
||||
((= (rb-p-type) "lparen")
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(rb-p-skip-seps!)
|
||||
(let ((node (rb-p-parse-expr)))
|
||||
(rb-p-skip-seps!)
|
||||
(rb-p-expect! "rparen")
|
||||
node)))
|
||||
(:else
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
{:type "error"
|
||||
:msg (join "" (list "unexpected " (rb-p-type)
|
||||
" '" (or (rb-p-val) "") "'"))})))))
|
||||
|
||||
;; .method ::Const [index] chains
|
||||
(define rb-p-parse-postfix
|
||||
(fn ()
|
||||
(let ((node (rb-p-parse-primary)))
|
||||
(define rb-p-pf-loop
|
||||
(fn ()
|
||||
(cond
|
||||
((= (rb-p-type) "dot")
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(let ((method (rb-p-val)))
|
||||
(rb-p-advance!)
|
||||
(let ((args (if (= (rb-p-type) "lparen")
|
||||
(rb-p-parse-args-parens) (list)))
|
||||
(blk (rb-p-parse-block)))
|
||||
(set! node {:type "call" :recv node :method method
|
||||
:args args :block blk})
|
||||
(rb-p-pf-loop)))))
|
||||
((= (rb-p-type) "dcolon")
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(let ((name (rb-p-val)))
|
||||
(rb-p-advance!)
|
||||
(if (= (rb-p-type) "lparen")
|
||||
(let ((args (rb-p-parse-args-parens))
|
||||
(blk (rb-p-parse-block)))
|
||||
(set! node {:type "call" :recv node :method name
|
||||
:args args :block blk}))
|
||||
(set! node {:type "const-path" :left node :name name}))
|
||||
(rb-p-pf-loop))))
|
||||
((= (rb-p-type) "lbracket")
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(rb-p-skip-newlines!)
|
||||
(let ((idxargs (list)))
|
||||
(define rb-p-idx-loop
|
||||
(fn ()
|
||||
(when (not (or (= (rb-p-type) "rbracket") (= (rb-p-type) "eof")))
|
||||
(do
|
||||
(append! idxargs (rb-p-parse-assign))
|
||||
(when (= (rb-p-type) "comma")
|
||||
(do (rb-p-advance!) (rb-p-skip-newlines!)))
|
||||
(rb-p-idx-loop)))))
|
||||
(rb-p-idx-loop)
|
||||
(rb-p-expect! "rbracket")
|
||||
(set! node {:type "index" :recv node :args idxargs})
|
||||
(rb-p-pf-loop))))
|
||||
(:else nil))))
|
||||
(rb-p-pf-loop)
|
||||
node)))
|
||||
|
||||
(define rb-p-parse-unary
|
||||
(fn ()
|
||||
(cond
|
||||
((and (= (rb-p-type) "op") (= (rb-p-val) "!"))
|
||||
(do (rb-p-advance!)
|
||||
{:type "unop" :op "!" :value (rb-p-parse-unary)}))
|
||||
((and (= (rb-p-type) "op") (= (rb-p-val) "~"))
|
||||
(do (rb-p-advance!)
|
||||
{:type "unop" :op "~" :value (rb-p-parse-unary)}))
|
||||
((and (= (rb-p-type) "op") (= (rb-p-val) "-"))
|
||||
(do (rb-p-advance!)
|
||||
{:type "unop" :op "-" :value (rb-p-parse-unary)}))
|
||||
((and (= (rb-p-type) "op") (= (rb-p-val) "+"))
|
||||
(do (rb-p-advance!) (rb-p-parse-unary)))
|
||||
(:else (rb-p-parse-postfix)))))
|
||||
|
||||
(define rb-p-parse-power
|
||||
(fn ()
|
||||
(let ((node (rb-p-parse-unary)))
|
||||
(if (and (= (rb-p-type) "op") (= (rb-p-val) "**"))
|
||||
(do (rb-p-advance!)
|
||||
{:type "binop" :op "**" :left node :right (rb-p-parse-power)})
|
||||
node))))
|
||||
|
||||
(define rb-p-parse-mul
|
||||
(fn ()
|
||||
(let ((node (rb-p-parse-power)))
|
||||
(define rb-p-mul-loop
|
||||
(fn ()
|
||||
(if (and (= (rb-p-type) "op")
|
||||
(or (= (rb-p-val) "*") (= (rb-p-val) "/") (= (rb-p-val) "%")))
|
||||
(let ((op (rb-p-val)))
|
||||
(rb-p-advance!)
|
||||
(set! node {:type "binop" :op op :left node :right (rb-p-parse-power)})
|
||||
(rb-p-mul-loop))
|
||||
node)))
|
||||
(rb-p-mul-loop))))
|
||||
|
||||
(define rb-p-parse-add
|
||||
(fn ()
|
||||
(let ((node (rb-p-parse-mul)))
|
||||
(define rb-p-add-loop
|
||||
(fn ()
|
||||
(if (and (= (rb-p-type) "op")
|
||||
(or (= (rb-p-val) "+") (= (rb-p-val) "-")))
|
||||
(let ((op (rb-p-val)))
|
||||
(rb-p-advance!)
|
||||
(set! node {:type "binop" :op op :left node :right (rb-p-parse-mul)})
|
||||
(rb-p-add-loop))
|
||||
node)))
|
||||
(rb-p-add-loop))))
|
||||
|
||||
(define rb-p-parse-shift
|
||||
(fn ()
|
||||
(let ((node (rb-p-parse-add)))
|
||||
(define rb-p-sh-loop
|
||||
(fn ()
|
||||
(if (and (= (rb-p-type) "op")
|
||||
(or (= (rb-p-val) "<<") (= (rb-p-val) ">>")))
|
||||
(let ((op (rb-p-val)))
|
||||
(rb-p-advance!)
|
||||
(set! node {:type "binop" :op op :left node :right (rb-p-parse-add)})
|
||||
(rb-p-sh-loop))
|
||||
node)))
|
||||
(rb-p-sh-loop))))
|
||||
|
||||
(define rb-p-parse-bitand
|
||||
(fn ()
|
||||
(let ((node (rb-p-parse-shift)))
|
||||
(define rb-p-ba-loop
|
||||
(fn ()
|
||||
(if (and (= (rb-p-type) "op") (= (rb-p-val) "&"))
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(set! node {:type "binop" :op "&" :left node :right (rb-p-parse-shift)})
|
||||
(rb-p-ba-loop))
|
||||
node)))
|
||||
(rb-p-ba-loop))))
|
||||
|
||||
;; | is "pipe" token (not "op")
|
||||
(define rb-p-parse-bitor
|
||||
(fn ()
|
||||
(let ((node (rb-p-parse-bitand)))
|
||||
(define rb-p-bo-loop
|
||||
(fn ()
|
||||
(cond
|
||||
((= (rb-p-type) "pipe")
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(set! node {:type "binop" :op "|" :left node :right (rb-p-parse-bitand)})
|
||||
(rb-p-bo-loop)))
|
||||
((and (= (rb-p-type) "op") (= (rb-p-val) "^"))
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(set! node {:type "binop" :op "^" :left node :right (rb-p-parse-bitand)})
|
||||
(rb-p-bo-loop)))
|
||||
(:else node))))
|
||||
(rb-p-bo-loop))))
|
||||
|
||||
(define rb-p-parse-comparison
|
||||
(fn ()
|
||||
(let ((node (rb-p-parse-bitor)))
|
||||
(if (and (= (rb-p-type) "op")
|
||||
(contains? (list "==" "!=" "<" ">" "<=" ">="
|
||||
"<=>" "===" "=~" "!~") (rb-p-val)))
|
||||
(let ((op (rb-p-val)))
|
||||
(rb-p-advance!)
|
||||
{:type "binop" :op op :left node :right (rb-p-parse-bitor)})
|
||||
node))))
|
||||
|
||||
(define rb-p-parse-not
|
||||
(fn ()
|
||||
(if (and (= (rb-p-type) "keyword") (= (rb-p-val) "not"))
|
||||
(do (rb-p-advance!)
|
||||
{:type "not" :value (rb-p-parse-not)})
|
||||
(rb-p-parse-comparison))))
|
||||
|
||||
(define rb-p-parse-and
|
||||
(fn ()
|
||||
(let ((node (rb-p-parse-not)))
|
||||
(define rb-p-and-loop
|
||||
(fn ()
|
||||
(cond
|
||||
((and (= (rb-p-type) "op") (= (rb-p-val) "&&"))
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(set! node {:type "binop" :op "&&" :left node :right (rb-p-parse-not)})
|
||||
(rb-p-and-loop)))
|
||||
((and (= (rb-p-type) "keyword") (= (rb-p-val) "and"))
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(set! node {:type "binop" :op "and" :left node :right (rb-p-parse-not)})
|
||||
(rb-p-and-loop)))
|
||||
(:else node))))
|
||||
(rb-p-and-loop))))
|
||||
|
||||
(define rb-p-parse-or
|
||||
(fn ()
|
||||
(let ((node (rb-p-parse-and)))
|
||||
(define rb-p-or-loop
|
||||
(fn ()
|
||||
(cond
|
||||
((and (= (rb-p-type) "op") (= (rb-p-val) "||"))
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(set! node {:type "binop" :op "||" :left node :right (rb-p-parse-and)})
|
||||
(rb-p-or-loop)))
|
||||
((and (= (rb-p-type) "keyword") (= (rb-p-val) "or"))
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(set! node {:type "binop" :op "or" :left node :right (rb-p-parse-and)})
|
||||
(rb-p-or-loop)))
|
||||
(:else node))))
|
||||
(rb-p-or-loop))))
|
||||
|
||||
(define rb-p-parse-range
|
||||
(fn ()
|
||||
(let ((node (rb-p-parse-or)))
|
||||
(cond
|
||||
((= (rb-p-type) "dotdot")
|
||||
(do (rb-p-advance!)
|
||||
{:type "range" :from node :to (rb-p-parse-or) :exclusive false}))
|
||||
((= (rb-p-type) "dotdotdot")
|
||||
(do (rb-p-advance!)
|
||||
{:type "range" :from node :to (rb-p-parse-or) :exclusive true}))
|
||||
(:else node)))))
|
||||
|
||||
(define rb-p-parse-assign
|
||||
(fn ()
|
||||
(let ((node (rb-p-parse-range)))
|
||||
(cond
|
||||
((and (= (rb-p-type) "op") (= (rb-p-val) "="))
|
||||
(do (rb-p-advance!)
|
||||
{:type "assign" :target node :value (rb-p-parse-assign)}))
|
||||
((and (= (rb-p-type) "op")
|
||||
(contains? (list "+=" "-=" "*=" "/=" "%=" "**="
|
||||
"<<=" ">>=" "&=" "|=" "^=" "&&=" "||=")
|
||||
(rb-p-val)))
|
||||
(let ((op (substring (rb-p-val) 0 (- (len (rb-p-val)) 1))))
|
||||
(rb-p-advance!)
|
||||
{:type "op-assign" :target node :op op :value (rb-p-parse-assign)}))
|
||||
(:else node)))))
|
||||
|
||||
(define rb-p-parse-expr
|
||||
(fn () (rb-p-parse-assign)))
|
||||
|
||||
;; e, e, ... → single node or array
|
||||
(define rb-p-parse-multi-val
|
||||
(fn ()
|
||||
(let ((vals (list)))
|
||||
(define rb-p-mv-loop
|
||||
(fn ()
|
||||
(append! vals (rb-p-parse-assign))
|
||||
(when (= (rb-p-type) "comma")
|
||||
(do (rb-p-advance!) (rb-p-skip-newlines!) (rb-p-mv-loop)))))
|
||||
(rb-p-mv-loop)
|
||||
(if (= (len vals) 1)
|
||||
(nth vals 0)
|
||||
{:type "array" :elems vals}))))
|
||||
|
||||
;; a, b, *c = rhs
|
||||
(define rb-p-parse-massign
|
||||
(fn ()
|
||||
(let ((targets (list)))
|
||||
(define rb-p-ma-loop
|
||||
(fn ()
|
||||
(cond
|
||||
((and (= (rb-p-type) "op") (= (rb-p-val) "*"))
|
||||
(do
|
||||
(rb-p-advance!)
|
||||
(if (= (rb-p-type) "ident")
|
||||
(do
|
||||
(append! targets {:type "splat-target" :name (rb-p-val)})
|
||||
(rb-p-advance!))
|
||||
(append! targets {:type "splat-target" :name nil}))))
|
||||
((= (rb-p-type) "ident")
|
||||
(do (append! targets {:type "lvar" :name (rb-p-val)}) (rb-p-advance!)))
|
||||
((= (rb-p-type) "ivar")
|
||||
(do (append! targets {:type "ivar" :name (rb-p-val)}) (rb-p-advance!)))
|
||||
((= (rb-p-type) "cvar")
|
||||
(do (append! targets {:type "cvar" :name (rb-p-val)}) (rb-p-advance!)))
|
||||
((= (rb-p-type) "gvar")
|
||||
(do (append! targets {:type "gvar" :name (rb-p-val)}) (rb-p-advance!)))
|
||||
((= (rb-p-type) "const")
|
||||
(do (append! targets {:type "const" :name (rb-p-val)}) (rb-p-advance!)))
|
||||
(:else nil))
|
||||
(when (= (rb-p-type) "comma")
|
||||
(do (rb-p-advance!) (rb-p-skip-newlines!) (rb-p-ma-loop)))))
|
||||
(rb-p-ma-loop)
|
||||
(rb-p-advance!)
|
||||
{:type "massign" :targets targets :value (rb-p-parse-multi-val)})))
|
||||
|
||||
(define rb-p-parse-stmt
|
||||
(fn ()
|
||||
(cond
|
||||
((and (= (rb-p-type) "keyword") (= (rb-p-val) "def"))
|
||||
(rb-p-parse-def))
|
||||
((and (= (rb-p-type) "keyword") (= (rb-p-val) "class"))
|
||||
(rb-p-parse-class))
|
||||
((and (= (rb-p-type) "keyword") (= (rb-p-val) "module"))
|
||||
(rb-p-parse-module))
|
||||
((and (= (rb-p-type) "keyword") (= (rb-p-val) "return"))
|
||||
(do (rb-p-advance!)
|
||||
{:type "return"
|
||||
:value (if (or (rb-p-sep?) (= (rb-p-type) "eof"))
|
||||
nil (rb-p-parse-multi-val))}))
|
||||
((and (= (rb-p-type) "keyword") (= (rb-p-val) "yield"))
|
||||
(do (rb-p-advance!)
|
||||
{:type "yield"
|
||||
:args (cond
|
||||
((= (rb-p-type) "lparen") (rb-p-parse-args-parens))
|
||||
((or (rb-p-sep?) (= (rb-p-type) "eof")) (list))
|
||||
(:else (rb-p-parse-args-bare)))}))
|
||||
((and (= (rb-p-type) "keyword") (= (rb-p-val) "break"))
|
||||
(do (rb-p-advance!)
|
||||
{:type "break"
|
||||
:value (if (or (rb-p-sep?) (= (rb-p-type) "eof"))
|
||||
nil (rb-p-parse-expr))}))
|
||||
((and (= (rb-p-type) "keyword") (= (rb-p-val) "next"))
|
||||
(do (rb-p-advance!)
|
||||
{:type "next"
|
||||
:value (if (or (rb-p-sep?) (= (rb-p-type) "eof"))
|
||||
nil (rb-p-parse-expr))}))
|
||||
((and (= (rb-p-type) "keyword") (= (rb-p-val) "redo"))
|
||||
(do (rb-p-advance!) {:type "redo"}))
|
||||
((and (= (rb-p-type) "keyword") (= (rb-p-val) "raise"))
|
||||
(do (rb-p-advance!)
|
||||
{:type "raise"
|
||||
:value (if (or (rb-p-sep?) (= (rb-p-type) "eof"))
|
||||
nil (rb-p-parse-expr))}))
|
||||
;; Massign: token followed by comma
|
||||
((and (or (= (rb-p-type) "ident") (= (rb-p-type) "ivar")
|
||||
(= (rb-p-type) "cvar") (= (rb-p-type) "gvar")
|
||||
(= (rb-p-type) "const"))
|
||||
(= (get (rb-p-peek 1) :type) "comma"))
|
||||
(rb-p-parse-massign))
|
||||
(:else
|
||||
(let ((node (rb-p-parse-assign)))
|
||||
(if (and (= (get node :type) "send")
|
||||
(= (len (get node :args)) 0)
|
||||
(nil? (get node :block)))
|
||||
;; Bare send: check for block or no-paren args
|
||||
(cond
|
||||
;; Block immediately follows (do or {)
|
||||
((or (and (= (rb-p-type) "keyword") (= (rb-p-val) "do"))
|
||||
(= (rb-p-type) "lbrace"))
|
||||
(let ((blk (rb-p-parse-block)))
|
||||
{:type "send" :name (get node :name) :args (list) :block blk}))
|
||||
;; No-paren args (stop before block/sep/end keywords)
|
||||
((and (not (rb-p-sep?))
|
||||
(not (= (rb-p-type) "eof"))
|
||||
(not (= (rb-p-type) "op"))
|
||||
(not (= (rb-p-type) "dot"))
|
||||
(not (= (rb-p-type) "dcolon"))
|
||||
(not (= (rb-p-type) "rparen"))
|
||||
(not (= (rb-p-type) "rbracket"))
|
||||
(not (= (rb-p-type) "rbrace"))
|
||||
(not (= (rb-p-type) "lbrace"))
|
||||
(not (and (= (rb-p-type) "keyword")
|
||||
(contains? (list "end" "else" "elsif" "when"
|
||||
"rescue" "ensure" "then" "do"
|
||||
"and" "or" "not")
|
||||
(rb-p-val)))))
|
||||
(let ((args (rb-p-parse-args-bare))
|
||||
(blk (rb-p-parse-block)))
|
||||
(if (> (len args) 0)
|
||||
{:type "send" :name (get node :name) :args args :block blk}
|
||||
node)))
|
||||
(:else node))
|
||||
node))))))
|
||||
|
||||
(define rb-p-parse-stmts
|
||||
(fn (terminators)
|
||||
(let ((stmts (list)))
|
||||
(define rb-p-at-term?
|
||||
(fn ()
|
||||
(or (= (rb-p-type) "eof")
|
||||
(and (= (rb-p-type) "keyword")
|
||||
(contains? terminators (rb-p-val)))
|
||||
(and (= (rb-p-type) "rbrace")
|
||||
(contains? terminators "rbrace")))))
|
||||
(define rb-p-ps-loop
|
||||
(fn ()
|
||||
(rb-p-skip-seps!)
|
||||
(when (not (rb-p-at-term?))
|
||||
(do
|
||||
(append! stmts (rb-p-parse-stmt))
|
||||
(rb-p-skip-seps!)
|
||||
(rb-p-ps-loop)))))
|
||||
(rb-p-ps-loop)
|
||||
stmts)))
|
||||
|
||||
{:type "program" :stmts (rb-p-parse-stmts (list))})))
|
||||
|
||||
(define rb-parse-str
|
||||
(fn (src) (rb-parse (rb-tokenize src))))
|
||||
92
lib/ruby/test.sh
Executable file
92
lib/ruby/test.sh
Executable file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
# Ruby-on-SX test runner.
|
||||
# Usage:
|
||||
# bash lib/ruby/test.sh # run all tests
|
||||
# bash lib/ruby/test.sh -v # verbose
|
||||
# bash lib/ruby/test.sh tests/parse.sx # single file
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
SX_SERVER="hosts/ocaml/_build/default/bin/sx_server.exe"
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
MAIN_ROOT=$(git worktree list | head -1 | awk '{print $1}')
|
||||
if [ -x "$MAIN_ROOT/$SX_SERVER" ]; then
|
||||
SX_SERVER="$MAIN_ROOT/$SX_SERVER"
|
||||
else
|
||||
echo "ERROR: sx_server.exe not found."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
VERBOSE=""
|
||||
FILES=()
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
-v|--verbose) VERBOSE=1 ;;
|
||||
*) FILES+=("$arg") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ ${#FILES[@]} -eq 0 ]; then
|
||||
mapfile -t FILES < <(find lib/ruby/tests -maxdepth 2 -name '*.sx' | sort)
|
||||
fi
|
||||
|
||||
TOTAL_PASS=0
|
||||
TOTAL_FAIL=0
|
||||
FAILED_FILES=()
|
||||
|
||||
for FILE in "${FILES[@]}"; do
|
||||
[ -f "$FILE" ] || { echo "skip $FILE (not found)"; continue; }
|
||||
TMPFILE=$(mktemp)
|
||||
# Build epoch sequence: load runtime files, then test file, then eval summary.
|
||||
{
|
||||
echo "(epoch 1)"
|
||||
echo "(load \"lib/ruby/tokenizer.sx\")"
|
||||
if [ -f "lib/ruby/parser.sx" ]; then
|
||||
echo "(epoch 2)"
|
||||
echo "(load \"lib/ruby/parser.sx\")"
|
||||
fi
|
||||
echo "(epoch 3)"
|
||||
echo "(load \"$FILE\")"
|
||||
echo "(epoch 4)"
|
||||
echo "(eval \"(list rb-test-pass rb-test-fail)\")"
|
||||
} > "$TMPFILE"
|
||||
|
||||
OUTPUT=$(timeout 60 "$SX_SERVER" < "$TMPFILE" 2>&1 || true)
|
||||
rm -f "$TMPFILE"
|
||||
|
||||
# Extract epoch 4 result: (ok-len 4 N)\n<val> or (ok 4 <val>)
|
||||
LINE=$(printf '%s\n' "$OUTPUT" | awk '/^\(ok-len 4 / {getline; print; exit}')
|
||||
if [ -z "$LINE" ]; then
|
||||
LINE=$(printf '%s\n' "$OUTPUT" \
|
||||
| grep -E '^\(ok 4 \([0-9]+ [0-9]+\)\)' | tail -1 \
|
||||
| sed -E 's/^\(ok 4 //; s/\)$//')
|
||||
fi
|
||||
if [ -z "$LINE" ]; then
|
||||
echo "✗ $FILE: could not extract summary"
|
||||
printf '%s\n' "$OUTPUT" | grep -v '^(ok ' | tail -10
|
||||
TOTAL_FAIL=$((TOTAL_FAIL + 1))
|
||||
FAILED_FILES+=("$FILE")
|
||||
continue
|
||||
fi
|
||||
P=$(printf '%s\n' "$LINE" | sed -E 's/^\(([0-9]+) ([0-9]+)\).*/\1/')
|
||||
F=$(printf '%s\n' "$LINE" | sed -E 's/^\(([0-9]+) ([0-9]+)\).*/\2/')
|
||||
TOTAL_PASS=$((TOTAL_PASS + P))
|
||||
TOTAL_FAIL=$((TOTAL_FAIL + F))
|
||||
if [ "$F" -gt 0 ]; then
|
||||
FAILED_FILES+=("$FILE")
|
||||
printf '✗ %-40s %d/%d\n' "$FILE" "$P" "$((P+F))"
|
||||
elif [ "$VERBOSE" = "1" ]; then
|
||||
printf '✓ %-40s %d passed\n' "$FILE" "$P"
|
||||
fi
|
||||
done
|
||||
|
||||
TOTAL=$((TOTAL_PASS + TOTAL_FAIL))
|
||||
if [ $TOTAL_FAIL -eq 0 ]; then
|
||||
echo "✓ $TOTAL_PASS/$TOTAL ruby-on-sx tests passed"
|
||||
else
|
||||
echo "✗ $TOTAL_PASS/$TOTAL passed, $TOTAL_FAIL failed in: ${FAILED_FILES[*]}"
|
||||
fi
|
||||
|
||||
[ $TOTAL_FAIL -eq 0 ]
|
||||
439
lib/ruby/tests/parse.sx
Normal file
439
lib/ruby/tests/parse.sx
Normal file
@@ -0,0 +1,439 @@
|
||||
;; Parser tests for Ruby 2.7 subset.
|
||||
|
||||
(define rb-deep=?
|
||||
(fn (a b)
|
||||
(cond
|
||||
((= a b) true)
|
||||
((and (dict? a) (dict? b))
|
||||
(let ((ak (keys a)) (bk (keys b)))
|
||||
(if (not (= (len ak) (len bk)))
|
||||
false
|
||||
(every?
|
||||
(fn (k)
|
||||
(and (has-key? b k) (rb-deep=? (get a k) (get b k))))
|
||||
ak))))
|
||||
((and (list? a) (list? b))
|
||||
(if (not (= (len a) (len b)))
|
||||
false
|
||||
(let ((i 0) (ok true))
|
||||
(define rb-de-loop
|
||||
(fn ()
|
||||
(when (and ok (< i (len a)))
|
||||
(do
|
||||
(when (not (rb-deep=? (nth a i) (nth b i)))
|
||||
(set! ok false))
|
||||
(set! i (+ i 1))
|
||||
(rb-de-loop)))))
|
||||
(rb-de-loop)
|
||||
ok)))
|
||||
(:else false))))
|
||||
|
||||
(define rb-test-pass 0)
|
||||
(define rb-test-fail 0)
|
||||
(define rb-test-fails (list))
|
||||
|
||||
(define rb-test
|
||||
(fn (name actual expected)
|
||||
(if (rb-deep=? actual expected)
|
||||
(set! rb-test-pass (+ rb-test-pass 1))
|
||||
(do
|
||||
(set! rb-test-fail (+ rb-test-fail 1))
|
||||
(append! rb-test-fails {:name name :actual actual :expected expected})))))
|
||||
|
||||
;; Shorthand: parse src and extract :stmts list
|
||||
(define rb-p-stmts
|
||||
(fn (src)
|
||||
(get (rb-parse-str src) :stmts)))
|
||||
|
||||
;; Shorthand: parse and get first statement
|
||||
(define rb-p-first
|
||||
(fn (src)
|
||||
(nth (rb-p-stmts src) 0)))
|
||||
|
||||
;; ── Literals ─────────────────────────────────────────────────────────────────
|
||||
|
||||
(rb-test "int literal"
|
||||
(rb-p-first "42")
|
||||
{:type "lit-int" :value 42})
|
||||
|
||||
(rb-test "negative int"
|
||||
(rb-p-first "-7")
|
||||
{:type "unop" :op "-" :value {:type "lit-int" :value 7}})
|
||||
|
||||
(rb-test "float literal"
|
||||
(rb-p-first "3.14")
|
||||
{:type "lit-float" :value "3.14"})
|
||||
|
||||
(rb-test "string literal"
|
||||
(rb-p-first "\"hello\"")
|
||||
{:type "lit-str" :value "hello"})
|
||||
|
||||
(rb-test "symbol literal"
|
||||
(rb-p-first ":foo")
|
||||
{:type "lit-sym" :value "foo"})
|
||||
|
||||
(rb-test "nil literal"
|
||||
(rb-p-first "nil")
|
||||
{:type "lit-nil"})
|
||||
|
||||
(rb-test "true literal"
|
||||
(rb-p-first "true")
|
||||
{:type "lit-bool" :value true})
|
||||
|
||||
(rb-test "false literal"
|
||||
(rb-p-first "false")
|
||||
{:type "lit-bool" :value false})
|
||||
|
||||
(rb-test "self"
|
||||
(rb-p-first "self")
|
||||
{:type "self"})
|
||||
|
||||
(rb-test "%w[] words"
|
||||
(rb-p-first "%w[a b c]")
|
||||
{:type "lit-words" :elems (list "a" "b" "c")})
|
||||
|
||||
(rb-test "%i[] isymbols"
|
||||
(rb-p-first "%i[x y]")
|
||||
{:type "lit-isyms" :elems (list "x" "y")})
|
||||
|
||||
;; ── Variables ─────────────────────────────────────────────────────────────────
|
||||
|
||||
(rb-test "local var / send"
|
||||
(rb-p-first "x")
|
||||
{:type "send" :name "x" :args (list) :block nil})
|
||||
|
||||
(rb-test "ivar"
|
||||
(rb-p-first "@foo")
|
||||
{:type "ivar" :name "@foo"})
|
||||
|
||||
(rb-test "cvar"
|
||||
(rb-p-first "@@count")
|
||||
{:type "cvar" :name "@@count"})
|
||||
|
||||
(rb-test "gvar"
|
||||
(rb-p-first "$stdout")
|
||||
{:type "gvar" :name "$stdout"})
|
||||
|
||||
(rb-test "constant"
|
||||
(rb-p-first "Foo")
|
||||
{:type "const" :name "Foo"})
|
||||
|
||||
(rb-test "const path"
|
||||
(rb-p-first "Foo::Bar")
|
||||
{:type "const-path"
|
||||
:left {:type "const" :name "Foo"}
|
||||
:name "Bar"})
|
||||
|
||||
(rb-test "triple const path"
|
||||
(rb-p-first "A::B::C")
|
||||
{:type "const-path"
|
||||
:left {:type "const-path"
|
||||
:left {:type "const" :name "A"}
|
||||
:name "B"}
|
||||
:name "C"})
|
||||
|
||||
;; ── Arrays and Hashes ─────────────────────────────────────────────────────────
|
||||
|
||||
(rb-test "empty array"
|
||||
(rb-p-first "[]")
|
||||
{:type "array" :elems (list)})
|
||||
|
||||
(rb-test "array literal"
|
||||
(rb-p-first "[1, 2, 3]")
|
||||
{:type "array" :elems (list {:type "lit-int" :value 1}
|
||||
{:type "lit-int" :value 2}
|
||||
{:type "lit-int" :value 3})})
|
||||
|
||||
(rb-test "hash colon style"
|
||||
(get (rb-p-first "{a: 1}") :type)
|
||||
"hash")
|
||||
|
||||
(rb-test "hash pair style"
|
||||
(get (nth (get (rb-p-first "{a: 1}") :pairs) 0) :style)
|
||||
"colon")
|
||||
|
||||
(rb-test "hash symbol key"
|
||||
(get (get (nth (get (rb-p-first "{a: 1}") :pairs) 0) :key) :value)
|
||||
"a")
|
||||
|
||||
;; ── Binary operators ──────────────────────────────────────────────────────────
|
||||
|
||||
(rb-test "addition"
|
||||
(rb-p-first "1 + 2")
|
||||
{:type "binop" :op "+"
|
||||
:left {:type "lit-int" :value 1}
|
||||
:right {:type "lit-int" :value 2}})
|
||||
|
||||
(rb-test "subtraction"
|
||||
(get (rb-p-first "a - b") :op)
|
||||
"-")
|
||||
|
||||
(rb-test "multiplication"
|
||||
(get (rb-p-first "x * y") :op)
|
||||
"*")
|
||||
|
||||
(rb-test "precedence: * before +"
|
||||
(rb-p-first "1 + 2 * 3")
|
||||
{:type "binop" :op "+"
|
||||
:left {:type "lit-int" :value 1}
|
||||
:right {:type "binop" :op "*"
|
||||
:left {:type "lit-int" :value 2}
|
||||
:right {:type "lit-int" :value 3}}})
|
||||
|
||||
(rb-test "power right-assoc"
|
||||
(rb-p-first "2 ** 3 ** 4")
|
||||
{:type "binop" :op "**"
|
||||
:left {:type "lit-int" :value 2}
|
||||
:right {:type "binop" :op "**"
|
||||
:left {:type "lit-int" :value 3}
|
||||
:right {:type "lit-int" :value 4}}})
|
||||
|
||||
(rb-test "equality"
|
||||
(get (rb-p-first "a == b") :op)
|
||||
"==")
|
||||
|
||||
(rb-test "logical and"
|
||||
(get (rb-p-first "a && b") :op)
|
||||
"&&")
|
||||
|
||||
(rb-test "logical or"
|
||||
(get (rb-p-first "a || b") :op)
|
||||
"||")
|
||||
|
||||
(rb-test "range inclusive"
|
||||
(rb-p-first "1..5")
|
||||
{:type "range"
|
||||
:from {:type "lit-int" :value 1}
|
||||
:to {:type "lit-int" :value 5}
|
||||
:exclusive false})
|
||||
|
||||
(rb-test "range exclusive"
|
||||
(get (rb-p-first "1...5") :exclusive)
|
||||
true)
|
||||
|
||||
;; ── Assignment ────────────────────────────────────────────────────────────────
|
||||
|
||||
(rb-test "assign"
|
||||
(rb-p-first "x = 1")
|
||||
{:type "assign"
|
||||
:target {:type "send" :name "x" :args (list) :block nil}
|
||||
:value {:type "lit-int" :value 1}})
|
||||
|
||||
(rb-test "op-assign +="
|
||||
(get (rb-p-first "x += 1") :type)
|
||||
"op-assign")
|
||||
|
||||
(rb-test "op-assign op"
|
||||
(get (rb-p-first "x += 1") :op)
|
||||
"+")
|
||||
|
||||
(rb-test "massign"
|
||||
(get (rb-p-first "a, b = 1, 2") :type)
|
||||
"massign")
|
||||
|
||||
(rb-test "massign targets"
|
||||
(len (get (rb-p-first "a, b = 1, 2") :targets))
|
||||
2)
|
||||
|
||||
(rb-test "massign value array"
|
||||
(get (get (rb-p-first "a, b = 1, 2") :value) :type)
|
||||
"array")
|
||||
|
||||
;; ── Method calls ──────────────────────────────────────────────────────────────
|
||||
|
||||
(rb-test "call with parens"
|
||||
(rb-p-first "foo(1, 2)")
|
||||
{:type "send" :name "foo"
|
||||
:args (list {:type "lit-int" :value 1}
|
||||
{:type "lit-int" :value 2})
|
||||
:block nil})
|
||||
|
||||
(rb-test "chained call"
|
||||
(get (rb-p-first "obj.foo") :type)
|
||||
"call")
|
||||
|
||||
(rb-test "chained call method"
|
||||
(get (rb-p-first "obj.foo") :method)
|
||||
"foo")
|
||||
|
||||
(rb-test "chained call with args"
|
||||
(len (get (rb-p-first "obj.foo(1, 2)") :args))
|
||||
2)
|
||||
|
||||
(rb-test "no-paren call"
|
||||
(get (rb-p-first "puts \"hello\"") :type)
|
||||
"send")
|
||||
|
||||
(rb-test "no-paren call name"
|
||||
(get (rb-p-first "puts \"hello\"") :name)
|
||||
"puts")
|
||||
|
||||
(rb-test "no-paren call args"
|
||||
(len (get (rb-p-first "puts \"hello\"") :args))
|
||||
1)
|
||||
|
||||
(rb-test "indexing"
|
||||
(get (rb-p-first "a[0]") :type)
|
||||
"index")
|
||||
|
||||
;; ── Unary operators ───────────────────────────────────────────────────────────
|
||||
|
||||
(rb-test "unary not"
|
||||
(rb-p-first "!x")
|
||||
{:type "unop" :op "!"
|
||||
:value {:type "send" :name "x" :args (list) :block nil}})
|
||||
|
||||
(rb-test "unary minus"
|
||||
(get (rb-p-first "-x") :op)
|
||||
"-")
|
||||
|
||||
;; ── Method def ────────────────────────────────────────────────────────────────
|
||||
|
||||
(rb-test "empty method def"
|
||||
(get (rb-p-first "def foo; end") :type)
|
||||
"method-def")
|
||||
|
||||
(rb-test "method def name"
|
||||
(get (rb-p-first "def foo; end") :name)
|
||||
"foo")
|
||||
|
||||
(rb-test "method def no params"
|
||||
(len (get (rb-p-first "def foo; end") :params))
|
||||
0)
|
||||
|
||||
(rb-test "method def with params"
|
||||
(len (get (rb-p-first "def foo(a, b); end") :params))
|
||||
2)
|
||||
|
||||
(rb-test "method def param-req"
|
||||
(get (nth (get (rb-p-first "def foo(a); end") :params) 0) :type)
|
||||
"param-req")
|
||||
|
||||
(rb-test "method def param name"
|
||||
(get (nth (get (rb-p-first "def foo(a); end") :params) 0) :name)
|
||||
"a")
|
||||
|
||||
(rb-test "method def optional param"
|
||||
(get (nth (get (rb-p-first "def foo(a, b=1); end") :params) 1) :type)
|
||||
"param-opt")
|
||||
|
||||
(rb-test "method def splat"
|
||||
(get (nth (get (rb-p-first "def foo(*args); end") :params) 0) :type)
|
||||
"param-rest")
|
||||
|
||||
(rb-test "method def double splat"
|
||||
(get (nth (get (rb-p-first "def foo(**opts); end") :params) 0) :type)
|
||||
"param-kwrest")
|
||||
|
||||
(rb-test "method def block param"
|
||||
(get (nth (get (rb-p-first "def foo(&blk); end") :params) 0) :type)
|
||||
"param-block")
|
||||
|
||||
(rb-test "method def all param types"
|
||||
(len (get (rb-p-first "def foo(a, b=1, *c, **d, &e); end") :params))
|
||||
5)
|
||||
|
||||
(rb-test "method def singleton recv"
|
||||
(get (get (rb-p-first "def self.bar; end") :recv) :type)
|
||||
"self")
|
||||
|
||||
(rb-test "method def body"
|
||||
(len (get (rb-p-first "def foo; 1; 2; end") :body))
|
||||
2)
|
||||
|
||||
;; ── Class def ────────────────────────────────────────────────────────────────
|
||||
|
||||
(rb-test "class def type"
|
||||
(get (rb-p-first "class Foo; end") :type)
|
||||
"class-def")
|
||||
|
||||
(rb-test "class def name"
|
||||
(get (get (rb-p-first "class Foo; end") :name) :name)
|
||||
"Foo")
|
||||
|
||||
(rb-test "class def no super"
|
||||
(nil? (get (rb-p-first "class Foo; end") :super))
|
||||
true)
|
||||
|
||||
(rb-test "class def with super"
|
||||
(get (get (rb-p-first "class Foo < Bar; end") :super) :name)
|
||||
"Bar")
|
||||
|
||||
(rb-test "singleton class"
|
||||
(get (rb-p-first "class << self; end") :type)
|
||||
"sclass")
|
||||
|
||||
;; ── Module def ────────────────────────────────────────────────────────────────
|
||||
|
||||
(rb-test "module def type"
|
||||
(get (rb-p-first "module M; end") :type)
|
||||
"module-def")
|
||||
|
||||
(rb-test "module def name"
|
||||
(get (get (rb-p-first "module M; end") :name) :name)
|
||||
"M")
|
||||
|
||||
;; ── Blocks ────────────────────────────────────────────────────────────────────
|
||||
|
||||
(rb-test "block do...end"
|
||||
(get (get (rb-p-first "foo do |x| x end") :block) :type)
|
||||
"block")
|
||||
|
||||
(rb-test "block brace"
|
||||
(get (get (rb-p-first "foo { |x| x }") :block) :type)
|
||||
"block")
|
||||
|
||||
(rb-test "block params"
|
||||
(len (get (get (rb-p-first "foo { |a, b| a }") :block) :params))
|
||||
2)
|
||||
|
||||
(rb-test "block no params"
|
||||
(len (get (get (rb-p-first "foo { 42 }") :block) :params))
|
||||
0)
|
||||
|
||||
;; ── Control flow ──────────────────────────────────────────────────────────────
|
||||
|
||||
(rb-test "return type"
|
||||
(get (rb-p-first "return 1") :type)
|
||||
"return")
|
||||
|
||||
(rb-test "return value"
|
||||
(get (get (rb-p-first "return 1") :value) :value)
|
||||
1)
|
||||
|
||||
(rb-test "return nil"
|
||||
(nil? (get (rb-p-first "return") :value))
|
||||
true)
|
||||
|
||||
(rb-test "yield type"
|
||||
(get (rb-p-first "yield 1") :type)
|
||||
"yield")
|
||||
|
||||
(rb-test "break type"
|
||||
(get (rb-p-first "break") :type)
|
||||
"break")
|
||||
|
||||
(rb-test "next type"
|
||||
(get (rb-p-first "next") :type)
|
||||
"next")
|
||||
|
||||
(rb-test "redo type"
|
||||
(get (rb-p-first "redo") :type)
|
||||
"redo")
|
||||
|
||||
;; ── Multi-statement program ───────────────────────────────────────────────────
|
||||
|
||||
(rb-test "two statements"
|
||||
(len (rb-p-stmts "1\n2"))
|
||||
2)
|
||||
|
||||
(rb-test "semi-separated"
|
||||
(len (rb-p-stmts "1; 2; 3"))
|
||||
3)
|
||||
|
||||
(rb-test "class with method"
|
||||
(let ((cls (rb-p-first "class Foo\n def bar\n 1\n end\nend")))
|
||||
(len (get cls :body)))
|
||||
1)
|
||||
|
||||
(list rb-test-pass rb-test-fail)
|
||||
210
lib/ruby/tests/tokenizer.sx
Normal file
210
lib/ruby/tests/tokenizer.sx
Normal file
@@ -0,0 +1,210 @@
|
||||
;; Ruby tokenizer tests.
|
||||
;; Final value: {:pass N :fail N :fails (list)}
|
||||
|
||||
(define rb-deep=?
|
||||
(fn (a b)
|
||||
(cond
|
||||
((= a b) true)
|
||||
((and (dict? a) (dict? b))
|
||||
(let ((ak (keys a)) (bk (keys b)))
|
||||
(if (not (= (len ak) (len bk)))
|
||||
false
|
||||
(every?
|
||||
(fn (k) (and (has-key? b k) (rb-deep=? (get a k) (get b k))))
|
||||
ak))))
|
||||
((and (list? a) (list? b))
|
||||
(if (not (= (len a) (len b)))
|
||||
false
|
||||
(let ((i 0) (ok true))
|
||||
(define rb-de-loop
|
||||
(fn ()
|
||||
(when (and ok (< i (len a)))
|
||||
(do
|
||||
(when (not (rb-deep=? (nth a i) (nth b i)))
|
||||
(set! ok false))
|
||||
(set! i (+ i 1))
|
||||
(rb-de-loop)))))
|
||||
(rb-de-loop)
|
||||
ok)))
|
||||
(:else false))))
|
||||
|
||||
(define rb-test-pass 0)
|
||||
(define rb-test-fail 0)
|
||||
(define rb-test-fails (list))
|
||||
|
||||
(define rb-test
|
||||
(fn (name actual expected)
|
||||
(if (rb-deep=? actual expected)
|
||||
(set! rb-test-pass (+ rb-test-pass 1))
|
||||
(do
|
||||
(set! rb-test-fail (+ rb-test-fail 1))
|
||||
(append! rb-test-fails {:name name :actual actual :expected expected})))))
|
||||
|
||||
;; Helper: tokenize, drop newline+eof, return {:type :value} pairs
|
||||
(define rb-toks
|
||||
(fn (src)
|
||||
(map
|
||||
(fn (tok) {:value (get tok "value") :type (get tok "type")})
|
||||
(filter
|
||||
(fn (tok)
|
||||
(let ((ty (get tok "type")))
|
||||
(not (or (= ty "newline") (= ty "eof")))))
|
||||
(rb-tokenize src)))))
|
||||
|
||||
;; Helper: get just types
|
||||
(define rb-types
|
||||
(fn (src) (map (fn (t) (get t "type")) (rb-toks src))))
|
||||
|
||||
;; Helper: get first token type
|
||||
(define rb-first-type
|
||||
(fn (src) (get (get (rb-tokenize src) 0) "type")))
|
||||
|
||||
(define rb-first-value
|
||||
(fn (src) (get (get (rb-tokenize src) 0) "value")))
|
||||
|
||||
;; ── 1. Keywords ────────────────────────<E29480><E29480><EFBFBD>─────────────────────────
|
||||
(rb-test "keyword def" (rb-toks "def") (list {:value "def" :type "keyword"}))
|
||||
(rb-test "keyword end" (rb-toks "end") (list {:value "end" :type "keyword"}))
|
||||
(rb-test "keyword class" (rb-toks "class") (list {:value "class" :type "keyword"}))
|
||||
(rb-test "keyword if" (rb-toks "if") (list {:value "if" :type "keyword"}))
|
||||
(rb-test "keyword while" (rb-toks "while") (list {:value "while" :type "keyword"}))
|
||||
(rb-test "keyword nil" (rb-toks "nil") (list {:value "nil" :type "keyword"}))
|
||||
(rb-test "keyword true" (rb-toks "true") (list {:value "true" :type "keyword"}))
|
||||
(rb-test "keyword false" (rb-toks "false") (list {:value "false" :type "keyword"}))
|
||||
(rb-test "keyword return" (rb-toks "return") (list {:value "return" :type "keyword"}))
|
||||
(rb-test "keyword yield" (rb-toks "yield") (list {:value "yield" :type "keyword"}))
|
||||
(rb-test "keyword begin" (rb-toks "begin") (list {:value "begin" :type "keyword"}))
|
||||
(rb-test "keyword rescue" (rb-toks "rescue") (list {:value "rescue" :type "keyword"}))
|
||||
(rb-test "keyword self" (rb-toks "self") (list {:value "self" :type "keyword"}))
|
||||
(rb-test "keyword super" (rb-toks "super") (list {:value "super" :type "keyword"}))
|
||||
|
||||
;; ── 2. Identifiers ────────────────────────────────────────────────
|
||||
(rb-test "ident simple" (rb-toks "foo") (list {:value "foo" :type "ident"}))
|
||||
(rb-test "ident underscore" (rb-toks "_foo") (list {:value "_foo" :type "ident"}))
|
||||
(rb-test "ident with digit" (rb-toks "foo2") (list {:value "foo2" :type "ident"}))
|
||||
(rb-test "ident predicate" (rb-toks "empty?") (list {:value "empty?" :type "ident"}))
|
||||
(rb-test "ident bang" (rb-toks "save!") (list {:value "save!" :type "ident"}))
|
||||
(rb-test "defined?" (rb-toks "defined?") (list {:value "defined?" :type "keyword"}))
|
||||
|
||||
;; ── 3. Constants ──────────────────────────────────────────────────
|
||||
(rb-test "const simple" (rb-toks "Foo") (list {:value "Foo" :type "const"}))
|
||||
(rb-test "const upcase" (rb-toks "MY_CONST") (list {:value "MY_CONST" :type "const"}))
|
||||
(rb-test "const class" (rb-toks "String") (list {:value "String" :type "const"}))
|
||||
|
||||
;; ── 4. Sigil variables ───────────────────────────────────────────
|
||||
(rb-test "ivar" (rb-toks "@name") (list {:value "@name" :type "ivar"}))
|
||||
(rb-test "cvar" (rb-toks "@@count") (list {:value "@@count" :type "cvar"}))
|
||||
(rb-test "gvar" (rb-toks "$global") (list {:value "$global" :type "gvar"}))
|
||||
|
||||
;; ── 5. Integers ───────────────────────────────────────────────────
|
||||
(rb-test "int decimal" (rb-first-value "42") 42)
|
||||
(rb-test "int zero" (rb-first-value "0") 0)
|
||||
(rb-test "int underscore" (rb-first-value "1_000") 1000)
|
||||
(rb-test "int hex" (rb-first-value "0xFF") 255)
|
||||
(rb-test "int hex lower" (rb-first-value "0xff") 255)
|
||||
(rb-test "int octal" (rb-first-value "0o17") 15)
|
||||
(rb-test "int binary" (rb-first-value "0b1010") 10)
|
||||
(rb-test "int type" (rb-first-type "42") "int")
|
||||
|
||||
;; ── 6. Floats ─────────────────────────────────────────────────────
|
||||
(rb-test "float simple" (rb-first-type "3.14") "float")
|
||||
(rb-test "float value" (rb-first-value "3.14") "3.14")
|
||||
(rb-test "float exp" (rb-first-type "1.5e10") "float")
|
||||
(rb-test "float exp value" (rb-first-value "1.5e10") "1.5e10")
|
||||
|
||||
;; ── 7. Strings ────────────────────────────────────────────────────
|
||||
(rb-test "dq string" (rb-first-value "\"hello\"") "hello")
|
||||
(rb-test "dq string type" (rb-first-type "\"hello\"") "string")
|
||||
(rb-test "sq string" (rb-first-value "'world'") "world")
|
||||
(rb-test "dq escape nl" (rb-first-value "\"a\\nb\"") "a\nb")
|
||||
(rb-test "dq escape tab" (rb-first-value "\"a\\tb\"") "a\tb")
|
||||
(rb-test "dq escape quote" (rb-first-value "\"a\\\"b\"") "a\"b")
|
||||
(rb-test "sq no escape" (rb-first-value "'a\\nb'") "a\\nb")
|
||||
(rb-test "sq escape backslash" (rb-first-value "'a\\\\'") "a\\")
|
||||
(rb-test "dq interp kept" (rb-first-value "\"#{x}\"") "#{x}")
|
||||
|
||||
;; ── 8. Symbols ────────────────────────────────────────────────────
|
||||
(rb-test "symbol simple" (rb-first-type ":foo") "symbol")
|
||||
(rb-test "symbol value" (rb-first-value ":foo") "foo")
|
||||
(rb-test "symbol predicate" (rb-first-value ":empty?") "empty?")
|
||||
(rb-test "symbol dq" (rb-first-value ":\"hello world\"") "hello world")
|
||||
(rb-test "symbol sq" (rb-first-value ":'hello'") "hello")
|
||||
|
||||
;; ── 9. %w and %i literals ────────────────────────────────────────
|
||||
(rb-test "%w bracket" (rb-first-type "%w[a b c]") "words")
|
||||
(rb-test "%w value" (rb-first-value "%w[a b c]") (list "a" "b" "c"))
|
||||
(rb-test "%w paren" (rb-first-value "%w(x y)") (list "x" "y"))
|
||||
(rb-test "%i bracket" (rb-first-type "%i[a b]") "isymbols")
|
||||
(rb-test "%i value" (rb-first-value "%i[foo bar]") (list "foo" "bar"))
|
||||
|
||||
;; ── 10. Punctuation ───────────────────────────────────────────────
|
||||
(rb-test "dot" (rb-first-type ".") "dot")
|
||||
(rb-test "dotdot" (rb-first-type "..") "dotdot")
|
||||
(rb-test "dotdotdot" (rb-first-type "...") "dotdotdot")
|
||||
(rb-test "dcolon" (rb-first-type "::") "dcolon")
|
||||
(rb-test "comma" (rb-first-type ",") "comma")
|
||||
(rb-test "semi" (rb-first-type ";") "semi")
|
||||
(rb-test "lparen" (rb-first-type "(") "lparen")
|
||||
(rb-test "rparen" (rb-first-type ")") "rparen")
|
||||
(rb-test "lbracket" (rb-first-type "[") "lbracket")
|
||||
(rb-test "rbracket" (rb-first-type "]") "rbracket")
|
||||
(rb-test "lbrace" (rb-first-type "{") "lbrace")
|
||||
(rb-test "rbrace" (rb-first-type "}") "rbrace")
|
||||
(rb-test "pipe" (rb-first-type "|") "pipe")
|
||||
|
||||
;; ── 11. Operators ─────────────────────────────────────────────────
|
||||
(rb-test "op plus" (rb-first-value "+") "+")
|
||||
(rb-test "op minus" (rb-first-value "-") "-")
|
||||
(rb-test "op star" (rb-first-value "*") "*")
|
||||
(rb-test "op slash" (rb-first-value "/") "/")
|
||||
(rb-test "op eq" (rb-first-value "=") "=")
|
||||
(rb-test "op eqeq" (rb-first-value "==") "==")
|
||||
(rb-test "op neq" (rb-first-value "!=") "!=")
|
||||
(rb-test "op lt" (rb-first-value "<") "<")
|
||||
(rb-test "op gt" (rb-first-value ">") ">")
|
||||
(rb-test "op lte" (rb-first-value "<=") "<=")
|
||||
(rb-test "op gte" (rb-first-value ">=") ">=")
|
||||
(rb-test "op spaceship" (rb-first-value "<=>") "<=>")
|
||||
(rb-test "op tripleq" (rb-first-value "===") "===")
|
||||
(rb-test "op match" (rb-first-value "=~") "=~")
|
||||
(rb-test "op nomatch" (rb-first-value "!~") "!~")
|
||||
(rb-test "op lshift" (rb-first-value "<<") "<<")
|
||||
(rb-test "op rshift" (rb-first-value ">>") ">>")
|
||||
(rb-test "op and" (rb-first-value "&&") "&&")
|
||||
(rb-test "op or" (rb-first-value "||") "||")
|
||||
(rb-test "op power" (rb-first-value "**") "**")
|
||||
(rb-test "op plus-eq" (rb-first-value "+=") "+=")
|
||||
(rb-test "op minus-eq" (rb-first-value "-=") "-=")
|
||||
(rb-test "op arrow" (rb-first-value "->") "->")
|
||||
(rb-test "op hash-rocket" (rb-first-value "=>") "=>")
|
||||
|
||||
;; ── 12. Comments ──────────────────────────────────────────────────
|
||||
(rb-test "comment skipped" (len (rb-toks "# this is a comment")) 0)
|
||||
(rb-test "comment mid-line" (rb-types "x = 1 # comment") (list "ident" "op" "int"))
|
||||
|
||||
;; ── 13. Multi-token sequences ─────────────────────────────────────
|
||||
(rb-test "method call" (rb-types "foo.bar")
|
||||
(list "ident" "dot" "ident"))
|
||||
(rb-test "class def" (rb-types "class Foo")
|
||||
(list "keyword" "const"))
|
||||
(rb-test "method def" (rb-types "def greet(name)")
|
||||
(list "keyword" "ident" "lparen" "ident" "rparen"))
|
||||
(rb-test "assignment" (rb-types "x = 42")
|
||||
(list "ident" "op" "int"))
|
||||
(rb-test "block params" (rb-types "|x, y|")
|
||||
(list "pipe" "ident" "comma" "ident" "pipe"))
|
||||
(rb-test "scope resolution" (rb-types "Foo::Bar")
|
||||
(list "const" "dcolon" "const"))
|
||||
(rb-test "range" (rb-types "1..10")
|
||||
(list "int" "dotdot" "int"))
|
||||
(rb-test "exclusive range" (rb-types "1...10")
|
||||
(list "int" "dotdotdot" "int"))
|
||||
|
||||
;; ── 14. Line/col tracking ────────────────────────────────────────
|
||||
(define rb-tok1 (get (rb-tokenize "hello\nworld") 0))
|
||||
(define rb-tok2 (get (rb-tokenize "hello\nworld") 2))
|
||||
(rb-test "line track start" (get rb-tok1 "line") 1)
|
||||
(rb-test "line track second" (get rb-tok2 "line") 2)
|
||||
(rb-test "col track start" (get rb-tok1 "col") 1)
|
||||
|
||||
(list rb-test-pass rb-test-fail)
|
||||
549
lib/ruby/tokenizer.sx
Normal file
549
lib/ruby/tokenizer.sx
Normal file
@@ -0,0 +1,549 @@
|
||||
;; Ruby tokenizer for Ruby 2.7 subset.
|
||||
;; Token: {:type T :value V :line L :col C}
|
||||
;;
|
||||
;; Types: keyword ident ivar cvar gvar const
|
||||
;; int float string symbol
|
||||
;; op dot dotdot dotdotdot dcolon colon
|
||||
;; lparen rparen lbracket rbracket lbrace rbrace
|
||||
;; comma semi pipe newline words isymbols eof
|
||||
|
||||
;; ── Character code table ──────────────────────────────────────────
|
||||
(define rb-ord-table
|
||||
(let ((t (dict)) (i 0))
|
||||
(define rb-build-table
|
||||
(fn ()
|
||||
(when (< i 128)
|
||||
(do
|
||||
(dict-set! t (char-from-code i) i)
|
||||
(set! i (+ i 1))
|
||||
(rb-build-table)))))
|
||||
(rb-build-table)
|
||||
t))
|
||||
|
||||
(define rb-ord (fn (c) (or (get rb-ord-table c) 0)))
|
||||
|
||||
;; ── Character predicates ──────────────────────────────────────────
|
||||
(define rb-digit?
|
||||
(fn (c) (and (string? c) (>= (rb-ord c) 48) (<= (rb-ord c) 57))))
|
||||
|
||||
(define rb-hex-digit?
|
||||
(fn (c)
|
||||
(and (string? c)
|
||||
(or (and (>= (rb-ord c) 48) (<= (rb-ord c) 57))
|
||||
(and (>= (rb-ord c) 97) (<= (rb-ord c) 102))
|
||||
(and (>= (rb-ord c) 65) (<= (rb-ord c) 70))))))
|
||||
|
||||
(define rb-octal-digit?
|
||||
(fn (c) (and (string? c) (>= (rb-ord c) 48) (<= (rb-ord c) 55))))
|
||||
|
||||
(define rb-binary-digit? (fn (c) (or (= c "0") (= c "1"))))
|
||||
|
||||
(define rb-lower?
|
||||
(fn (c) (and (string? c) (>= (rb-ord c) 97) (<= (rb-ord c) 122))))
|
||||
|
||||
(define rb-upper?
|
||||
(fn (c) (and (string? c) (>= (rb-ord c) 65) (<= (rb-ord c) 90))))
|
||||
|
||||
(define rb-ident-start?
|
||||
(fn (c) (or (rb-lower? c) (rb-upper? c) (= c "_"))))
|
||||
|
||||
(define rb-ident-cont?
|
||||
(fn (c) (or (rb-lower? c) (rb-upper? c) (rb-digit? c) (= c "_"))))
|
||||
|
||||
(define rb-space? (fn (c) (or (= c " ") (= c "\t") (= c "\r"))))
|
||||
|
||||
;; ── Reserved words ────────────────────────────────────────────────
|
||||
(define rb-keywords
|
||||
(list "__ENCODING__" "__LINE__" "__FILE__"
|
||||
"BEGIN" "END"
|
||||
"alias" "and"
|
||||
"begin" "break"
|
||||
"case" "class"
|
||||
"def" "defined?" "do"
|
||||
"else" "elsif" "end" "ensure"
|
||||
"false" "for"
|
||||
"if" "in"
|
||||
"module"
|
||||
"next" "nil" "not"
|
||||
"or"
|
||||
"redo" "rescue" "retry" "return"
|
||||
"self" "super"
|
||||
"then" "true"
|
||||
"undef" "unless" "until"
|
||||
"when" "while"
|
||||
"yield"))
|
||||
|
||||
(define rb-keyword? (fn (w) (contains? rb-keywords w)))
|
||||
|
||||
;; ── Token constructor ─────────────────────────────────────────────
|
||||
(define rb-make-token
|
||||
(fn (type value line col) {:type type :value value :line line :col col}))
|
||||
|
||||
;; ── Radix number parser ───────────────────────────────────────────
|
||||
(define rb-parse-radix
|
||||
(fn (s radix)
|
||||
(let ((n (len s)) (i 0) (acc 0))
|
||||
(define rb-rad-loop
|
||||
(fn ()
|
||||
(when (< i n)
|
||||
(do
|
||||
(let ((c (substring s i (+ i 1))))
|
||||
(cond
|
||||
((and (>= (rb-ord c) 48) (<= (rb-ord c) 57))
|
||||
(set! acc (+ (* acc radix) (- (rb-ord c) 48))))
|
||||
((and (>= (rb-ord c) 97) (<= (rb-ord c) 102))
|
||||
(set! acc (+ (* acc radix) (+ 10 (- (rb-ord c) 97)))))
|
||||
((and (>= (rb-ord c) 65) (<= (rb-ord c) 70))
|
||||
(set! acc (+ (* acc radix) (+ 10 (- (rb-ord c) 65)))))))
|
||||
(set! i (+ i 1))
|
||||
(rb-rad-loop)))))
|
||||
(rb-rad-loop)
|
||||
acc)))
|
||||
|
||||
;; ── Strip underscores from numeric literals ───────────────────────
|
||||
(define rb-strip-underscores
|
||||
(fn (s)
|
||||
(let ((n (len s)) (i 0) (parts (list)))
|
||||
(define rb-su-loop
|
||||
(fn ()
|
||||
(when (< i n)
|
||||
(do
|
||||
(let ((c (substring s i (+ i 1))))
|
||||
(when (not (= c "_"))
|
||||
(append! parts c)))
|
||||
(set! i (+ i 1))
|
||||
(rb-su-loop)))))
|
||||
(rb-su-loop)
|
||||
(join "" parts))))
|
||||
|
||||
;; ── Main tokenizer ────────────────────────────────────────────────
|
||||
(define rb-tokenize
|
||||
(fn (src)
|
||||
(let ((tokens (list))
|
||||
(pos 0)
|
||||
(line 1)
|
||||
(col 1)
|
||||
(src-len (len src)))
|
||||
(define rb-peek
|
||||
(fn (offset)
|
||||
(if (< (+ pos offset) src-len)
|
||||
(substring src (+ pos offset) (+ pos offset 1))
|
||||
nil)))
|
||||
(define rb-cur (fn () (rb-peek 0)))
|
||||
(define rb-advance!
|
||||
(fn ()
|
||||
(let ((c (rb-cur)))
|
||||
(set! pos (+ pos 1))
|
||||
(if (= c "\n")
|
||||
(do (set! line (+ line 1)) (set! col 1))
|
||||
(set! col (+ col 1))))))
|
||||
(define rb-advance-n!
|
||||
(fn (n)
|
||||
(when (> n 0)
|
||||
(do (rb-advance!) (rb-advance-n! (- n 1))))))
|
||||
(define rb-push!
|
||||
(fn (type value tok-line tok-col)
|
||||
(append! tokens (rb-make-token type value tok-line tok-col))))
|
||||
(define rb-read-while
|
||||
(fn (pred)
|
||||
(let ((start pos))
|
||||
(define rb-rw-loop
|
||||
(fn ()
|
||||
(when (and (< pos src-len) (pred (rb-cur)))
|
||||
(do (rb-advance!) (rb-rw-loop)))))
|
||||
(rb-rw-loop)
|
||||
(substring src start pos))))
|
||||
(define rb-skip-line-comment!
|
||||
(fn ()
|
||||
(define rb-slc-loop
|
||||
(fn ()
|
||||
(when (and (< pos src-len) (not (= (rb-cur) "\n")))
|
||||
(do (rb-advance!) (rb-slc-loop)))))
|
||||
(rb-slc-loop)))
|
||||
(define rb-read-escape
|
||||
(fn ()
|
||||
(rb-advance!)
|
||||
(let ((c (rb-cur)))
|
||||
(cond
|
||||
((= c "n") (do (rb-advance!) "\n"))
|
||||
((= c "t") (do (rb-advance!) "\t"))
|
||||
((= c "r") (do (rb-advance!) "\r"))
|
||||
((= c "\\") (do (rb-advance!) "\\"))
|
||||
((= c "'") (do (rb-advance!) "'"))
|
||||
((= c "\"") (do (rb-advance!) "\""))
|
||||
((= c "a") (do (rb-advance!) (char-from-code 7)))
|
||||
((= c "b") (do (rb-advance!) (char-from-code 8)))
|
||||
((= c "f") (do (rb-advance!) (char-from-code 12)))
|
||||
((= c "v") (do (rb-advance!) (char-from-code 11)))
|
||||
((= c "e") (do (rb-advance!) (char-from-code 27)))
|
||||
((= c "s") (do (rb-advance!) " "))
|
||||
((= c "0") (do (rb-advance!) (char-from-code 0)))
|
||||
(:else (do (rb-advance!) (str "\\" c)))))))
|
||||
(define rb-read-sq-string
|
||||
(fn ()
|
||||
(let ((parts (list)))
|
||||
(rb-advance!)
|
||||
(define rb-sq-loop
|
||||
(fn ()
|
||||
(cond
|
||||
((>= pos src-len) nil)
|
||||
((= (rb-cur) "'") (rb-advance!))
|
||||
((and (= (rb-cur) "\\")
|
||||
(let ((n (rb-peek 1)))
|
||||
(or (= n "\\") (= n "'"))))
|
||||
(do
|
||||
(rb-advance!)
|
||||
(append! parts (rb-cur))
|
||||
(rb-advance!)
|
||||
(rb-sq-loop)))
|
||||
(:else
|
||||
(do
|
||||
(append! parts (rb-cur))
|
||||
(rb-advance!)
|
||||
(rb-sq-loop))))))
|
||||
(rb-sq-loop)
|
||||
(join "" parts))))
|
||||
(define rb-read-dq-string
|
||||
(fn ()
|
||||
(let ((parts (list)))
|
||||
(rb-advance!)
|
||||
(define rb-dq-loop
|
||||
(fn ()
|
||||
(cond
|
||||
((>= pos src-len) nil)
|
||||
((= (rb-cur) "\"") (rb-advance!))
|
||||
((= (rb-cur) "\\")
|
||||
(do
|
||||
(append! parts (rb-read-escape))
|
||||
(rb-dq-loop)))
|
||||
((and (= (rb-cur) "#") (= (rb-peek 1) "{"))
|
||||
(do
|
||||
(append! parts "#{")
|
||||
(rb-advance-n! 2)
|
||||
(let ((depth 1))
|
||||
(define rb-interp-inner
|
||||
(fn ()
|
||||
(when (and (< pos src-len) (> depth 0))
|
||||
(do
|
||||
(let ((c (rb-cur)))
|
||||
(cond
|
||||
((= c "{")
|
||||
(do
|
||||
(set! depth (+ depth 1))
|
||||
(append! parts c)
|
||||
(rb-advance!)))
|
||||
((= c "}")
|
||||
(do
|
||||
(set! depth (- depth 1))
|
||||
(when (> depth 0)
|
||||
(do (append! parts c) (rb-advance!)))))
|
||||
(:else
|
||||
(do (append! parts c) (rb-advance!)))))
|
||||
(rb-interp-inner)))))
|
||||
(rb-interp-inner))
|
||||
(when (= (rb-cur) "}")
|
||||
(do (append! parts "}") (rb-advance!)))
|
||||
(rb-dq-loop)))
|
||||
(:else
|
||||
(do
|
||||
(append! parts (rb-cur))
|
||||
(rb-advance!)
|
||||
(rb-dq-loop))))))
|
||||
(rb-dq-loop)
|
||||
(join "" parts))))
|
||||
(define rb-read-percent-words
|
||||
(fn ()
|
||||
(rb-advance-n! 2)
|
||||
(let ((open-ch (rb-cur)))
|
||||
(let ((close-ch
|
||||
(cond
|
||||
((= open-ch "[") "]")
|
||||
((= open-ch "(") ")")
|
||||
((= open-ch "{") "}")
|
||||
((= open-ch "<") ">")
|
||||
(:else open-ch))))
|
||||
(rb-advance!)
|
||||
(let ((items (list)))
|
||||
(define rb-pw-skip
|
||||
(fn ()
|
||||
(when (and (< pos src-len) (or (rb-space? (rb-cur)) (= (rb-cur) "\n")))
|
||||
(do (rb-advance!) (rb-pw-skip)))))
|
||||
(define rb-pw-word
|
||||
(fn (wparts)
|
||||
(if (or (>= pos src-len)
|
||||
(rb-space? (rb-cur))
|
||||
(= (rb-cur) "\n")
|
||||
(= (rb-cur) close-ch))
|
||||
(append! items (join "" wparts))
|
||||
(do
|
||||
(append! wparts (rb-cur))
|
||||
(rb-advance!)
|
||||
(rb-pw-word wparts)))))
|
||||
(define rb-pw-loop
|
||||
(fn ()
|
||||
(rb-pw-skip)
|
||||
(when (and (< pos src-len) (not (= (rb-cur) close-ch)))
|
||||
(do
|
||||
(rb-pw-word (list))
|
||||
(rb-pw-loop)))))
|
||||
(rb-pw-loop)
|
||||
(when (= (rb-cur) close-ch) (rb-advance!))
|
||||
items)))))
|
||||
(define rb-read-ident-word
|
||||
(fn ()
|
||||
(let ((start pos))
|
||||
(rb-read-while rb-ident-cont?)
|
||||
(when (and (= (rb-cur) "?") (not (= (rb-peek 1) "=")))
|
||||
(rb-advance!))
|
||||
(when (and (= (rb-cur) "!") (not (or (= (rb-peek 1) "=") (= (rb-peek 1) "~"))))
|
||||
(rb-advance!))
|
||||
(substring src start pos))))
|
||||
(define rb-read-number!
|
||||
(fn (tok-line tok-col)
|
||||
(let ((start pos))
|
||||
(cond
|
||||
((and (= (rb-cur) "0") (let ((p (rb-peek 1))) (or (= p "b") (= p "B"))))
|
||||
(do
|
||||
(rb-advance-n! 2)
|
||||
(let ((bin-str (rb-read-while rb-binary-digit?)))
|
||||
(rb-push! "int" (rb-parse-radix bin-str 2) tok-line tok-col))))
|
||||
((and (= (rb-cur) "0") (let ((p (rb-peek 1))) (or (= p "o") (= p "O"))))
|
||||
(do
|
||||
(rb-advance-n! 2)
|
||||
(let ((oct-str (rb-read-while rb-octal-digit?)))
|
||||
(rb-push! "int" (rb-parse-radix oct-str 8) tok-line tok-col))))
|
||||
((and (= (rb-cur) "0") (let ((p (rb-peek 1))) (or (= p "x") (= p "X"))))
|
||||
(do
|
||||
(rb-advance-n! 2)
|
||||
(let ((hex-str (rb-read-while rb-hex-digit?)))
|
||||
(rb-push! "int" (rb-parse-radix hex-str 16) tok-line tok-col))))
|
||||
(:else
|
||||
(do
|
||||
(rb-read-while (fn (c) (or (rb-digit? c) (= c "_"))))
|
||||
(let ((is-float false))
|
||||
(when (and (= (rb-cur) ".") (rb-digit? (rb-peek 1)))
|
||||
(do
|
||||
(set! is-float true)
|
||||
(rb-advance!)
|
||||
(rb-read-while (fn (c) (or (rb-digit? c) (= c "_"))))))
|
||||
(when (or (= (rb-cur) "e") (= (rb-cur) "E"))
|
||||
(do
|
||||
(set! is-float true)
|
||||
(rb-advance!)
|
||||
(when (or (= (rb-cur) "+") (= (rb-cur) "-"))
|
||||
(rb-advance!))
|
||||
(rb-read-while rb-digit?)))
|
||||
(let ((num-str (rb-strip-underscores (substring src start pos))))
|
||||
(if is-float
|
||||
(rb-push! "float" num-str tok-line tok-col)
|
||||
(rb-push! "int" (parse-int num-str) tok-line tok-col))))))))))
|
||||
(define rb-read-op!
|
||||
(fn (tok-line tok-col)
|
||||
(let ((c0 (rb-cur)) (c1 (rb-peek 1)) (c2 (rb-peek 2)))
|
||||
(cond
|
||||
((and (= c0 "<") (= c1 "=") (= c2 ">"))
|
||||
(do (rb-advance-n! 3) (rb-push! "op" "<=>" tok-line tok-col)))
|
||||
((and (= c0 "=") (= c1 "=") (= c2 "="))
|
||||
(do (rb-advance-n! 3) (rb-push! "op" "===" tok-line tok-col)))
|
||||
((and (= c0 "*") (= c1 "*") (= c2 "="))
|
||||
(do (rb-advance-n! 3) (rb-push! "op" "**=" tok-line tok-col)))
|
||||
((and (= c0 "<") (= c1 "<") (= c2 "="))
|
||||
(do (rb-advance-n! 3) (rb-push! "op" "<<=" tok-line tok-col)))
|
||||
((and (= c0 ">") (= c1 ">") (= c2 "="))
|
||||
(do (rb-advance-n! 3) (rb-push! "op" ">>=" tok-line tok-col)))
|
||||
((and (= c0 "&") (= c1 "&") (= c2 "="))
|
||||
(do (rb-advance-n! 3) (rb-push! "op" "&&=" tok-line tok-col)))
|
||||
((and (= c0 "|") (= c1 "|") (= c2 "="))
|
||||
(do (rb-advance-n! 3) (rb-push! "op" "||=" tok-line tok-col)))
|
||||
((and (= c0 "*") (= c1 "*"))
|
||||
(do (rb-advance-n! 2) (rb-push! "op" "**" tok-line tok-col)))
|
||||
((and (= c0 "=") (= c1 "="))
|
||||
(do (rb-advance-n! 2) (rb-push! "op" "==" tok-line tok-col)))
|
||||
((and (= c0 "!") (= c1 "="))
|
||||
(do (rb-advance-n! 2) (rb-push! "op" "!=" tok-line tok-col)))
|
||||
((and (= c0 "<") (= c1 "="))
|
||||
(do (rb-advance-n! 2) (rb-push! "op" "<=" tok-line tok-col)))
|
||||
((and (= c0 ">") (= c1 "="))
|
||||
(do (rb-advance-n! 2) (rb-push! "op" ">=" tok-line tok-col)))
|
||||
((and (= c0 "=") (= c1 "~"))
|
||||
(do (rb-advance-n! 2) (rb-push! "op" "=~" tok-line tok-col)))
|
||||
((and (= c0 "!") (= c1 "~"))
|
||||
(do (rb-advance-n! 2) (rb-push! "op" "!~" tok-line tok-col)))
|
||||
((and (= c0 "<") (= c1 "<"))
|
||||
(do (rb-advance-n! 2) (rb-push! "op" "<<" tok-line tok-col)))
|
||||
((and (= c0 ">") (= c1 ">"))
|
||||
(do (rb-advance-n! 2) (rb-push! "op" ">>" tok-line tok-col)))
|
||||
((and (= c0 "&") (= c1 "&"))
|
||||
(do (rb-advance-n! 2) (rb-push! "op" "&&" tok-line tok-col)))
|
||||
((and (= c0 "|") (= c1 "|"))
|
||||
(do (rb-advance-n! 2) (rb-push! "op" "||" tok-line tok-col)))
|
||||
((and (= c0 "+") (= c1 "="))
|
||||
(do (rb-advance-n! 2) (rb-push! "op" "+=" tok-line tok-col)))
|
||||
((and (= c0 "-") (= c1 "="))
|
||||
(do (rb-advance-n! 2) (rb-push! "op" "-=" tok-line tok-col)))
|
||||
((and (= c0 "*") (= c1 "="))
|
||||
(do (rb-advance-n! 2) (rb-push! "op" "*=" tok-line tok-col)))
|
||||
((and (= c0 "/") (= c1 "="))
|
||||
(do (rb-advance-n! 2) (rb-push! "op" "/=" tok-line tok-col)))
|
||||
((and (= c0 "%") (= c1 "="))
|
||||
(do (rb-advance-n! 2) (rb-push! "op" "%=" tok-line tok-col)))
|
||||
((and (= c0 "&") (= c1 "="))
|
||||
(do (rb-advance-n! 2) (rb-push! "op" "&=" tok-line tok-col)))
|
||||
((and (= c0 "|") (= c1 "="))
|
||||
(do (rb-advance-n! 2) (rb-push! "op" "|=" tok-line tok-col)))
|
||||
((and (= c0 "^") (= c1 "="))
|
||||
(do (rb-advance-n! 2) (rb-push! "op" "^=" tok-line tok-col)))
|
||||
((and (= c0 "-") (= c1 ">"))
|
||||
(do (rb-advance-n! 2) (rb-push! "op" "->" tok-line tok-col)))
|
||||
((and (= c0 "=") (= c1 ">"))
|
||||
(do (rb-advance-n! 2) (rb-push! "op" "=>" tok-line tok-col)))
|
||||
((and (= c0 "|") (nil? c1))
|
||||
(do (rb-advance!) (rb-push! "pipe" "|" tok-line tok-col)))
|
||||
((= c0 "|")
|
||||
(do (rb-advance!) (rb-push! "pipe" "|" tok-line tok-col)))
|
||||
(:else
|
||||
(do (rb-advance!) (rb-push! "op" c0 tok-line tok-col)))))))
|
||||
(define rb-scan!
|
||||
(fn ()
|
||||
(cond
|
||||
((>= pos src-len) nil)
|
||||
((rb-space? (rb-cur)) (do (rb-advance!) (rb-scan!)))
|
||||
((= (rb-cur) "#") (do (rb-skip-line-comment!) (rb-scan!)))
|
||||
((= (rb-cur) "\n")
|
||||
(do
|
||||
(let ((l line) (c col))
|
||||
(rb-advance!)
|
||||
(rb-push! "newline" nil l c))
|
||||
(rb-scan!)))
|
||||
((rb-digit? (rb-cur))
|
||||
(do
|
||||
(let ((l line) (c col))
|
||||
(rb-read-number! l c))
|
||||
(rb-scan!)))
|
||||
((rb-ident-start? (rb-cur))
|
||||
(do
|
||||
(let ((l line) (c col))
|
||||
(let ((w (rb-read-ident-word)))
|
||||
(if (rb-keyword? w)
|
||||
(rb-push! "keyword" w l c)
|
||||
(if (rb-upper? (substring w 0 1))
|
||||
(rb-push! "const" w l c)
|
||||
(rb-push! "ident" w l c)))))
|
||||
(rb-scan!)))
|
||||
((= (rb-cur) "@")
|
||||
(do
|
||||
(let ((l line) (c col))
|
||||
(if (= (rb-peek 1) "@")
|
||||
(do
|
||||
(rb-advance-n! 2)
|
||||
(let ((name (rb-read-while rb-ident-cont?)))
|
||||
(rb-push! "cvar" (str "@@" name) l c)))
|
||||
(do
|
||||
(rb-advance!)
|
||||
(let ((name (rb-read-while rb-ident-cont?)))
|
||||
(rb-push! "ivar" (str "@" name) l c)))))
|
||||
(rb-scan!)))
|
||||
((= (rb-cur) "$")
|
||||
(do
|
||||
(let ((l line) (c col))
|
||||
(rb-advance!)
|
||||
(let ((name (rb-read-while rb-ident-cont?)))
|
||||
(rb-push! "gvar" (str "$" name) l c)))
|
||||
(rb-scan!)))
|
||||
((= (rb-cur) "\"")
|
||||
(do
|
||||
(let ((l line) (c col))
|
||||
(rb-push! "string" (rb-read-dq-string) l c))
|
||||
(rb-scan!)))
|
||||
((= (rb-cur) "'")
|
||||
(do
|
||||
(let ((l line) (c col))
|
||||
(rb-push! "string" (rb-read-sq-string) l c))
|
||||
(rb-scan!)))
|
||||
((and (= (rb-cur) ":") (= (rb-peek 1) ":"))
|
||||
(do
|
||||
(let ((l line) (c col))
|
||||
(rb-advance-n! 2)
|
||||
(rb-push! "dcolon" "::" l c))
|
||||
(rb-scan!)))
|
||||
((= (rb-cur) ":")
|
||||
(do
|
||||
(let ((l line) (c col))
|
||||
(rb-advance!)
|
||||
(cond
|
||||
((= (rb-cur) "\"")
|
||||
(rb-push! "symbol" (rb-read-dq-string) l c))
|
||||
((= (rb-cur) "'")
|
||||
(rb-push! "symbol" (rb-read-sq-string) l c))
|
||||
((rb-ident-start? (rb-cur))
|
||||
(let ((name (rb-read-ident-word)))
|
||||
(rb-push! "symbol" name l c)))
|
||||
(:else
|
||||
(rb-push! "colon" ":" l c))))
|
||||
(rb-scan!)))
|
||||
((and (= (rb-cur) "%")
|
||||
(let ((p (rb-peek 1)))
|
||||
(or (= p "w") (= p "W") (= p "i") (= p "I"))))
|
||||
(do
|
||||
(let ((l line) (c col))
|
||||
(let ((kind (rb-peek 1)))
|
||||
(let ((items (rb-read-percent-words)))
|
||||
(if (or (= kind "i") (= kind "I"))
|
||||
(rb-push! "isymbols" items l c)
|
||||
(rb-push! "words" items l c)))))
|
||||
(rb-scan!)))
|
||||
((= (rb-cur) ".")
|
||||
(do
|
||||
(let ((l line) (c col))
|
||||
(cond
|
||||
((and (= (rb-peek 1) ".") (= (rb-peek 2) "."))
|
||||
(do (rb-advance-n! 3) (rb-push! "dotdotdot" "..." l c)))
|
||||
((= (rb-peek 1) ".")
|
||||
(do (rb-advance-n! 2) (rb-push! "dotdot" ".." l c)))
|
||||
(:else
|
||||
(do (rb-advance!) (rb-push! "dot" "." l c)))))
|
||||
(rb-scan!)))
|
||||
((= (rb-cur) ",")
|
||||
(do
|
||||
(let ((l line) (c col)) (rb-push! "comma" "," l c) (rb-advance!))
|
||||
(rb-scan!)))
|
||||
((= (rb-cur) ";")
|
||||
(do
|
||||
(let ((l line) (c col)) (rb-push! "semi" ";" l c) (rb-advance!))
|
||||
(rb-scan!)))
|
||||
((= (rb-cur) "(")
|
||||
(do
|
||||
(let ((l line) (c col)) (rb-push! "lparen" "(" l c) (rb-advance!))
|
||||
(rb-scan!)))
|
||||
((= (rb-cur) ")")
|
||||
(do
|
||||
(let ((l line) (c col)) (rb-push! "rparen" ")" l c) (rb-advance!))
|
||||
(rb-scan!)))
|
||||
((= (rb-cur) "[")
|
||||
(do
|
||||
(let ((l line) (c col)) (rb-push! "lbracket" "[" l c) (rb-advance!))
|
||||
(rb-scan!)))
|
||||
((= (rb-cur) "]")
|
||||
(do
|
||||
(let ((l line) (c col)) (rb-push! "rbracket" "]" l c) (rb-advance!))
|
||||
(rb-scan!)))
|
||||
((= (rb-cur) "{")
|
||||
(do
|
||||
(let ((l line) (c col)) (rb-push! "lbrace" "{" l c) (rb-advance!))
|
||||
(rb-scan!)))
|
||||
((= (rb-cur) "}")
|
||||
(do
|
||||
(let ((l line) (c col)) (rb-push! "rbrace" "}" l c) (rb-advance!))
|
||||
(rb-scan!)))
|
||||
((or (= (rb-cur) "+") (= (rb-cur) "-") (= (rb-cur) "*")
|
||||
(= (rb-cur) "/") (= (rb-cur) "%") (= (rb-cur) "=")
|
||||
(= (rb-cur) "!") (= (rb-cur) "<") (= (rb-cur) ">")
|
||||
(= (rb-cur) "&") (= (rb-cur) "^") (= (rb-cur) "~")
|
||||
(= (rb-cur) "|"))
|
||||
(do
|
||||
(let ((l line) (c col)) (rb-read-op! l c))
|
||||
(rb-scan!)))
|
||||
(:else (do (rb-advance!) (rb-scan!))))))
|
||||
(rb-scan!)
|
||||
(rb-push! "eof" nil line col)
|
||||
tokens)))
|
||||
@@ -1,145 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tcl-on-SX conformance runner — epoch protocol to sx_server.exe
|
||||
# Usage: lib/tcl/conformance.sh [file.tcl ...]
|
||||
# Defaults to lib/tcl/tests/programs/*.tcl
|
||||
set -uo pipefail
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe"
|
||||
fi
|
||||
if [ ! -x "$SX_SERVER" ]; then echo "ERROR: sx_server.exe not found"; exit 1; fi
|
||||
|
||||
SCOREBOARD_JSON="${SCOREBOARD_JSON:-lib/tcl/scoreboard.json}"
|
||||
SCOREBOARD_MD="${SCOREBOARD_MD:-lib/tcl/scoreboard.md}"
|
||||
|
||||
# Collect tcl files
|
||||
if [ "$#" -gt 0 ]; then
|
||||
TCL_FILES=("$@")
|
||||
else
|
||||
TCL_FILES=(lib/tcl/tests/programs/*.tcl)
|
||||
fi
|
||||
|
||||
# Generate a helper .sx file that defines the Tcl source as an SX string variable.
|
||||
# We escape the source for SX string literals: backslashes → \\, quotes → \", newlines → \n.
|
||||
# This is safe in a (define ...) context — no double-parsing like (eval "...") would cause.
|
||||
write_sx_helper() {
|
||||
local tcl_file="$1"
|
||||
local helper_file="$2"
|
||||
python3 << PYEOF
|
||||
src = open('${tcl_file}').read()
|
||||
escaped = src.replace('\\\\', '\\\\\\\\').replace('"', '\\\\"').replace('\\n', '\\\\n')
|
||||
with open('${helper_file}', 'w') as f:
|
||||
f.write(f'(define __tcl-src "{escaped}")\\n')
|
||||
f.write('(define __tcl-result (get (tcl-eval-string (make-default-tcl-interp) __tcl-src) :result))\\n')
|
||||
PYEOF
|
||||
}
|
||||
|
||||
total=0
|
||||
passed=0
|
||||
failed=0
|
||||
programs_json=""
|
||||
md_rows=""
|
||||
|
||||
for tcl_file in "${TCL_FILES[@]}"; do
|
||||
basename_noext=$(basename "$tcl_file" .tcl)
|
||||
total=$((total + 1))
|
||||
|
||||
# Read expected value from first-line comment "# expected: VALUE"
|
||||
expected=$(head -1 "$tcl_file" | sed -n 's/^# expected: *//p')
|
||||
if [ -z "$expected" ]; then
|
||||
echo "WARN: no '# expected:' annotation in $tcl_file — skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
tmpfile=$(mktemp)
|
||||
helper=$(mktemp --suffix=.sx)
|
||||
trap "rm -f $tmpfile $helper" EXIT
|
||||
|
||||
# Write helper .sx with Tcl source embedded as SX string
|
||||
write_sx_helper "$tcl_file" "$helper"
|
||||
|
||||
# Build epoch input using quoted heredoc for static parts; helper path via variable
|
||||
cat > "$tmpfile" << EPOCHS
|
||||
(epoch 1)
|
||||
(load "lib/tcl/tokenizer.sx")
|
||||
(epoch 2)
|
||||
(load "lib/tcl/parser.sx")
|
||||
(epoch 3)
|
||||
(load "lib/tcl/runtime.sx")
|
||||
(epoch 4)
|
||||
(load "$helper")
|
||||
(epoch 5)
|
||||
(eval "__tcl-result")
|
||||
(epoch 6)
|
||||
EPOCHS
|
||||
|
||||
output=$(timeout 30 "$SX_SERVER" < "$tmpfile" 2>&1)
|
||||
got=$(echo "$output" | grep -A1 "^(ok-len 5 " | tail -1 | tr -d '"')
|
||||
|
||||
if [ "$got" = "$expected" ]; then
|
||||
status="PASS"
|
||||
passed=$((passed + 1))
|
||||
echo "PASS $basename_noext (expected: $expected, got: $got)"
|
||||
else
|
||||
status="FAIL"
|
||||
failed=$((failed + 1))
|
||||
echo "FAIL $basename_noext (expected: $expected, got: ${got:-<empty>})"
|
||||
if [ -n "${VERBOSE:-}" ]; then
|
||||
echo "--- server output ---"
|
||||
echo "$output"
|
||||
echo "--- helper.sx ---"
|
||||
cat "$helper"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Accumulate JSON fragment (escape for JSON)
|
||||
got_json=$(printf '%s' "$got" | python3 -c "import sys,json; sys.stdout.write(json.dumps(sys.stdin.read()))" | tr -d '"')
|
||||
exp_json=$(printf '%s' "$expected" | python3 -c "import sys,json; sys.stdout.write(json.dumps(sys.stdin.read()))" | tr -d '"')
|
||||
|
||||
if [ -n "$programs_json" ]; then
|
||||
programs_json="${programs_json},"
|
||||
fi
|
||||
programs_json="${programs_json}
|
||||
\"${basename_noext}\": {\"status\": \"${status}\", \"expected\": \"${exp_json}\", \"got\": \"${got_json}\"}"
|
||||
|
||||
# Accumulate Markdown row
|
||||
if [ "$status" = "PASS" ]; then
|
||||
icon="✓ PASS"
|
||||
else
|
||||
icon="✗ FAIL"
|
||||
fi
|
||||
md_rows="${md_rows}| ${basename_noext} | ${icon} | ${expected} | ${got} |
|
||||
"
|
||||
done
|
||||
|
||||
# Write scoreboard.json
|
||||
cat > "$SCOREBOARD_JSON" << JSON
|
||||
{
|
||||
"total": ${total},
|
||||
"passed": ${passed},
|
||||
"failed": ${failed},
|
||||
"programs": {${programs_json}
|
||||
}
|
||||
}
|
||||
JSON
|
||||
|
||||
# Write scoreboard.md
|
||||
cat > "$SCOREBOARD_MD" << MD
|
||||
# Tcl-on-SX Conformance Scoreboard
|
||||
|
||||
| Program | Status | Expected | Got |
|
||||
|---|---|---|---|
|
||||
${md_rows}
|
||||
**${passed}/${total} passing**
|
||||
MD
|
||||
|
||||
echo ""
|
||||
echo "Scoreboard: ${passed}/${total} passing"
|
||||
echo "Written: $SCOREBOARD_JSON, $SCOREBOARD_MD"
|
||||
|
||||
if [ "$failed" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
@@ -1,41 +0,0 @@
|
||||
; Tcl parser — thin layer over tcl-tokenize
|
||||
; Adds tcl-parse entry point and word utility fns
|
||||
|
||||
; Entry point: parse Tcl source to a list of commands.
|
||||
; Returns same structure as tcl-tokenize.
|
||||
(define tcl-parse (fn (src) (tcl-tokenize src)))
|
||||
|
||||
; True if word has no substitutions — value can be read statically.
|
||||
; braced words are always simple. compound words are simple when all
|
||||
; parts are plain text with no var/cmd parts.
|
||||
(define tcl-word-simple?
|
||||
(fn (word)
|
||||
(cond
|
||||
((= (get word :type) "braced") true)
|
||||
((= (get word :type) "compound")
|
||||
(let ((parts (get word :parts)))
|
||||
(every? (fn (p) (= (get p :type) "text")) parts)))
|
||||
(else false))))
|
||||
|
||||
; Concatenate text parts of a simple word into a single string.
|
||||
; For braced words returns :value directly.
|
||||
; For compound words with only text parts, joins them.
|
||||
; Returns nil for words with substitutions.
|
||||
(define tcl-word-literal
|
||||
(fn (word)
|
||||
(cond
|
||||
((= (get word :type) "braced") (get word :value))
|
||||
((= (get word :type) "compound")
|
||||
(if (tcl-word-simple? word)
|
||||
(join "" (map (fn (p) (get p :value)) (get word :parts)))
|
||||
nil))
|
||||
(else nil))))
|
||||
|
||||
; Number of words in a parsed command.
|
||||
(define tcl-cmd-len
|
||||
(fn (cmd) (len (get cmd :words))))
|
||||
|
||||
; Nth word literal from a command (index 0 = command name).
|
||||
; Returns nil if word has substitutions.
|
||||
(define tcl-nth-literal
|
||||
(fn (cmd n) (tcl-word-literal (nth (get cmd :words) n))))
|
||||
3336
lib/tcl/runtime.sx
3336
lib/tcl/runtime.sx
File diff suppressed because it is too large
Load Diff
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"total": 3,
|
||||
"passed": 3,
|
||||
"failed": 0,
|
||||
"programs": {
|
||||
"assert": {"status": "PASS", "expected": "10", "got": "10"},
|
||||
"for-each-line": {"status": "PASS", "expected": "13", "got": "13"},
|
||||
"with-temp-var": {"status": "PASS", "expected": "100 999", "got": "100 999"}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
# Tcl-on-SX Conformance Scoreboard
|
||||
|
||||
| Program | Status | Expected | Got |
|
||||
|---|---|---|---|
|
||||
| assert | ✓ PASS | 10 | 10 |
|
||||
| for-each-line | ✓ PASS | 13 | 13 |
|
||||
| with-temp-var | ✓ PASS | 100 999 | 100 999 |
|
||||
|
||||
**3/3 passing**
|
||||
114
lib/tcl/test.sh
114
lib/tcl/test.sh
@@ -1,114 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Tcl-on-SX test runner — epoch protocol to sx_server.exe
|
||||
set -uo pipefail
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe"
|
||||
fi
|
||||
if [ ! -x "$SX_SERVER" ]; then echo "ERROR: sx_server.exe not found"; exit 1; fi
|
||||
|
||||
VERBOSE="${1:-}"
|
||||
TMPFILE=$(mktemp)
|
||||
HELPER=$(mktemp --suffix=.sx)
|
||||
trap "rm -f $TMPFILE $HELPER" EXIT
|
||||
|
||||
# Helper file: run all test suites and format a parseable summary string
|
||||
cat > "$HELPER" << 'HELPER_EOF'
|
||||
(define __pr (tcl-run-parse-tests))
|
||||
(define __er (tcl-run-eval-tests))
|
||||
(define __xr (tcl-run-error-tests))
|
||||
(define __nr (tcl-run-namespace-tests))
|
||||
(define __cr (tcl-run-coro-tests))
|
||||
(define __ir (tcl-run-idiom-tests))
|
||||
(define tcl-test-summary
|
||||
(str "PARSE:" (get __pr "passed") ":" (get __pr "failed")
|
||||
" EVAL:" (get __er "passed") ":" (get __er "failed")
|
||||
" ERROR:" (get __xr "passed") ":" (get __xr "failed")
|
||||
" NAMESPACE:" (get __nr "passed") ":" (get __nr "failed")
|
||||
" CORO:" (get __cr "passed") ":" (get __cr "failed")
|
||||
" IDIOM:" (get __ir "passed") ":" (get __ir "failed")))
|
||||
HELPER_EOF
|
||||
|
||||
cat > "$TMPFILE" << EPOCHS
|
||||
(epoch 1)
|
||||
(load "lib/tcl/tokenizer.sx")
|
||||
(epoch 2)
|
||||
(load "lib/tcl/parser.sx")
|
||||
(epoch 3)
|
||||
(load "lib/tcl/tests/parse.sx")
|
||||
(epoch 4)
|
||||
(load "lib/fiber.sx")
|
||||
(load "lib/tcl/runtime.sx")
|
||||
(epoch 5)
|
||||
(load "lib/tcl/tests/eval.sx")
|
||||
(epoch 6)
|
||||
(load "lib/tcl/tests/error.sx")
|
||||
(epoch 7)
|
||||
(load "lib/tcl/tests/namespace.sx")
|
||||
(epoch 8)
|
||||
(load "lib/tcl/tests/coro.sx")
|
||||
(epoch 9)
|
||||
(load "lib/tcl/tests/idioms.sx")
|
||||
(epoch 10)
|
||||
(load "$HELPER")
|
||||
(epoch 11)
|
||||
(eval "tcl-test-summary")
|
||||
EPOCHS
|
||||
|
||||
OUTPUT=$(timeout 180 "$SX_SERVER" < "$TMPFILE" 2>&1)
|
||||
[ "$VERBOSE" = "-v" ] && echo "$OUTPUT"
|
||||
|
||||
# Extract summary line from epoch 11 output
|
||||
SUMMARY=$(echo "$OUTPUT" | grep -A1 "^(ok-len 11 " | tail -1 | tr -d '"')
|
||||
|
||||
if [ -z "$SUMMARY" ]; then
|
||||
echo "ERROR: no summary from test run"
|
||||
echo "$OUTPUT" | tail -20
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse PARSE:N:M EVAL:N:M ERROR:N:M NAMESPACE:N:M CORO:N:M IDIOM:N:M
|
||||
PARSE_PART=$(echo "$SUMMARY" | grep -o 'PARSE:[0-9]*:[0-9]*')
|
||||
EVAL_PART=$(echo "$SUMMARY" | grep -o 'EVAL:[0-9]*:[0-9]*')
|
||||
ERROR_PART=$(echo "$SUMMARY" | grep -o 'ERROR:[0-9]*:[0-9]*')
|
||||
NAMESPACE_PART=$(echo "$SUMMARY" | grep -o 'NAMESPACE:[0-9]*:[0-9]*')
|
||||
CORO_PART=$(echo "$SUMMARY" | grep -o 'CORO:[0-9]*:[0-9]*')
|
||||
IDIOM_PART=$(echo "$SUMMARY" | grep -o 'IDIOM:[0-9]*:[0-9]*')
|
||||
|
||||
PARSE_PASSED=$(echo "$PARSE_PART" | cut -d: -f2)
|
||||
PARSE_FAILED=$(echo "$PARSE_PART" | cut -d: -f3)
|
||||
EVAL_PASSED=$(echo "$EVAL_PART" | cut -d: -f2)
|
||||
EVAL_FAILED=$(echo "$EVAL_PART" | cut -d: -f3)
|
||||
ERROR_PASSED=$(echo "$ERROR_PART" | cut -d: -f2)
|
||||
ERROR_FAILED=$(echo "$ERROR_PART" | cut -d: -f3)
|
||||
NAMESPACE_PASSED=$(echo "$NAMESPACE_PART" | cut -d: -f2)
|
||||
NAMESPACE_FAILED=$(echo "$NAMESPACE_PART" | cut -d: -f3)
|
||||
CORO_PASSED=$(echo "$CORO_PART" | cut -d: -f2)
|
||||
CORO_FAILED=$(echo "$CORO_PART" | cut -d: -f3)
|
||||
IDIOM_PASSED=$(echo "$IDIOM_PART" | cut -d: -f2)
|
||||
IDIOM_FAILED=$(echo "$IDIOM_PART" | cut -d: -f3)
|
||||
|
||||
PARSE_PASSED=${PARSE_PASSED:-0}; PARSE_FAILED=${PARSE_FAILED:-1}
|
||||
EVAL_PASSED=${EVAL_PASSED:-0}; EVAL_FAILED=${EVAL_FAILED:-1}
|
||||
ERROR_PASSED=${ERROR_PASSED:-0}; ERROR_FAILED=${ERROR_FAILED:-1}
|
||||
NAMESPACE_PASSED=${NAMESPACE_PASSED:-0}; NAMESPACE_FAILED=${NAMESPACE_FAILED:-1}
|
||||
CORO_PASSED=${CORO_PASSED:-0}; CORO_FAILED=${CORO_FAILED:-1}
|
||||
IDIOM_PASSED=${IDIOM_PASSED:-0}; IDIOM_FAILED=${IDIOM_FAILED:-1}
|
||||
|
||||
TOTAL_PASSED=$((PARSE_PASSED + EVAL_PASSED + ERROR_PASSED + NAMESPACE_PASSED + CORO_PASSED + IDIOM_PASSED))
|
||||
TOTAL_FAILED=$((PARSE_FAILED + EVAL_FAILED + ERROR_FAILED + NAMESPACE_FAILED + CORO_FAILED + IDIOM_FAILED))
|
||||
TOTAL=$((TOTAL_PASSED + TOTAL_FAILED))
|
||||
|
||||
if [ "$TOTAL_FAILED" = "0" ]; then
|
||||
echo "ok $TOTAL_PASSED/$TOTAL tcl tests passed (parse: $PARSE_PASSED, eval: $EVAL_PASSED, error: $ERROR_PASSED, namespace: $NAMESPACE_PASSED, coro: $CORO_PASSED, idiom: $IDIOM_PASSED)"
|
||||
exit 0
|
||||
else
|
||||
echo "FAIL $TOTAL_PASSED/$TOTAL passed, $TOTAL_FAILED failed (parse: $PARSE_PASSED/$((PARSE_PASSED+PARSE_FAILED)), eval: $EVAL_PASSED/$((EVAL_PASSED+EVAL_FAILED)), error: $ERROR_PASSED/$((ERROR_PASSED+ERROR_FAILED)), namespace: $NAMESPACE_PASSED/$((NAMESPACE_PASSED+NAMESPACE_FAILED)), coro: $CORO_PASSED/$((CORO_PASSED+CORO_FAILED)), idiom: $IDIOM_PASSED/$((IDIOM_PASSED+IDIOM_FAILED)))"
|
||||
if [ -z "$VERBOSE" ]; then
|
||||
echo "--- output ---"
|
||||
echo "$OUTPUT" | tail -30
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,136 +0,0 @@
|
||||
; Tcl-on-SX coroutine tests (Phase 6)
|
||||
(define tcl-coro-pass 0)
|
||||
(define tcl-coro-fail 0)
|
||||
(define tcl-coro-failures (list))
|
||||
|
||||
(define
|
||||
tcl-coro-assert
|
||||
(fn
|
||||
(label expected actual)
|
||||
(if
|
||||
(equal? expected actual)
|
||||
(set! tcl-coro-pass (+ tcl-coro-pass 1))
|
||||
(begin
|
||||
(set! tcl-coro-fail (+ tcl-coro-fail 1))
|
||||
(append!
|
||||
tcl-coro-failures
|
||||
(str label ": expected=" (str expected) " got=" (str actual)))))))
|
||||
|
||||
(define
|
||||
tcl-run-coro-tests
|
||||
(fn
|
||||
()
|
||||
(set! tcl-coro-pass 0)
|
||||
(set! tcl-coro-fail 0)
|
||||
(set! tcl-coro-failures (list))
|
||||
(define interp (fn () (make-default-tcl-interp)))
|
||||
(define run (fn (src) (tcl-eval-string (interp) src)))
|
||||
(define
|
||||
ok
|
||||
(fn (label actual expected) (tcl-coro-assert label expected actual)))
|
||||
|
||||
; --- basic coroutine: yields one value ---
|
||||
(ok "coro-single-yield"
|
||||
(get (run "proc gen {} { yield hello }\ncoroutine g gen\ng") :result)
|
||||
"hello")
|
||||
|
||||
; --- coroutine yields multiple values in order ---
|
||||
(ok "coro-multi-yield-1"
|
||||
(get (run "proc cnt {} { yield a; yield b; yield c }\ncoroutine c1 cnt\nc1") :result)
|
||||
"a")
|
||||
|
||||
(ok "coro-multi-yield-2"
|
||||
(get (run "proc cnt {} { yield a; yield b; yield c }\ncoroutine c1 cnt\nc1\nc1") :result)
|
||||
"b")
|
||||
|
||||
(ok "coro-multi-yield-3"
|
||||
(get (run "proc cnt {} { yield a; yield b; yield c }\ncoroutine c1 cnt\nc1\nc1\nc1") :result)
|
||||
"c")
|
||||
|
||||
; --- coroutine with arguments to proc ---
|
||||
(ok "coro-args"
|
||||
(get (run "proc gen2 {n} { yield $n; yield [expr {$n + 1}] }\ncoroutine g2 gen2 10\ng2") :result)
|
||||
"10")
|
||||
|
||||
(ok "coro-args-2"
|
||||
(get (run "proc gen2 {n} { yield $n; yield [expr {$n + 1}] }\ncoroutine g2 gen2 10\ng2\ng2") :result)
|
||||
"11")
|
||||
|
||||
; --- coroutine exhausted returns empty string ---
|
||||
(ok "coro-exhausted"
|
||||
(get (run "proc g3 {} { yield only }\ncoroutine c3 g3\nc3\nc3") :result)
|
||||
"")
|
||||
|
||||
; --- yield in while loop ---
|
||||
(ok "coro-while-loop-1"
|
||||
(get (run "proc counter {max} { set i 0; while {$i < $max} { yield $i; incr i } }\ncoroutine cw counter 3\ncw") :result)
|
||||
"0")
|
||||
|
||||
(ok "coro-while-loop-2"
|
||||
(get (run "proc counter {max} { set i 0; while {$i < $max} { yield $i; incr i } }\ncoroutine cw counter 3\ncw\ncw") :result)
|
||||
"1")
|
||||
|
||||
(ok "coro-while-loop-3"
|
||||
(get (run "proc counter {max} { set i 0; while {$i < $max} { yield $i; incr i } }\ncoroutine cw counter 3\ncw\ncw\ncw") :result)
|
||||
"2")
|
||||
|
||||
; --- collect all yields from coroutine ---
|
||||
(ok "coro-collect-all"
|
||||
(get
|
||||
(run
|
||||
"proc counter {n max} { while {$n < $max} { yield $n; incr n }; yield done }\ncoroutine gen1 counter 0 3\nset out {}\nfor {set i 0} {$i < 4} {incr i} { lappend out [gen1] }\nlindex $out 3")
|
||||
:result)
|
||||
"done")
|
||||
|
||||
; --- two independent coroutines ---
|
||||
(ok "coro-two-independent"
|
||||
(get
|
||||
(run
|
||||
"proc seq {start} { yield $start; yield [expr {$start+1}] }\ncoroutine ca seq 0\ncoroutine cb seq 10\nset r [ca]\nappend r \":\" [cb]")
|
||||
:result)
|
||||
"0:10")
|
||||
|
||||
; --- yield with no value returns empty string ---
|
||||
(ok "coro-yield-no-val"
|
||||
(get (run "proc g {} { yield }\ncoroutine cg g\ncg") :result)
|
||||
"")
|
||||
|
||||
; --- clock seconds ---
|
||||
(ok "clock-seconds"
|
||||
(> (parse-int (get (run "clock seconds") :result)) 0)
|
||||
true)
|
||||
|
||||
; --- clock milliseconds ---
|
||||
(ok "clock-milliseconds"
|
||||
(> (parse-int (get (run "clock milliseconds") :result)) 0)
|
||||
true)
|
||||
|
||||
; --- clock format stub ---
|
||||
(ok "clock-format"
|
||||
(get (run "clock format 0") :result)
|
||||
"Thu Jan 1 00:00:00 UTC 1970")
|
||||
|
||||
; --- file stubs ---
|
||||
(ok "file-exists-stub"
|
||||
(get (run "file exists /no/such/file") :result)
|
||||
"0")
|
||||
|
||||
(ok "file-join"
|
||||
(get (run "file join foo bar baz") :result)
|
||||
"foo/bar/baz")
|
||||
|
||||
(ok "open-returns-channel"
|
||||
(get (run "open /dev/null r") :result)
|
||||
"file0")
|
||||
|
||||
(ok "eof-returns-1"
|
||||
(get (run "set ch [open /dev/null r]\neof $ch") :result)
|
||||
"1")
|
||||
|
||||
(dict
|
||||
"passed"
|
||||
tcl-coro-pass
|
||||
"failed"
|
||||
tcl-coro-fail
|
||||
"failures"
|
||||
tcl-coro-failures)))
|
||||
@@ -1,192 +0,0 @@
|
||||
; Tcl-on-SX error handling tests (Phase 4)
|
||||
(define tcl-err-pass 0)
|
||||
(define tcl-err-fail 0)
|
||||
(define tcl-err-failures (list))
|
||||
|
||||
(define
|
||||
tcl-err-assert
|
||||
(fn
|
||||
(label expected actual)
|
||||
(if
|
||||
(equal? expected actual)
|
||||
(set! tcl-err-pass (+ tcl-err-pass 1))
|
||||
(begin
|
||||
(set! tcl-err-fail (+ tcl-err-fail 1))
|
||||
(append!
|
||||
tcl-err-failures
|
||||
(str label ": expected=" (str expected) " got=" (str actual)))))))
|
||||
|
||||
(define
|
||||
tcl-run-error-tests
|
||||
(fn
|
||||
()
|
||||
(set! tcl-err-pass 0)
|
||||
(set! tcl-err-fail 0)
|
||||
(set! tcl-err-failures (list))
|
||||
(define interp (fn () (make-default-tcl-interp)))
|
||||
(define run (fn (src) (tcl-eval-string (interp) src)))
|
||||
(define
|
||||
ok
|
||||
(fn (label actual expected) (tcl-err-assert label expected actual)))
|
||||
(define
|
||||
ok?
|
||||
(fn (label condition) (tcl-err-assert label true condition)))
|
||||
|
||||
; --- catch basic ---
|
||||
(ok "catch-ok-code" (get (run "catch {set x 1}") :result) "0")
|
||||
(ok "catch-ok-result-var" (tcl-var-get (run "catch {set x hello} r") "r") "hello")
|
||||
(ok "catch-ok-returns-0" (get (run "catch {set x hello} r") :result) "0")
|
||||
|
||||
; --- catch error ---
|
||||
(ok "catch-error-code" (get (run "catch {error oops} r") :result) "1")
|
||||
(ok "catch-error-result-var" (tcl-var-get (run "catch {error oops} r") "r") "oops")
|
||||
|
||||
; --- catch outer code stays 0 ---
|
||||
(ok? "catch-outer-code-ok" (= (get (run "catch {error boom} r") :code) 0))
|
||||
|
||||
; --- catch code 2 (return) ---
|
||||
(ok "catch-return-code" (get (run "proc p {} {return hello}\ncatch {p} r") :result) "0")
|
||||
(ok "catch-return-val" (tcl-var-get (run "proc p {} {return hello}\ncatch {p} r") "r") "hello")
|
||||
|
||||
; --- catch code 3 (break) ---
|
||||
(ok "catch-break-code" (get (run "catch {break} r") :result) "3")
|
||||
|
||||
; --- catch code 4 (continue) ---
|
||||
(ok "catch-continue-code" (get (run "catch {continue} r") :result) "4")
|
||||
|
||||
; --- catch no resultVar ---
|
||||
(ok "catch-no-var-ok" (get (run "catch {set x 1}") :result) "0")
|
||||
(ok "catch-no-var-err" (get (run "catch {error boom}") :result) "1")
|
||||
|
||||
; --- catch with optsVar ---
|
||||
(ok? "catch-opts-var-set"
|
||||
(let
|
||||
((i (run "catch {error boom} r opts")))
|
||||
(not (equal? (tcl-var-get i "opts") ""))))
|
||||
(ok? "catch-opts-contains-code"
|
||||
(let
|
||||
((i (run "catch {error boom} r opts")))
|
||||
(let
|
||||
((opts-str (tcl-var-get i "opts")))
|
||||
(not (equal? (tcl-string-first "-code" opts-str 0) "-1")))))
|
||||
|
||||
; --- catch nested ---
|
||||
(ok "catch-nested"
|
||||
(tcl-var-get (run "catch {catch {error inner} r2} outer") "r2")
|
||||
"inner")
|
||||
|
||||
; --- return -code error ---
|
||||
(ok "return-code-error-code"
|
||||
(get (run "catch {return -code error oops} r") :result)
|
||||
"1")
|
||||
(ok "return-code-error-val"
|
||||
(tcl-var-get (run "catch {return -code error oops} r") "r")
|
||||
"oops")
|
||||
|
||||
; --- return -code ok ---
|
||||
(ok "return-code-ok"
|
||||
(get (run "catch {return -code ok hello} r") :result)
|
||||
"0")
|
||||
(ok "return-code-ok-val"
|
||||
(tcl-var-get (run "catch {return -code ok hello} r") "r")
|
||||
"hello")
|
||||
|
||||
; --- return -code break ---
|
||||
(ok "return-code-break"
|
||||
(get (run "catch {return -code break} r") :result)
|
||||
"3")
|
||||
|
||||
; --- return -code continue ---
|
||||
(ok "return-code-continue"
|
||||
(get (run "catch {return -code continue} r") :result)
|
||||
"4")
|
||||
|
||||
; --- return -code numeric ---
|
||||
(ok "return-code-numeric-5"
|
||||
(get (run "catch {return -code 5 msg} r") :result)
|
||||
"5")
|
||||
|
||||
; --- return plain still code 2 (catch sees raw return code) ---
|
||||
(ok "return-plain-code"
|
||||
(get (run "catch {return hello} r") :result)
|
||||
"2")
|
||||
(ok "return-plain-val"
|
||||
(tcl-var-get (run "catch {return hello} r") "r")
|
||||
"hello")
|
||||
|
||||
; --- proc return -code error ---
|
||||
(ok "proc-return-code-error"
|
||||
(get (run "proc p {} {return -code error bad}\ncatch {p} r") :result)
|
||||
"1")
|
||||
(ok "proc-return-code-error-val"
|
||||
(tcl-var-get (run "proc p {} {return -code error bad}\ncatch {p} r") "r")
|
||||
"bad")
|
||||
|
||||
; --- error with info/code args ---
|
||||
(ok? "error-errorinfo-stored"
|
||||
(let
|
||||
((i (run "catch {error msg myinfo mycode} r")))
|
||||
(= (get i :code) 0)))
|
||||
|
||||
; --- throw ---
|
||||
(ok "throw-code" (get (run "catch {throw MYERR something} r") :result) "1")
|
||||
(ok "throw-msg" (tcl-var-get (run "catch {throw MYERR something} r") "r") "something")
|
||||
|
||||
; --- try basic ok ---
|
||||
(ok "try-ok-result"
|
||||
(get (run "try {set x hello} on ok {r} {set r2 $r}") :result)
|
||||
"hello")
|
||||
|
||||
; --- try on error ---
|
||||
(ok "try-on-error-handled"
|
||||
(get (run "try {error boom} on error {e} {set caught $e}") :result)
|
||||
"boom")
|
||||
(ok "try-on-error-var"
|
||||
(tcl-var-get (run "try {error boom} on error {e} {set caught $e}") "caught")
|
||||
"boom")
|
||||
|
||||
; --- try finally always runs ---
|
||||
(ok "try-finally-ok"
|
||||
(tcl-var-get (run "try {set x 1} finally {set done yes}") "done")
|
||||
"yes")
|
||||
(ok "try-finally-error"
|
||||
(tcl-var-get (run "catch {try {error boom} finally {set done yes}} r") "done")
|
||||
"yes")
|
||||
|
||||
; --- try on error + finally ---
|
||||
(ok "try-error-finally"
|
||||
(tcl-var-get
|
||||
(run "try {error oops} on error {e} {set caught $e} finally {set cleaned yes}")
|
||||
"cleaned")
|
||||
"yes")
|
||||
(ok "try-error-finally-caught"
|
||||
(tcl-var-get
|
||||
(run "try {error oops} on error {e} {set caught $e} finally {set cleaned yes}")
|
||||
"caught")
|
||||
"oops")
|
||||
|
||||
; --- try on ok and on error ---
|
||||
(ok "try-multi-clause-ok"
|
||||
(tcl-var-get
|
||||
(run "try {set x 1} on ok {r} {set which ok} on error {e} {set which err}")
|
||||
"which")
|
||||
"ok")
|
||||
(ok "try-multi-clause-err"
|
||||
(tcl-var-get
|
||||
(run "try {error boom} on ok {r} {set which ok} on error {e} {set which err}")
|
||||
"which")
|
||||
"err")
|
||||
|
||||
; --- catch preserves output ---
|
||||
(ok "catch-output-preserved"
|
||||
(get (run "puts -nonewline before\ncatch {puts -nonewline inside\nerror oops}\nputs -nonewline after")
|
||||
:output)
|
||||
"beforeinsideafter")
|
||||
|
||||
(dict
|
||||
"passed"
|
||||
tcl-err-pass
|
||||
"failed"
|
||||
tcl-err-fail
|
||||
"failures"
|
||||
tcl-err-failures)))
|
||||
@@ -1,386 +0,0 @@
|
||||
; Tcl-on-SX eval tests
|
||||
(define tcl-eval-pass 0)
|
||||
(define tcl-eval-fail 0)
|
||||
(define tcl-eval-failures (list))
|
||||
|
||||
(define
|
||||
tcl-eval-assert
|
||||
(fn
|
||||
(label expected actual)
|
||||
(if
|
||||
(equal? expected actual)
|
||||
(set! tcl-eval-pass (+ tcl-eval-pass 1))
|
||||
(begin
|
||||
(set! tcl-eval-fail (+ tcl-eval-fail 1))
|
||||
(append!
|
||||
tcl-eval-failures
|
||||
(str label ": expected=" (str expected) " got=" (str actual)))))))
|
||||
|
||||
(define
|
||||
tcl-run-eval-tests
|
||||
(fn
|
||||
()
|
||||
(set! tcl-eval-pass 0)
|
||||
(set! tcl-eval-fail 0)
|
||||
(set! tcl-eval-failures (list))
|
||||
(define interp (fn () (make-default-tcl-interp)))
|
||||
(define run (fn (src) (tcl-eval-string (interp) src)))
|
||||
(define
|
||||
ok
|
||||
(fn (label actual expected) (tcl-eval-assert label expected actual)))
|
||||
(define
|
||||
ok?
|
||||
(fn (label condition) (tcl-eval-assert label true condition)))
|
||||
(tcl-eval-assert "set-result" "hello" (get (run "set x hello") :result))
|
||||
(tcl-eval-assert
|
||||
"set-stored"
|
||||
"hello"
|
||||
(tcl-var-get (run "set x hello") "x"))
|
||||
(tcl-eval-assert
|
||||
"var-sub"
|
||||
"hello"
|
||||
(tcl-var-get (run "set x hello\nset y $x") "y"))
|
||||
(tcl-eval-assert
|
||||
"puts"
|
||||
"world\n"
|
||||
(get (run "set x world\nputs $x") :output))
|
||||
(tcl-eval-assert
|
||||
"puts-nonewline"
|
||||
"hi"
|
||||
(get (run "puts -nonewline hi") :output))
|
||||
(tcl-eval-assert "incr" "6" (tcl-var-get (run "set x 5\nincr x") "x"))
|
||||
(tcl-eval-assert
|
||||
"incr-delta"
|
||||
"8"
|
||||
(tcl-var-get (run "set x 5\nincr x 3") "x"))
|
||||
(tcl-eval-assert
|
||||
"incr-neg"
|
||||
"7"
|
||||
(tcl-var-get (run "set x 10\nincr x -3") "x"))
|
||||
(tcl-eval-assert
|
||||
"append"
|
||||
"foobar"
|
||||
(tcl-var-get (run "set x foo\nappend x bar") "x"))
|
||||
(tcl-eval-assert
|
||||
"append-new"
|
||||
"hello"
|
||||
(tcl-var-get (run "append x hello") "x"))
|
||||
(tcl-eval-assert
|
||||
"cmdsub-result"
|
||||
"6"
|
||||
(get (run "set x 5\nset y [incr x]") :result))
|
||||
(tcl-eval-assert
|
||||
"cmdsub-y"
|
||||
"6"
|
||||
(tcl-var-get (run "set x 5\nset y [incr x]") "y"))
|
||||
(tcl-eval-assert
|
||||
"cmdsub-x"
|
||||
"6"
|
||||
(tcl-var-get (run "set x 5\nset y [incr x]") "x"))
|
||||
(tcl-eval-assert
|
||||
"multi-cmd"
|
||||
"second"
|
||||
(get (run "set x first\nset x second") :result))
|
||||
(tcl-eval-assert "semi-x" "1" (tcl-var-get (run "set x 1; set y 2") "x"))
|
||||
(tcl-eval-assert "semi-y" "2" (tcl-var-get (run "set x 1; set y 2") "y"))
|
||||
(tcl-eval-assert
|
||||
"braced-nosub"
|
||||
"$x"
|
||||
(tcl-var-get (run "set x 42\nset y {$x}") "y"))
|
||||
(tcl-eval-assert
|
||||
"concat-word"
|
||||
"foobar"
|
||||
(tcl-var-get (run "set x foo\nset y ${x}bar") "y"))
|
||||
(tcl-eval-assert
|
||||
"set-get"
|
||||
"world"
|
||||
(get (run "set x world\nset x") :result))
|
||||
(tcl-eval-assert
|
||||
"puts-channel"
|
||||
"hello\n"
|
||||
(get (run "puts stdout hello") :output))
|
||||
(ok "if-true" (get (run "set x 0\nif {1} {set x 1}") :result) "1")
|
||||
(ok "if-false" (get (run "set x 0\nif {0} {set x 1}") :result) "0")
|
||||
(ok
|
||||
"if-else-t"
|
||||
(tcl-var-get (run "if {1} {set x yes} else {set x no}") "x")
|
||||
"yes")
|
||||
(ok
|
||||
"if-else-f"
|
||||
(tcl-var-get (run "if {0} {set x yes} else {set x no}") "x")
|
||||
"no")
|
||||
(ok
|
||||
"if-cmp"
|
||||
(tcl-var-get
|
||||
(run "set x 5\nif {$x > 3} {set r big} else {set r small}")
|
||||
"r")
|
||||
"big")
|
||||
(ok
|
||||
"while"
|
||||
(tcl-var-get
|
||||
(run "set i 0\nset s 0\nwhile {$i < 5} {incr i\nincr s $i}")
|
||||
"s")
|
||||
"15")
|
||||
(ok
|
||||
"while-break"
|
||||
(tcl-var-get
|
||||
(run "set i 0\nwhile {1} {incr i\nif {$i == 3} {break}}")
|
||||
"i")
|
||||
"3")
|
||||
(ok
|
||||
"for"
|
||||
(tcl-var-get
|
||||
(run "set s 0\nfor {set i 1} {$i <= 5} {incr i} {incr s $i}")
|
||||
"s")
|
||||
"15")
|
||||
(ok
|
||||
"foreach"
|
||||
(tcl-var-get (run "set s 0\nforeach x {1 2 3 4 5} {incr s $x}") "s")
|
||||
"15")
|
||||
(ok
|
||||
"foreach-list"
|
||||
(get (run "set acc \"\"\nforeach w {hello world} {append acc $w}") :result)
|
||||
"helloworld")
|
||||
(ok
|
||||
"lappend"
|
||||
(tcl-var-get (run "lappend lst a\nlappend lst b\nlappend lst c") "lst")
|
||||
"a b c")
|
||||
(ok?
|
||||
"unset-gone"
|
||||
(let
|
||||
((i (run "set x 42\nunset x")))
|
||||
(let
|
||||
((frame (get i :frame)))
|
||||
(nil? (get (get frame :locals) "x")))))
|
||||
(ok "eval" (tcl-var-get (run "eval {set x hello}") "x") "hello")
|
||||
(ok "expr-precedence" (get (run "expr {3 + 4 * 2}") :result) "11")
|
||||
(ok "expr-parens" (get (run "expr {(3 + 4) * 2}") :result) "14")
|
||||
(ok "expr-unary-minus" (get (run "expr {-5}") :result) "-5")
|
||||
(ok "expr-unary-not-0" (get (run "expr {!0}") :result) "1")
|
||||
(ok "expr-unary-not-1" (get (run "expr {!1}") :result) "0")
|
||||
(ok "expr-power" (get (run "expr {2 ** 10}") :result) "1024")
|
||||
(ok "expr-le" (get (run "expr {3 <= 3}") :result) "1")
|
||||
(ok "expr-ge" (get (run "expr {4 >= 5}") :result) "0")
|
||||
(ok "expr-and" (get (run "expr {1 && 1}") :result) "1")
|
||||
(ok "expr-or" (get (run "expr {0 || 1}") :result) "1")
|
||||
(ok "expr-var-sub" (get (run "set x 7\nexpr {$x * 3}") :result) "21")
|
||||
(ok "expr-abs-neg" (get (run "expr {abs(-3)}") :result) "3")
|
||||
(ok "expr-abs-pos" (get (run "expr {abs(5)}") :result) "5")
|
||||
(ok "expr-pow-fn" (get (run "expr {pow(2, 8)}") :result) "256")
|
||||
(ok "expr-max" (get (run "expr {max(3, 7)}") :result) "7")
|
||||
(ok "expr-min" (get (run "expr {min(3, 7)}") :result) "3")
|
||||
(ok "expr-sqrt-9" (get (run "expr {sqrt(9)}") :result) "3")
|
||||
(ok "expr-sqrt-16" (get (run "expr {sqrt(16)}") :result) "4")
|
||||
(ok "expr-mod" (get (run "expr {17 % 5}") :result) "2")
|
||||
(ok "expr-nospace" (get (run "expr {3+4*2}") :result) "11")
|
||||
(ok "expr-add" (get (run "expr {3 + 4}") :result) "7")
|
||||
(ok "expr-cmp" (get (run "expr {5 > 3}") :result) "1")
|
||||
(ok
|
||||
"break-stops"
|
||||
(tcl-var-get (run "set x 0\nwhile {1} {set x 1\nbreak\nset x 99}") "x")
|
||||
"1")
|
||||
(ok
|
||||
"continue"
|
||||
(tcl-var-get
|
||||
(run
|
||||
"set s 0\nfor {set i 1} {$i <= 5} {incr i} {if {$i == 3} {continue}\nincr s $i}")
|
||||
"s")
|
||||
"12")
|
||||
(ok
|
||||
"switch"
|
||||
(tcl-var-get
|
||||
(run "set x foo\nswitch $x {{foo} {set r yes} {bar} {set r no}}")
|
||||
"r")
|
||||
"yes")
|
||||
(ok
|
||||
"switch-default"
|
||||
(tcl-var-get
|
||||
(run "set x baz\nswitch $x {{foo} {set r yes} default {set r other}}")
|
||||
"r")
|
||||
"other")
|
||||
(ok
|
||||
"nested-if"
|
||||
(tcl-var-get
|
||||
(run
|
||||
"set x 5\nif {$x > 10} {set r big} elseif {$x > 3} {set r mid} else {set r small}")
|
||||
"r")
|
||||
"mid")
|
||||
(ok "str-length" (get (run "string length hello") :result) "5")
|
||||
(ok "str-length-empty" (get (run "string length {}") :result) "0")
|
||||
(ok "str-index" (get (run "string index hello 1") :result) "e")
|
||||
(ok "str-index-oob" (get (run "string index hello 99") :result) "")
|
||||
(ok "str-range" (get (run "string range hello 1 3") :result) "ell")
|
||||
(ok "str-range-clamp" (get (run "string range hello 3 99") :result) "lo")
|
||||
(ok "str-compare-eq" (get (run "string compare abc abc") :result) "0")
|
||||
(ok "str-compare-lt" (get (run "string compare abc abd") :result) "-1")
|
||||
(ok "str-compare-gt" (get (run "string compare b a") :result) "1")
|
||||
(ok "str-match-star" (get (run "string match h*o hello") :result) "1")
|
||||
(ok "str-match-q" (get (run "string match h?llo hello") :result) "1")
|
||||
(ok "str-match-no" (get (run "string match h*x hello") :result) "0")
|
||||
(ok "str-toupper" (get (run "string toupper hello") :result) "HELLO")
|
||||
(ok "str-tolower" (get (run "string tolower WORLD") :result) "world")
|
||||
(ok "str-trim" (get (run "string trim { hi }") :result) "hi")
|
||||
(ok "str-trimleft" (get (run "string trimleft { hi }") :result) "hi ")
|
||||
(ok "str-trimright" (get (run "string trimright { hi }") :result) " hi")
|
||||
(ok "str-trim-chars" (get (run "string trim {xxhelloxx} x") :result) "hello")
|
||||
(ok "str-map" (get (run "string map {a X b Y} {abc}") :result) "XYc")
|
||||
(ok "str-repeat" (get (run "string repeat ab 3") :result) "ababab")
|
||||
(ok "str-first" (get (run "string first ll hello") :result) "2")
|
||||
(ok "str-first-miss" (get (run "string first z hello") :result) "-1")
|
||||
(ok "str-last" (get (run "string last l hello") :result) "3")
|
||||
(ok "str-is-int" (get (run "string is integer 42") :result) "1")
|
||||
(ok "str-is-not-int" (get (run "string is integer foo") :result) "0")
|
||||
(ok "str-is-alpha" (get (run "string is alpha hello") :result) "1")
|
||||
(ok "str-is-alpha-no" (get (run "string is alpha hello1") :result) "0")
|
||||
(ok "str-is-boolean" (get (run "string is boolean true") :result) "1")
|
||||
(ok "str-cat" (get (run "string cat foo bar baz") :result) "foobarbaz")
|
||||
; --- list command tests ---
|
||||
(ok "list-simple" (get (run "list a b c") :result) "a b c")
|
||||
(ok "list-brace-elem" (get (run "list {a b} c") :result) "{a b} c")
|
||||
(ok "list-empty" (get (run "list") :result) "")
|
||||
(ok "lindex-1" (get (run "lindex {a b c} 1") :result) "b")
|
||||
(ok "lindex-0" (get (run "lindex {a b c} 0") :result) "a")
|
||||
(ok "lindex-oob" (get (run "lindex {a b c} 5") :result) "")
|
||||
(ok "lrange" (get (run "lrange {a b c d} 1 2") :result) "b c")
|
||||
(ok "lrange-full" (get (run "lrange {a b c} 0 end") :result) "a b c")
|
||||
(ok "llength" (get (run "llength {a b c}") :result) "3")
|
||||
(ok "llength-empty" (get (run "llength {}") :result) "0")
|
||||
(ok "lreverse" (get (run "lreverse {1 2 3}") :result) "3 2 1")
|
||||
(ok "lsearch-found" (get (run "lsearch {a b c} b") :result) "1")
|
||||
(ok "lsearch-missing" (get (run "lsearch {a b c} z") :result) "-1")
|
||||
(ok "lsearch-exact" (get (run "lsearch -exact {foo bar} foo") :result) "0")
|
||||
(ok "lsort-asc" (get (run "lsort {banana apple cherry}") :result) "apple banana cherry")
|
||||
(ok "lsort-int" (get (run "lsort -integer {10 2 30 5}") :result) "2 5 10 30")
|
||||
(ok "lsort-dec" (get (run "lsort -decreasing {c a b}") :result) "c b a")
|
||||
(ok "lreplace" (get (run "lreplace {a b c d} 1 2 X Y") :result) "a X Y d")
|
||||
(ok "linsert" (get (run "linsert {a b c} 1 X Y") :result) "a X Y b c")
|
||||
(ok "linsert-end" (get (run "linsert {a b} end Z") :result) "a b Z")
|
||||
(ok "concat" (get (run "concat {a b} {c d}") :result) "a b c d")
|
||||
(ok "split-sep" (get (run "split {a:b:c} :") :result) "a b c")
|
||||
(ok "split-ws" (get (run "split {a b c}") :result) "a b c")
|
||||
(ok "join-sep" (get (run "join {a b c} -") :result) "a-b-c")
|
||||
(ok "join-default" (get (run "join {a b c}") :result) "a b c")
|
||||
(ok "list-var" (get (run "set L {x y z}\nllength $L") :result) "3")
|
||||
; --- dict command tests ---
|
||||
(ok "dict-create" (get (run "dict create a 1 b 2") :result) "a 1 b 2")
|
||||
(ok "dict-create-empty" (get (run "dict create") :result) "")
|
||||
(ok "dict-get" (get (run "dict get {a 1 b 2} a") :result) "1")
|
||||
(ok "dict-get-b" (get (run "dict get {a 1 b 2} b") :result) "2")
|
||||
(ok "dict-exists-yes" (get (run "dict exists {a 1 b 2} a") :result) "1")
|
||||
(ok "dict-exists-no" (get (run "dict exists {a 1 b 2} z") :result) "0")
|
||||
(ok "dict-set-new" (get (run "set d {}\ndict set d x 42") :result) "x 42")
|
||||
(ok "dict-set-update" (get (run "set d {a 1 b 2}\ndict set d a 99") :result) "a 99 b 2")
|
||||
(ok "dict-set-stored" (tcl-var-get (run "set d {a 1}\ndict set d b 2") "d") "a 1 b 2")
|
||||
(ok "dict-unset" (get (run "set d {a 1 b 2}\ndict unset d a") :result) "b 2")
|
||||
(ok "dict-unset-stored" (tcl-var-get (run "set d {a 1 b 2}\ndict unset d a") "d") "b 2")
|
||||
(ok "dict-keys" (get (run "dict keys {a 1 b 2}") :result) "a b")
|
||||
(ok "dict-keys-pattern" (get (run "dict keys {abc 1 abd 2 xyz 3} ab*") :result) "abc abd")
|
||||
(ok "dict-values" (get (run "dict values {a 1 b 2}") :result) "1 2")
|
||||
(ok "dict-size" (get (run "dict size {a 1 b 2 c 3}") :result) "3")
|
||||
(ok "dict-size-empty" (get (run "dict size {}") :result) "0")
|
||||
(ok "dict-for" (tcl-var-get (run "set acc {}\ndict for {k v} {a 1 b 2} {append acc $k$v}") "acc") "a1b2")
|
||||
(ok "dict-merge-disjoint" (get (run "dict merge {a 1} {b 2}") :result) "a 1 b 2")
|
||||
(ok "dict-merge-overlap" (get (run "dict merge {a 1 b 2} {b 99}") :result) "a 1 b 99")
|
||||
(ok "dict-incr-existing" (get (run "set d {x 5}\ndict incr d x") :result) "x 6")
|
||||
(ok "dict-incr-delta" (get (run "set d {x 5}\ndict incr d x 3") :result) "x 8")
|
||||
(ok "dict-incr-missing" (get (run "set d {}\ndict incr d n") :result) "n 1")
|
||||
(ok "dict-append" (get (run "set d {x hello}\ndict append d x _hi") :result) "x hello_hi")
|
||||
(ok "dict-append-new" (get (run "set d {}\ndict append d k val") :result) "k val")
|
||||
; --- proc tests ---
|
||||
(ok "proc-basic" (get (run "proc add {a b} {expr {$a + $b}}\nadd 3 4") :result) "7")
|
||||
(ok "proc-return" (get (run "proc greet {name} {set msg \"hi $name\"\nreturn $msg}\ngreet World") :result) "hi World")
|
||||
(ok "proc-factorial" (get (run "proc factorial {n} {if {$n <= 1} {return 1}\nexpr {$n * [factorial [expr {$n - 1}]]}}\nfactorial 5") :result) "120")
|
||||
(ok "proc-args" (get (run "proc sum args {set t 0\nforeach x $args {incr t $x}\nreturn $t}\nsum 1 2 3 4") :result) "10")
|
||||
(ok "proc-isolated" (get (run "set x outer\nproc p {} {set x inner\nreturn $x}\np") :result) "inner")
|
||||
(ok "proc-caller-unchanged" (tcl-var-get (run "set x outer\nproc p {} {set x inner\nreturn $x}\np\nset dummy 1") "x") "outer")
|
||||
(ok "proc-output" (get (run "proc hello {} {puts -nonewline hi}\nhello") :output) "hi")
|
||||
; --- upvar tests ---
|
||||
(ok "upvar-incr" (tcl-var-get (run "proc incr2 {varname} {upvar 1 $varname v\nincr v}\nset counter 10\nincr2 counter\nset counter") "counter") "11")
|
||||
(ok "upvar-double" (tcl-var-get (run "proc double-it {varname} {upvar 1 $varname x\nset x [expr {$x * 2}]}\nset val 5\ndouble-it val\nset val") "val") "10")
|
||||
(ok "upvar-result" (get (run "proc double-it {varname} {upvar 1 $varname x\nset x [expr {$x * 2}]}\nset val 5\ndouble-it val\nset val") :result) "10")
|
||||
; --- uplevel tests ---
|
||||
(ok "uplevel-set" (tcl-var-get (run "proc setvar {name val} {uplevel 1 \"set $name $val\"}\nsetvar x 99\nset x") "x") "99")
|
||||
(ok "uplevel-get" (get (run "proc getvar {name} {uplevel 1 \"set $name\"}\nset y 77\ngetvar y") :result) "77")
|
||||
; --- global tests ---
|
||||
(ok "global-read" (get (run "set g 100\nproc getg {} {global g\nreturn $g}\ngetg") :result) "100")
|
||||
(ok "global-write" (tcl-var-get (run "set g 0\nproc bumping {} {global g\nincr g}\nbumping\nbumping\nset g") "g") "2")
|
||||
; --- info tests ---
|
||||
(ok "info-level-0" (get (run "info level") :result) "0")
|
||||
(ok "info-level-proc" (get (run "proc p {} {info level}\np") :result) "1")
|
||||
(ok "info-procs" (let ((r (get (run "proc myfn {} {}\ninfo procs") :result))) (contains? (tcl-list-split r) "myfn")) true)
|
||||
(ok "info-args" (get (run "proc add {a b} {expr {$a+$b}}\ninfo args add") :result) "a b")
|
||||
(ok "info-commands-has-set" (let ((r (get (run "info commands") :result))) (contains? (tcl-list-split r) "set")) true)
|
||||
; --- classic programs ---
|
||||
(ok
|
||||
"classic-for-each-line"
|
||||
(get
|
||||
(run "proc for-each-line {var lines body} {\n foreach item $lines {\n uplevel 1 [list set $var $item]\n uplevel 1 $body\n }\n}\nset total 0\nfor-each-line line {hello world foo} {\n incr total [string length $line]\n}\nset total")
|
||||
:result)
|
||||
"13")
|
||||
(ok
|
||||
"classic-assert"
|
||||
(get
|
||||
(run "proc assert {expr_str} {\n set result [uplevel 1 [list expr $expr_str]]\n if {!$result} {\n error \"Assertion failed: $expr_str\"\n }\n}\nset x 42\nassert {$x == 42}\nassert {$x > 0}\nset x 10\nassert {$x < 100}\nset x")
|
||||
:result)
|
||||
"10")
|
||||
(ok
|
||||
"classic-with-temp-var"
|
||||
(get
|
||||
(run "proc with-temp-var {varname tempval body} {\n upvar 1 $varname v\n set saved $v\n set v $tempval\n uplevel 1 $body\n set v $saved\n}\nset x 100\nwith-temp-var x 999 {\n set captured $x\n}\nlist $x $captured")
|
||||
:result)
|
||||
"100 999")
|
||||
(ok
|
||||
"array-set-get"
|
||||
(get
|
||||
(run "array set a {x 1 y 2 z 3}; array get a x")
|
||||
:result)
|
||||
"x 1")
|
||||
(ok
|
||||
"array-names"
|
||||
(get
|
||||
(run "array set a {p 10 q 20}; lsort [array names a]")
|
||||
:result)
|
||||
"p q")
|
||||
(ok
|
||||
"array-size"
|
||||
(get
|
||||
(run "array set a {x 1 y 2 z 3}; array size a")
|
||||
:result)
|
||||
"3")
|
||||
(ok
|
||||
"array-exists-true"
|
||||
(get
|
||||
(run "array set a {x 1}; array exists a")
|
||||
:result)
|
||||
"1")
|
||||
(ok
|
||||
"array-exists-false"
|
||||
(get
|
||||
(run "array exists nosucharray")
|
||||
:result)
|
||||
"0")
|
||||
(ok
|
||||
"array-unset-key"
|
||||
(get
|
||||
(run "array set a {x 1 y 2 z 3}; array unset a y; lsort [array names a]")
|
||||
:result)
|
||||
"x z")
|
||||
(ok
|
||||
"array-scalar-access"
|
||||
(get
|
||||
(run "set a(foo) hello; set a(bar) world; set a(foo)")
|
||||
:result)
|
||||
"hello")
|
||||
(ok
|
||||
"array-get-all"
|
||||
(get
|
||||
(run "set a(k) v; set pairs [array get a]; llength $pairs")
|
||||
:result)
|
||||
"2")
|
||||
(dict
|
||||
"passed"
|
||||
tcl-eval-pass
|
||||
"failed"
|
||||
tcl-eval-fail
|
||||
"failures"
|
||||
tcl-eval-failures)))
|
||||
@@ -1,196 +0,0 @@
|
||||
; Tcl-on-SX idiom corpus (Phase 6)
|
||||
; Classic Tcl idioms covering lists, dicts, procs, patterns
|
||||
(define tcl-idiom-pass 0)
|
||||
(define tcl-idiom-fail 0)
|
||||
(define tcl-idiom-failures (list))
|
||||
|
||||
(define
|
||||
tcl-idiom-assert
|
||||
(fn
|
||||
(label expected actual)
|
||||
(if
|
||||
(equal? expected actual)
|
||||
(set! tcl-idiom-pass (+ tcl-idiom-pass 1))
|
||||
(begin
|
||||
(set! tcl-idiom-fail (+ tcl-idiom-fail 1))
|
||||
(append!
|
||||
tcl-idiom-failures
|
||||
(str label ": expected=" (str expected) " got=" (str actual)))))))
|
||||
|
||||
(define
|
||||
tcl-run-idiom-tests
|
||||
(fn
|
||||
()
|
||||
(set! tcl-idiom-pass 0)
|
||||
(set! tcl-idiom-fail 0)
|
||||
(set! tcl-idiom-failures (list))
|
||||
(define interp (fn () (make-default-tcl-interp)))
|
||||
(define run (fn (src) (tcl-eval-string (interp) src)))
|
||||
(define
|
||||
ok
|
||||
(fn (label actual expected) (tcl-idiom-assert label expected actual)))
|
||||
(ok
|
||||
"idiom-lmap"
|
||||
(get
|
||||
(run
|
||||
"set result {}\nforeach x {1 2 3} { lappend result [expr {$x * $x}] }\nset result")
|
||||
:result)
|
||||
"1 4 9")
|
||||
(ok
|
||||
"idiom-flatten"
|
||||
(get
|
||||
(run
|
||||
"proc flatten {lst} { set out {}\n foreach item $lst {\n if {[llength $item] > 1} {\n foreach sub [flatten $item] { lappend out $sub }\n } else {\n lappend out $item\n }\n }\n return $out\n}\nflatten {1 {2 3} {4 {5 6}}}")
|
||||
:result)
|
||||
"1 2 3 4 5 6")
|
||||
(ok
|
||||
"idiom-string-builder"
|
||||
(get
|
||||
(run
|
||||
"set buf \"\"\nforeach w {Hello World Tcl} { append buf $w \" \" }\nstring trimright $buf")
|
||||
:result)
|
||||
"Hello World Tcl")
|
||||
(ok
|
||||
"idiom-default-param"
|
||||
(get (run "if {![info exists x]} { set x 42 }\nset x") :result)
|
||||
"42")
|
||||
(ok
|
||||
"idiom-alist-lookup"
|
||||
(get
|
||||
(run
|
||||
"set keys {a b c}\nset vals {10 20 30}\nset idx [lsearch $keys b]\nlindex $vals $idx")
|
||||
:result)
|
||||
"20")
|
||||
(ok
|
||||
"idiom-optional-args"
|
||||
(get
|
||||
(run
|
||||
"proc greet {name args} {\n set greeting \"Hello\"\n if {[llength $args] > 0} { set greeting [lindex $args 0] }\n return \"$greeting $name\"\n}\ngreet World Hi")
|
||||
:result)
|
||||
"Hi World")
|
||||
(ok
|
||||
"idiom-dict-builder"
|
||||
(get
|
||||
(run
|
||||
"proc build-dict {args} { dict create {*}$args }\ndict get [build-dict name Alice age 30] name")
|
||||
:result)
|
||||
"Alice")
|
||||
(ok
|
||||
"idiom-loop-with-index"
|
||||
(get
|
||||
(run "set i 0\nforeach x {a b c} { set arr($i) $x; incr i }\nset arr(1)")
|
||||
:result)
|
||||
"b")
|
||||
(ok
|
||||
"idiom-string-reverse"
|
||||
(get
|
||||
(run
|
||||
"set s hello\nset chars [split $s \"\"]\nset rev [lreverse $chars]\njoin $rev \"\"")
|
||||
:result)
|
||||
"olleh")
|
||||
(ok "idiom-number-format" (get (run "format \"%05d\" 42") :result) "00042")
|
||||
(ok
|
||||
"idiom-dict-comprehension"
|
||||
(get
|
||||
(run
|
||||
"set squares {}\nforeach n {1 2 3 4} { dict set squares $n [expr {$n * $n}] }\ndict get $squares 3")
|
||||
:result)
|
||||
"9")
|
||||
(ok
|
||||
"idiom-stack"
|
||||
(get
|
||||
(run
|
||||
"proc stack-push {stackvar val} { upvar $stackvar s; lappend s $val }\nproc stack-pop {stackvar} { upvar $stackvar s; set val [lindex $s end]; set s [lrange $s 0 end-1]; return $val }\nset stk {}\nstack-push stk 10\nstack-push stk 20\nstack-push stk 30\nstack-pop stk")
|
||||
:result)
|
||||
"30")
|
||||
(ok
|
||||
"idiom-queue"
|
||||
(get
|
||||
(run
|
||||
"proc q-enq {qvar val} { upvar $qvar q; lappend q $val }\nproc q-deq {qvar} { upvar $qvar q; set val [lindex $q 0]; set q [lrange $q 1 end]; return $val }\nset q {}\nq-enq q alpha\nq-enq q beta\nq-enq q gamma\nq-deq q")
|
||||
:result)
|
||||
"alpha")
|
||||
(ok
|
||||
"idiom-pipeline"
|
||||
(get
|
||||
(run
|
||||
"proc double {x} { expr {$x * 2} }\nproc add1 {x} { expr {$x + 1} }\nproc pipeline {val procs} { foreach p $procs { set val [$p $val] }; return $val }\npipeline 5 {double add1 double}")
|
||||
:result)
|
||||
"22")
|
||||
(ok
|
||||
"idiom-memoize"
|
||||
(get
|
||||
(run
|
||||
"set cache {}\nproc cached-square {n} { global cache\n if {[dict exists $cache $n]} { return [dict get $cache $n] }\n set r [expr {$n * $n}]\n dict set cache $n $r\n return $r\n}\nset a [cached-square 7]\nset b [cached-square 7]\nset c [cached-square 8]\nexpr {$a == $b && $c == 64}")
|
||||
:result)
|
||||
"1")
|
||||
(ok
|
||||
"idiom-recursive-eval"
|
||||
(get
|
||||
(run
|
||||
"proc calc {expr} { return [::tcl::mathop::+ 0 [expr $expr]] }\nexpr {3 + 4 * 2}")
|
||||
:result)
|
||||
"11")
|
||||
(ok
|
||||
"idiom-dict-for"
|
||||
(get
|
||||
(run
|
||||
"set d [dict create a 1 b 2 c 3]\nset total 0\ndict for {k v} $d { incr total $v }\nset total")
|
||||
:result)
|
||||
"6")
|
||||
(ok
|
||||
"idiom-find-max"
|
||||
(get
|
||||
(run
|
||||
"proc list-max {lst} {\n set m [lindex $lst 0]\n foreach x $lst { if {$x > $m} { set m $x } }\n return $m\n}\nlist-max {3 1 4 1 5 9 2 6}")
|
||||
:result)
|
||||
"9")
|
||||
(ok
|
||||
"idiom-filter-list"
|
||||
(get
|
||||
(run
|
||||
"proc list-filter {lst pred} {\n set out {}\n foreach x $lst { if {[$pred $x]} { lappend out $x } }\n return $out\n}\nproc is-even {n} { expr {$n % 2 == 0} }\nlist-filter {1 2 3 4 5 6} is-even")
|
||||
:result)
|
||||
"2 4 6")
|
||||
(ok
|
||||
"idiom-zip"
|
||||
(get
|
||||
(run
|
||||
"proc zip {a b} {\n set out {}\n set n [llength $a]\n for {set i 0} {$i < $n} {incr i} {\n lappend out [lindex $a $i]\n lappend out [lindex $b $i]\n }\n return $out\n}\nzip {1 2 3} {a b c}")
|
||||
:result)
|
||||
"1 a 2 b 3 c")
|
||||
(ok
|
||||
"env-lookup-basic"
|
||||
(env-lookup (let ((x 42)) (current-env)) "x")
|
||||
42)
|
||||
(ok
|
||||
"env-lookup-missing"
|
||||
(env-lookup (let ((x 42)) (current-env)) "z")
|
||||
nil)
|
||||
(ok
|
||||
"env-extend-lookup"
|
||||
(let
|
||||
((e (let ((x 5)) (current-env))))
|
||||
(env-lookup (env-extend e "y" 10) "y"))
|
||||
10)
|
||||
(ok
|
||||
"eval-in-env-parent"
|
||||
(let
|
||||
((x 5))
|
||||
(eval-in-env (env-extend (current-env) "y" 10) (quote (+ x y))))
|
||||
15)
|
||||
(ok
|
||||
"eval-in-env-multi"
|
||||
(let
|
||||
((base (current-env)))
|
||||
(eval-in-env
|
||||
(env-extend (env-extend base "a" 3) "b" 7)
|
||||
(quote (* a b))))
|
||||
21)
|
||||
(dict
|
||||
"passed"
|
||||
tcl-idiom-pass
|
||||
"failed"
|
||||
tcl-idiom-fail
|
||||
"failures"
|
||||
tcl-idiom-failures)))
|
||||
@@ -1,147 +0,0 @@
|
||||
; Tcl-on-SX namespace tests (Phase 5)
|
||||
(define tcl-ns-pass 0)
|
||||
(define tcl-ns-fail 0)
|
||||
(define tcl-ns-failures (list))
|
||||
|
||||
(define
|
||||
tcl-ns-assert
|
||||
(fn
|
||||
(label expected actual)
|
||||
(if
|
||||
(equal? expected actual)
|
||||
(set! tcl-ns-pass (+ tcl-ns-pass 1))
|
||||
(begin
|
||||
(set! tcl-ns-fail (+ tcl-ns-fail 1))
|
||||
(append!
|
||||
tcl-ns-failures
|
||||
(str label ": expected=" (str expected) " got=" (str actual)))))))
|
||||
|
||||
(define
|
||||
tcl-run-namespace-tests
|
||||
(fn
|
||||
()
|
||||
(set! tcl-ns-pass 0)
|
||||
(set! tcl-ns-fail 0)
|
||||
(set! tcl-ns-failures (list))
|
||||
(define interp (fn () (make-default-tcl-interp)))
|
||||
(define run (fn (src) (tcl-eval-string (interp) src)))
|
||||
(define
|
||||
ok
|
||||
(fn (label actual expected) (tcl-ns-assert label expected actual)))
|
||||
(define
|
||||
ok?
|
||||
(fn (label condition) (tcl-ns-assert label true condition)))
|
||||
|
||||
; --- namespace current ---
|
||||
(ok "ns-current-global"
|
||||
(get (run "namespace current") :result)
|
||||
"::")
|
||||
|
||||
; --- namespace eval defines proc ---
|
||||
(ok "ns-eval-proc-result"
|
||||
(get (run "namespace eval myns { proc foo {} { return bar } }\nmyns::foo") :result)
|
||||
"bar")
|
||||
|
||||
; --- fully qualified call ---
|
||||
(ok "ns-qualified-call"
|
||||
(get (run "namespace eval myns { proc greet {name} { return \"hello $name\" } }\n::myns::greet World") :result)
|
||||
"hello World")
|
||||
|
||||
; --- namespace current inside eval ---
|
||||
(ok "ns-current-inside"
|
||||
(get (run "namespace eval myns { namespace current }") :result)
|
||||
"::myns")
|
||||
|
||||
; --- namespace current restored after eval ---
|
||||
(ok "ns-current-restored"
|
||||
(get (run "namespace eval myns { set x 1 }\nnamespace current") :result)
|
||||
"::")
|
||||
|
||||
; --- relative call from within namespace ---
|
||||
(ok "ns-relative-call"
|
||||
(get (run "namespace eval math {\n proc double {x} { expr {$x * 2} }\n proc quad {x} { double [double $x] }\n}\nmath::quad 3") :result)
|
||||
"12")
|
||||
|
||||
; --- proc defined as qualified name inside namespace eval ---
|
||||
(ok "ns-qualified-proc-name"
|
||||
(get (run "namespace eval utils { proc ::utils::helper {x} { return $x } }\n::utils::helper done") :result)
|
||||
"done")
|
||||
|
||||
; --- namespace exists ---
|
||||
(ok "ns-exists-yes"
|
||||
(get (run "namespace eval testns { proc p {} {} }\nnamespace exists testns") :result)
|
||||
"1")
|
||||
|
||||
(ok "ns-exists-no"
|
||||
(get (run "namespace exists nosuchns") :result)
|
||||
"0")
|
||||
|
||||
(ok "ns-exists-global"
|
||||
(get (run "proc top {} {}\nnamespace exists ::") :result)
|
||||
"1")
|
||||
|
||||
; --- namespace delete ---
|
||||
(ok "ns-delete-removes"
|
||||
(get (run "namespace eval todel { proc pp {} { return yes } }\nnamespace delete todel\nnamespace exists todel") :result)
|
||||
"0")
|
||||
|
||||
; --- namespace which ---
|
||||
(ok "ns-which-found"
|
||||
(get (run "namespace eval wns { proc wfn {} {} }\nnamespace which -command wns::wfn") :result)
|
||||
"::wns::wfn")
|
||||
|
||||
(ok "ns-which-not-found"
|
||||
(get (run "namespace which -command nosuchfn") :result)
|
||||
"")
|
||||
|
||||
; --- namespace ensemble create auto-map ---
|
||||
(ok "ns-ensemble-add"
|
||||
(get (run "namespace eval mymath {\n proc add {a b} { expr {$a + $b} }\n proc mul {a b} { expr {$a * $b} }\n namespace ensemble create\n}\nmymath add 3 4") :result)
|
||||
"7")
|
||||
|
||||
(ok "ns-ensemble-mul"
|
||||
(get (run "namespace eval mymath {\n proc add {a b} { expr {$a + $b} }\n proc mul {a b} { expr {$a * $b} }\n namespace ensemble create\n}\nmymath mul 3 4") :result)
|
||||
"12")
|
||||
|
||||
; --- namespace ensemble with -map ---
|
||||
(ok "ns-ensemble-map"
|
||||
(get (run "namespace eval ops {\n proc do-add {a b} { expr {$a + $b} }\n namespace ensemble create -map {plus ::ops::do-add}\n}\nops plus 5 6") :result)
|
||||
"11")
|
||||
|
||||
; --- proc inside namespace eval with args ---
|
||||
(ok "ns-proc-args"
|
||||
(get (run "namespace eval calc {\n proc sum {a b c} { expr {$a + $b + $c} }\n}\ncalc::sum 1 2 3") :result)
|
||||
"6")
|
||||
|
||||
; --- info procs inside namespace ---
|
||||
(ok? "ns-info-procs-in-ns"
|
||||
(let
|
||||
((r (get (run "namespace eval foo { proc bar {} {} }\nnamespace eval foo { info procs }") :result)))
|
||||
(contains? (tcl-list-split r) "bar")))
|
||||
|
||||
; --- variable inside namespace eval ---
|
||||
(ok "ns-variable-inside"
|
||||
(get (run "namespace eval storage {\n variable count 0\n proc bump {} { global count\n incr count\n return $count }\n}\n::storage::bump\n::storage::bump") :result)
|
||||
"2")
|
||||
|
||||
; --- nested namespaces ---
|
||||
(ok "ns-nested"
|
||||
(get (run "namespace eval outer {\n namespace eval inner {\n proc greet {} { return nested }\n }\n}\n::outer::inner::greet") :result)
|
||||
"nested")
|
||||
|
||||
; --- namespace eval accumulates procs ---
|
||||
(ok "ns-eval-accumulate"
|
||||
(get (run "namespace eval acc { proc f1 {} { return one } }\nnamespace eval acc { proc f2 {} { return two } }\nacc::f1") :result)
|
||||
"one")
|
||||
|
||||
(ok "ns-eval-accumulate-2"
|
||||
(get (run "namespace eval acc { proc f1 {} { return one } }\nnamespace eval acc { proc f2 {} { return two } }\nacc::f2") :result)
|
||||
"two")
|
||||
|
||||
(dict
|
||||
"passed"
|
||||
tcl-ns-pass
|
||||
"failed"
|
||||
tcl-ns-fail
|
||||
"failures"
|
||||
tcl-ns-failures)))
|
||||
@@ -1,186 +0,0 @@
|
||||
(define tcl-parse-pass 0)
|
||||
(define tcl-parse-fail 0)
|
||||
(define tcl-parse-failures (list))
|
||||
|
||||
(define tcl-assert
|
||||
(fn (label expected actual)
|
||||
(if (= expected actual)
|
||||
(set! tcl-parse-pass (+ tcl-parse-pass 1))
|
||||
(begin
|
||||
(set! tcl-parse-fail (+ tcl-parse-fail 1))
|
||||
(append! tcl-parse-failures
|
||||
(str label ": expected=" (str expected) " got=" (str actual)))))))
|
||||
|
||||
(define tcl-first-cmd
|
||||
(fn (src) (nth (tcl-tokenize src) 0)))
|
||||
|
||||
(define tcl-cmd-words
|
||||
(fn (src) (get (tcl-first-cmd src) :words)))
|
||||
|
||||
(define tcl-word
|
||||
(fn (src wi) (nth (tcl-cmd-words src) wi)))
|
||||
|
||||
(define tcl-parts
|
||||
(fn (src wi) (get (tcl-word src wi) :parts)))
|
||||
|
||||
(define tcl-part
|
||||
(fn (src wi pi) (nth (tcl-parts src wi) pi)))
|
||||
|
||||
(define tcl-run-parse-tests
|
||||
(fn ()
|
||||
(set! tcl-parse-pass 0)
|
||||
(set! tcl-parse-fail 0)
|
||||
(set! tcl-parse-failures (list))
|
||||
|
||||
; empty / whitespace-only
|
||||
(tcl-assert "empty" 0 (len (tcl-tokenize "")))
|
||||
(tcl-assert "ws-only" 0 (len (tcl-tokenize " ")))
|
||||
(tcl-assert "nl-only" 0 (len (tcl-tokenize "\n\n")))
|
||||
|
||||
; single command word count
|
||||
(tcl-assert "1word" 1 (len (tcl-cmd-words "set")))
|
||||
(tcl-assert "3words" 3 (len (tcl-cmd-words "set x 1")))
|
||||
(tcl-assert "4words" 4 (len (tcl-cmd-words "set a b c")))
|
||||
|
||||
; word type — bare word is compound
|
||||
(tcl-assert "bare-type" "compound" (get (tcl-word "set x 1" 0) :type))
|
||||
(tcl-assert "bare-quoted" false (get (tcl-word "set x 1" 0) :quoted))
|
||||
(tcl-assert "bare-part-type" "text" (get (tcl-part "set x 1" 0 0) :type))
|
||||
(tcl-assert "bare-part-val" "set" (get (tcl-part "set x 1" 0 0) :value))
|
||||
(tcl-assert "bare-part2-val" "x" (get (tcl-part "set x 1" 1 0) :value))
|
||||
(tcl-assert "bare-part3-val" "1" (get (tcl-part "set x 1" 2 0) :value))
|
||||
|
||||
; multiple commands
|
||||
(tcl-assert "semi-sep" 2 (len (tcl-tokenize "set x 1; set y 2")))
|
||||
(tcl-assert "nl-sep" 2 (len (tcl-tokenize "set x 1\nset y 2")))
|
||||
(tcl-assert "multi-nl" 3 (len (tcl-tokenize "a\nb\nc")))
|
||||
|
||||
; comments
|
||||
(tcl-assert "comment-only" 0 (len (tcl-tokenize "# comment")))
|
||||
(tcl-assert "comment-nl" 0 (len (tcl-tokenize "# comment\n")))
|
||||
(tcl-assert "comment-then-cmd" 1 (len (tcl-tokenize "# comment\nset x 1")))
|
||||
(tcl-assert "semi-then-comment" 1 (len (tcl-tokenize "set x 1; # comment")))
|
||||
|
||||
; brace-quoted words
|
||||
(tcl-assert "brace-type" "braced" (get (tcl-word "{hello}" 0) :type))
|
||||
(tcl-assert "brace-value" "hello" (get (tcl-word "{hello}" 0) :value))
|
||||
(tcl-assert "brace-spaces" "hello world" (get (tcl-word "{hello world}" 0) :value))
|
||||
(tcl-assert "brace-nested" "a {b} c" (get (tcl-word "{a {b} c}" 0) :value))
|
||||
(tcl-assert "brace-no-var-sub" "hello $x" (get (tcl-word "{hello $x}" 0) :value))
|
||||
(tcl-assert "brace-no-cmd-sub" "[expr 1]" (get (tcl-word "{[expr 1]}" 0) :value))
|
||||
|
||||
; double-quoted words
|
||||
(tcl-assert "dq-type" "compound" (get (tcl-word "\"hello\"" 0) :type))
|
||||
(tcl-assert "dq-quoted" true (get (tcl-word "\"hello\"" 0) :quoted))
|
||||
(tcl-assert "dq-literal" "hello" (get (tcl-part "\"hello\"" 0 0) :value))
|
||||
|
||||
; variable substitution in bare word
|
||||
(tcl-assert "var-type" "var" (get (tcl-part "$x" 0 0) :type))
|
||||
(tcl-assert "var-name" "x" (get (tcl-part "$x" 0 0) :name))
|
||||
(tcl-assert "var-long" "long_name" (get (tcl-part "$long_name" 0 0) :name))
|
||||
|
||||
; ${name} form
|
||||
(tcl-assert "var-brace-type" "var" (get (tcl-part "${x}" 0 0) :type))
|
||||
(tcl-assert "var-brace-name" "x" (get (tcl-part "${x}" 0 0) :name))
|
||||
|
||||
; array variable substitution
|
||||
(tcl-assert "arr-type" "var-arr" (get (tcl-part "$arr(key)" 0 0) :type))
|
||||
(tcl-assert "arr-name" "arr" (get (tcl-part "$arr(key)" 0 0) :name))
|
||||
(tcl-assert "arr-key-len" 1 (len (get (tcl-part "$arr(key)" 0 0) :key)))
|
||||
(tcl-assert "arr-key-text" "key"
|
||||
(get (nth (get (tcl-part "$arr(key)" 0 0) :key) 0) :value))
|
||||
|
||||
; command substitution
|
||||
(tcl-assert "cmd-type" "cmd" (get (tcl-part "[expr 1+1]" 0 0) :type))
|
||||
(tcl-assert "cmd-src" "expr 1+1" (get (tcl-part "[expr 1+1]" 0 0) :src))
|
||||
|
||||
; nested command substitution
|
||||
(tcl-assert "cmd-nested-src" "expr [string length x]"
|
||||
(get (tcl-part "[expr [string length x]]" 0 0) :src))
|
||||
|
||||
; backslash substitution in double-quoted word
|
||||
(let ((ps (tcl-parts "\"a\\nb\"" 0)))
|
||||
(begin
|
||||
(tcl-assert "bs-n-part0" "a" (get (nth ps 0) :value))
|
||||
(tcl-assert "bs-n-part1" "\n" (get (nth ps 1) :value))
|
||||
(tcl-assert "bs-n-part2" "b" (get (nth ps 2) :value))))
|
||||
|
||||
(let ((ps (tcl-parts "\"a\\tb\"" 0)))
|
||||
(tcl-assert "bs-t-part1" "\t" (get (nth ps 1) :value)))
|
||||
|
||||
(let ((ps (tcl-parts "\"a\\\\b\"" 0)))
|
||||
(tcl-assert "bs-bs-part1" "\\" (get (nth ps 1) :value)))
|
||||
|
||||
; mixed word: text + var + text in double-quoted
|
||||
(let ((ps (tcl-parts "\"hello $name!\"" 0)))
|
||||
(begin
|
||||
(tcl-assert "mixed-text0" "hello " (get (nth ps 0) :value))
|
||||
(tcl-assert "mixed-var1-type" "var" (get (nth ps 1) :type))
|
||||
(tcl-assert "mixed-var1-name" "name" (get (nth ps 1) :name))
|
||||
(tcl-assert "mixed-text2" "!" (get (nth ps 2) :value))))
|
||||
|
||||
; {*} expansion
|
||||
(tcl-assert "expand-type" "expand" (get (tcl-word "{*}$list" 0) :type))
|
||||
|
||||
; line continuation between words
|
||||
(tcl-assert "cont-words" 3 (len (tcl-cmd-words "set x \\\n 1")))
|
||||
|
||||
; continuation — third command word is correct
|
||||
(tcl-assert "cont-word2-val" "1"
|
||||
(get (tcl-part "set x \\\n 1" 2 0) :value))
|
||||
|
||||
|
||||
; --- parser helpers ---
|
||||
; tcl-parse is an alias for tcl-tokenize
|
||||
(tcl-assert "parse-cmd-count" 1 (len (tcl-parse "set x 1")))
|
||||
(tcl-assert "parse-2cmds" 2 (len (tcl-parse "set x 1; set y 2")))
|
||||
|
||||
; tcl-cmd-len
|
||||
(tcl-assert "cmd-len-3" 3 (tcl-cmd-len (nth (tcl-parse "set x 1") 0)))
|
||||
(tcl-assert "cmd-len-1" 1 (tcl-cmd-len (nth (tcl-parse "puts") 0)))
|
||||
|
||||
; tcl-word-simple? on braced word
|
||||
(tcl-assert "simple-braced" true
|
||||
(tcl-word-simple? (nth (get (nth (tcl-parse "{hello}") 0) :words) 0)))
|
||||
|
||||
; tcl-word-simple? on bare word with no subs
|
||||
(tcl-assert "simple-bare" true
|
||||
(tcl-word-simple? (nth (get (nth (tcl-parse "hello") 0) :words) 0)))
|
||||
|
||||
; tcl-word-simple? on word containing a var sub — false
|
||||
(tcl-assert "simple-var-false" false
|
||||
(tcl-word-simple? (nth (get (nth (tcl-parse "$x") 0) :words) 0)))
|
||||
|
||||
; tcl-word-simple? on word containing a cmd sub — false
|
||||
(tcl-assert "simple-cmd-false" false
|
||||
(tcl-word-simple? (nth (get (nth (tcl-parse "[expr 1]") 0) :words) 0)))
|
||||
|
||||
; tcl-word-literal on braced word
|
||||
(tcl-assert "lit-braced" "hello world"
|
||||
(tcl-word-literal (nth (get (nth (tcl-parse "{hello world}") 0) :words) 0)))
|
||||
|
||||
; tcl-word-literal on bare word
|
||||
(tcl-assert "lit-bare" "hello"
|
||||
(tcl-word-literal (nth (get (nth (tcl-parse "hello") 0) :words) 0)))
|
||||
|
||||
; tcl-word-literal on word with var sub returns nil
|
||||
(tcl-assert "lit-var-nil" nil
|
||||
(tcl-word-literal (nth (get (nth (tcl-parse "$x") 0) :words) 0)))
|
||||
|
||||
; tcl-nth-literal
|
||||
(tcl-assert "nth-lit-0" "set"
|
||||
(tcl-nth-literal (nth (tcl-parse "set x 1") 0) 0))
|
||||
(tcl-assert "nth-lit-1" "x"
|
||||
(tcl-nth-literal (nth (tcl-parse "set x 1") 0) 1))
|
||||
(tcl-assert "nth-lit-2" "1"
|
||||
(tcl-nth-literal (nth (tcl-parse "set x 1") 0) 2))
|
||||
|
||||
; tcl-nth-literal returns nil when word has subs
|
||||
(tcl-assert "nth-lit-nil" nil
|
||||
(tcl-nth-literal (nth (tcl-parse "set x $y") 0) 2))
|
||||
|
||||
|
||||
(dict
|
||||
"passed" tcl-parse-pass
|
||||
"failed" tcl-parse-fail
|
||||
"failures" tcl-parse-failures)))
|
||||
@@ -1,14 +0,0 @@
|
||||
# expected: 10
|
||||
proc assert {expr_str} {
|
||||
set result [uplevel 1 [list expr $expr_str]]
|
||||
if {!$result} {
|
||||
error "Assertion failed: $expr_str"
|
||||
}
|
||||
}
|
||||
|
||||
set x 42
|
||||
assert {$x == 42}
|
||||
assert {$x > 0}
|
||||
set x 10
|
||||
assert {$x < 100}
|
||||
set x
|
||||
@@ -1,22 +0,0 @@
|
||||
# expected: done
|
||||
# Cooperative scheduler demo using coroutines (generator style)
|
||||
# coroutine eagerly collects all yields; invoking the coroutine name pops values
|
||||
|
||||
proc counter {n max} {
|
||||
while {$n < $max} {
|
||||
yield $n
|
||||
incr n
|
||||
}
|
||||
yield done
|
||||
}
|
||||
|
||||
coroutine gen1 counter 0 3
|
||||
|
||||
# gen1 yields: 0 1 2 done
|
||||
set out {}
|
||||
for {set i 0} {$i < 4} {incr i} {
|
||||
lappend out [gen1]
|
||||
}
|
||||
|
||||
# last val is "done"
|
||||
lindex $out 3
|
||||
@@ -1,14 +0,0 @@
|
||||
# expected: 13
|
||||
proc for-each-line {var lines body} {
|
||||
foreach item $lines {
|
||||
uplevel 1 [list set $var $item]
|
||||
uplevel 1 $body
|
||||
}
|
||||
}
|
||||
|
||||
# Usage: accumulate lengths of each "line"
|
||||
set total 0
|
||||
for-each-line line {hello world foo} {
|
||||
incr total [string length $line]
|
||||
}
|
||||
set total
|
||||
@@ -1,14 +0,0 @@
|
||||
# expected: 100 999
|
||||
proc with-temp-var {varname tempval body} {
|
||||
upvar 1 $varname v
|
||||
set saved $v
|
||||
set v $tempval
|
||||
uplevel 1 $body
|
||||
set v $saved
|
||||
}
|
||||
|
||||
set x 100
|
||||
with-temp-var x 999 {
|
||||
set captured $x
|
||||
}
|
||||
list $x $captured
|
||||
@@ -1,308 +0,0 @@
|
||||
(define tcl-ws? (fn (c) (or (= c " ") (= c "\t") (= c "\r"))))
|
||||
|
||||
(define tcl-alpha?
|
||||
(fn (c)
|
||||
(and
|
||||
(not (= c nil))
|
||||
(or (and (>= c "a") (<= c "z")) (and (>= c "A") (<= c "Z"))))))
|
||||
|
||||
(define tcl-digit?
|
||||
(fn (c) (and (not (= c nil)) (>= c "0") (<= c "9"))))
|
||||
|
||||
(define tcl-ident-start?
|
||||
(fn (c) (or (tcl-alpha? c) (= c "_"))))
|
||||
|
||||
(define tcl-ident-char?
|
||||
(fn (c) (or (tcl-ident-start? c) (tcl-digit? c))))
|
||||
|
||||
(define tcl-tokenize
|
||||
(fn (src)
|
||||
(let ((pos 0) (src-len (len src)) (commands (list)))
|
||||
|
||||
(define char-at
|
||||
(fn (off)
|
||||
(if (< (+ pos off) src-len) (nth src (+ pos off)) nil)))
|
||||
|
||||
(define cur (fn () (char-at 0)))
|
||||
|
||||
(define advance! (fn (n) (set! pos (+ pos n))))
|
||||
|
||||
(define skip-ws!
|
||||
(fn ()
|
||||
(when (tcl-ws? (cur))
|
||||
(begin (advance! 1) (skip-ws!)))))
|
||||
|
||||
(define skip-to-eol!
|
||||
(fn ()
|
||||
(when (and (< pos src-len) (not (= (cur) "\n")))
|
||||
(begin (advance! 1) (skip-to-eol!)))))
|
||||
|
||||
(define skip-brace-content!
|
||||
(fn (d)
|
||||
(when (and (< pos src-len) (> d 0))
|
||||
(cond
|
||||
((= (cur) "{") (begin (advance! 1) (skip-brace-content! (+ d 1))))
|
||||
((= (cur) "}") (begin (advance! 1) (skip-brace-content! (- d 1))))
|
||||
(else (begin (advance! 1) (skip-brace-content! d)))))))
|
||||
|
||||
(define skip-dquote-content!
|
||||
(fn ()
|
||||
(when (and (< pos src-len) (not (= (cur) "\"")))
|
||||
(begin
|
||||
(when (= (cur) "\\") (advance! 1))
|
||||
(when (< pos src-len) (advance! 1))
|
||||
(skip-dquote-content!)))))
|
||||
|
||||
(define parse-bs
|
||||
(fn ()
|
||||
(advance! 1)
|
||||
(let ((c (cur)))
|
||||
(cond
|
||||
((= c nil) "\\")
|
||||
((= c "n") (begin (advance! 1) "\n"))
|
||||
((= c "t") (begin (advance! 1) "\t"))
|
||||
((= c "r") (begin (advance! 1) "\r"))
|
||||
((= c "\\") (begin (advance! 1) "\\"))
|
||||
((= c "[") (begin (advance! 1) "["))
|
||||
((= c "]") (begin (advance! 1) "]"))
|
||||
((= c "{") (begin (advance! 1) "{"))
|
||||
((= c "}") (begin (advance! 1) "}"))
|
||||
((= c "$") (begin (advance! 1) "$"))
|
||||
((= c ";") (begin (advance! 1) ";"))
|
||||
((= c "\"") (begin (advance! 1) "\""))
|
||||
((= c "'") (begin (advance! 1) "'"))
|
||||
((= c " ") (begin (advance! 1) " "))
|
||||
((= c "\n")
|
||||
(begin
|
||||
(advance! 1)
|
||||
(skip-ws!)
|
||||
" "))
|
||||
(else (begin (advance! 1) (str "\\" c)))))))
|
||||
|
||||
(define parse-cmd-sub
|
||||
(fn ()
|
||||
(advance! 1)
|
||||
(let ((start pos) (depth 1))
|
||||
(define scan!
|
||||
(fn ()
|
||||
(when (and (< pos src-len) (> depth 0))
|
||||
(cond
|
||||
((= (cur) "[")
|
||||
(begin (set! depth (+ depth 1)) (advance! 1) (scan!)))
|
||||
((= (cur) "]")
|
||||
(begin
|
||||
(set! depth (- depth 1))
|
||||
(when (> depth 0) (advance! 1))
|
||||
(scan!)))
|
||||
((= (cur) "{")
|
||||
(begin (advance! 1) (skip-brace-content! 1) (scan!)))
|
||||
((= (cur) "\"")
|
||||
(begin
|
||||
(advance! 1)
|
||||
(skip-dquote-content!)
|
||||
(when (= (cur) "\"") (advance! 1))
|
||||
(scan!)))
|
||||
((= (cur) "\\")
|
||||
(begin (advance! 1) (when (< pos src-len) (advance! 1)) (scan!)))
|
||||
(else (begin (advance! 1) (scan!)))))))
|
||||
(scan!)
|
||||
(let ((src-text (slice src start pos)))
|
||||
(begin
|
||||
(when (= (cur) "]") (advance! 1))
|
||||
{:type "cmd" :src src-text})))))
|
||||
|
||||
(define scan-name!
|
||||
(fn ()
|
||||
(when (and (< pos src-len) (not (= (cur) "}")))
|
||||
(begin (advance! 1) (scan-name!)))))
|
||||
|
||||
(define scan-ns-name!
|
||||
(fn ()
|
||||
(cond
|
||||
((tcl-ident-char? (cur))
|
||||
(begin (advance! 1) (scan-ns-name!)))
|
||||
((and (= (cur) ":") (= (char-at 1) ":"))
|
||||
(begin (advance! 2) (scan-ns-name!)))
|
||||
(else nil))))
|
||||
|
||||
(define scan-klit!
|
||||
(fn ()
|
||||
(when (and (< pos src-len)
|
||||
(not (= (cur) ")"))
|
||||
(not (= (cur) "$"))
|
||||
(not (= (cur) "["))
|
||||
(not (= (cur) "\\")))
|
||||
(begin (advance! 1) (scan-klit!)))))
|
||||
|
||||
(define scan-key!
|
||||
(fn (kp)
|
||||
(when (and (< pos src-len) (not (= (cur) ")")))
|
||||
(cond
|
||||
((= (cur) "$")
|
||||
(begin (append! kp (parse-var-sub)) (scan-key! kp)))
|
||||
((= (cur) "[")
|
||||
(begin (append! kp (parse-cmd-sub)) (scan-key! kp)))
|
||||
((= (cur) "\\")
|
||||
(begin
|
||||
(append! kp {:type "text" :value (parse-bs)})
|
||||
(scan-key! kp)))
|
||||
(else
|
||||
(let ((kstart pos))
|
||||
(begin
|
||||
(scan-klit!)
|
||||
(append! kp {:type "text" :value (slice src kstart pos)})
|
||||
(scan-key! kp))))))))
|
||||
|
||||
(define parse-var-sub
|
||||
(fn ()
|
||||
(advance! 1)
|
||||
(cond
|
||||
((= (cur) "{")
|
||||
(begin
|
||||
(advance! 1)
|
||||
(let ((start pos))
|
||||
(begin
|
||||
(scan-name!)
|
||||
(let ((name (slice src start pos)))
|
||||
(begin
|
||||
(when (= (cur) "}") (advance! 1))
|
||||
{:type "var" :name name}))))))
|
||||
((tcl-ident-start? (cur))
|
||||
(let ((start pos))
|
||||
(begin
|
||||
(scan-ns-name!)
|
||||
(let ((name (slice src start pos)))
|
||||
(if (= (cur) "(")
|
||||
(begin
|
||||
(advance! 1)
|
||||
(let ((key-parts (list)))
|
||||
(begin
|
||||
(scan-key! key-parts)
|
||||
(when (= (cur) ")") (advance! 1))
|
||||
{:type "var-arr" :name name :key key-parts})))
|
||||
{:type "var" :name name})))))
|
||||
(else {:type "text" :value "$"}))))
|
||||
|
||||
(define scan-lit!
|
||||
(fn (stop?)
|
||||
(when (and (< pos src-len)
|
||||
(not (stop? (cur)))
|
||||
(not (= (cur) "$"))
|
||||
(not (= (cur) "["))
|
||||
(not (= (cur) "\\")))
|
||||
(begin (advance! 1) (scan-lit! stop?)))))
|
||||
|
||||
(define parse-word-parts!
|
||||
(fn (parts stop?)
|
||||
(when (and (< pos src-len) (not (stop? (cur))))
|
||||
(cond
|
||||
((= (cur) "$")
|
||||
(begin (append! parts (parse-var-sub)) (parse-word-parts! parts stop?)))
|
||||
((= (cur) "[")
|
||||
(begin (append! parts (parse-cmd-sub)) (parse-word-parts! parts stop?)))
|
||||
((= (cur) "\\")
|
||||
(begin
|
||||
(append! parts {:type "text" :value (parse-bs)})
|
||||
(parse-word-parts! parts stop?)))
|
||||
(else
|
||||
(let ((start pos))
|
||||
(begin
|
||||
(scan-lit! stop?)
|
||||
(when (> pos start)
|
||||
(append! parts {:type "text" :value (slice src start pos)}))
|
||||
(parse-word-parts! parts stop?))))))))
|
||||
|
||||
(define parse-brace-word
|
||||
(fn ()
|
||||
(advance! 1)
|
||||
(let ((depth 1) (start pos))
|
||||
(define scan!
|
||||
(fn ()
|
||||
(when (and (< pos src-len) (> depth 0))
|
||||
(cond
|
||||
((= (cur) "{")
|
||||
(begin (set! depth (+ depth 1)) (advance! 1) (scan!)))
|
||||
((= (cur) "}")
|
||||
(begin (set! depth (- depth 1)) (when (> depth 0) (advance! 1)) (scan!)))
|
||||
(else (begin (advance! 1) (scan!)))))))
|
||||
(scan!)
|
||||
(let ((value (slice src start pos)))
|
||||
(begin
|
||||
(when (= (cur) "}") (advance! 1))
|
||||
{:type "braced" :value value})))))
|
||||
|
||||
(define parse-dquote-word
|
||||
(fn ()
|
||||
(advance! 1)
|
||||
(let ((parts (list)))
|
||||
(begin
|
||||
(parse-word-parts! parts (fn (c) (or (= c "\"") (= c nil))))
|
||||
(when (= (cur) "\"") (advance! 1))
|
||||
{:type "compound" :parts parts :quoted true}))))
|
||||
|
||||
(define parse-bare-word
|
||||
(fn ()
|
||||
(let ((parts (list)))
|
||||
(begin
|
||||
(parse-word-parts!
|
||||
parts
|
||||
(fn (c) (or (tcl-ws? c) (= c "\n") (= c ";") (= c nil))))
|
||||
{:type "compound" :parts parts :quoted false}))))
|
||||
|
||||
(define parse-word-no-expand
|
||||
(fn ()
|
||||
(cond
|
||||
((= (cur) "{") (parse-brace-word))
|
||||
((= (cur) "\"") (parse-dquote-word))
|
||||
(else (parse-bare-word)))))
|
||||
|
||||
(define parse-word
|
||||
(fn ()
|
||||
(cond
|
||||
((and (= (cur) "{") (= (char-at 1) "*") (= (char-at 2) "}"))
|
||||
(begin
|
||||
(advance! 3)
|
||||
{:type "expand" :word (parse-word-no-expand)}))
|
||||
((= (cur) "{") (parse-brace-word))
|
||||
((= (cur) "\"") (parse-dquote-word))
|
||||
(else (parse-bare-word)))))
|
||||
|
||||
(define parse-words!
|
||||
(fn (words)
|
||||
(skip-ws!)
|
||||
(cond
|
||||
((or (= (cur) nil) (= (cur) "\n") (= (cur) ";")) nil)
|
||||
((and (= (cur) "\\") (= (char-at 1) "\n"))
|
||||
(begin (advance! 2) (skip-ws!) (parse-words! words)))
|
||||
(else
|
||||
(begin
|
||||
(append! words (parse-word))
|
||||
(parse-words! words))))))
|
||||
|
||||
(define skip-seps!
|
||||
(fn ()
|
||||
(when (< pos src-len)
|
||||
(cond
|
||||
((or (tcl-ws? (cur)) (= (cur) "\n") (= (cur) ";"))
|
||||
(begin (advance! 1) (skip-seps!)))
|
||||
((and (= (cur) "\\") (= (char-at 1) "\n"))
|
||||
(begin (advance! 2) (skip-seps!)))
|
||||
(else nil)))))
|
||||
|
||||
(define parse-all!
|
||||
(fn ()
|
||||
(skip-seps!)
|
||||
(when (< pos src-len)
|
||||
(cond
|
||||
((= (cur) "#")
|
||||
(begin (skip-to-eol!) (parse-all!)))
|
||||
(else
|
||||
(let ((words (list)))
|
||||
(begin
|
||||
(parse-words! words)
|
||||
(when (> (len words) 0)
|
||||
(append! commands {:type "command" :words words}))
|
||||
(parse-all!))))))))
|
||||
|
||||
(parse-all!)
|
||||
commands)))
|
||||
@@ -11,7 +11,7 @@ isolation: worktree
|
||||
|
||||
## Prompt
|
||||
|
||||
You are the sole background agent working `/root/rose-ash/plans/ruby-on-sx.md`. Isolated worktree, forever, one commit per feature. Never push.
|
||||
You are the sole background agent working `/root/rose-ash/plans/ruby-on-sx.md`. Isolated worktree, forever, one commit per feature. Push to `origin/loops/ruby` after every commit.
|
||||
|
||||
## Restart baseline — check before iterating
|
||||
|
||||
@@ -42,7 +42,7 @@ Every iteration: implement → test → commit → tick `[ ]` → Progress log
|
||||
- **Shared-file issues** → plan's Blockers with minimal repro.
|
||||
- **Delimited continuations** are in `lib/callcc.sx` + `spec/evaluator.sx` Step 5. `sx_summarise` spec/evaluator.sx first — 2300+ lines.
|
||||
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits.
|
||||
- **Worktree:** commit locally. Never push. Never touch `main`.
|
||||
- **Worktree:** commit, then push to `origin/loops/ruby`. Never touch `main`.
|
||||
- **Commit granularity:** one feature per commit.
|
||||
- **Plan file:** update Progress log + tick boxes every commit.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ isolation: worktree
|
||||
|
||||
## Prompt
|
||||
|
||||
You are the sole background agent working `/root/rose-ash/plans/tcl-on-sx.md`. Isolated worktree, forever, one commit per feature. Push to `origin/loops/tcl` after every commit.
|
||||
You are the sole background agent working `/root/rose-ash/plans/tcl-on-sx.md`. Isolated worktree, forever, one commit per feature. Never push.
|
||||
|
||||
## Restart baseline — check before iterating
|
||||
|
||||
@@ -42,7 +42,7 @@ Every iteration: implement → test → commit → tick `[ ]` → Progress log
|
||||
- **Shared-file issues** → plan's Blockers with minimal repro.
|
||||
- **Delimited continuations** are in `lib/callcc.sx` + `spec/evaluator.sx` Step 5. `sx_summarise` spec/evaluator.sx first — 2300+ lines.
|
||||
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits.
|
||||
- **Worktree:** commit, then push to `origin/loops/tcl`. Never touch `main`.
|
||||
- **Worktree:** commit locally. Never push. Never touch `main`.
|
||||
- **Commit granularity:** one feature per commit.
|
||||
- **Plan file:** update Progress log + tick boxes every commit.
|
||||
|
||||
|
||||
@@ -51,11 +51,11 @@ Core mapping:
|
||||
## Roadmap
|
||||
|
||||
### Phase 1 — tokenizer + parser
|
||||
- [ ] Tokenizer: keywords (`def end class module if unless while until do return yield begin rescue ensure case when then else elsif`), identifiers (lowercase = local/method, `@` = ivar, `@@` = cvar, `$` = global, uppercase = constant), numbers (int, float, `0x` `0o` `0b`, `_` separators), strings (`"…"` interpolation, `'…'` literal, `%w[a b c]`, `%i[a b c]`), symbols `:foo` `:"…"`, operators (`+ - * / % ** == != < > <= >= <=> === =~ !~ << >> & | ^ ~ ! && || and or not`), `:: . , ; ( ) [ ] { } -> => |`, comments `#`
|
||||
- [ ] Parser: program is sequence of statements separated by newlines or `;`; method def `def name(args) … end`; class `class Foo < Bar … end`; module `module M … end`; block `do |a, b| … end` and `{ |a, b| … }`; call sugar (no parens), `obj.method`, `Mod::Const`; arg shapes (positional, default, splat `*args`, double-splat `**opts`, block `&blk`)
|
||||
- [x] Tokenizer: keywords (`def end class module if unless while until do return yield begin rescue ensure case when then else elsif`), identifiers (lowercase = local/method, `@` = ivar, `@@` = cvar, `$` = global, uppercase = constant), numbers (int, float, `0x` `0o` `0b`, `_` separators), strings (`"…"` interpolation, `'…'` literal, `%w[a b c]`, `%i[a b c]`), symbols `:foo` `:"…"`, operators (`+ - * / % ** == != < > <= >= <=> === =~ !~ << >> & | ^ ~ ! && || and or not`), `:: . , ; ( ) [ ] { } -> => |`, comments `#`
|
||||
- [x] Parser: program is sequence of statements separated by newlines or `;`; method def `def name(args) … end`; class `class Foo < Bar … end`; module `module M … end`; block `do |a, b| … end` and `{ |a, b| … }`; call sugar (no parens), `obj.method`, `Mod::Const`; arg shapes (positional, default, splat `*args`, double-splat `**opts`, block `&blk`)
|
||||
- [ ] If/while/case expressions (return values), `unless`/`until`, postfix modifiers
|
||||
- [ ] Begin/rescue/ensure/retry, raise, raise with class+message
|
||||
- [ ] Unit tests in `lib/ruby/tests/parse.sx`
|
||||
- [x] Unit tests in `lib/ruby/tests/parse.sx`
|
||||
|
||||
### Phase 2 — object model + sequential eval
|
||||
- [ ] Class table bootstrap: `BasicObject`, `Object`, `Kernel`, `Module`, `Class`, `Numeric`, `Integer`, `Float`, `String`, `Symbol`, `Array`, `Hash`, `Range`, `NilClass`, `TrueClass`, `FalseClass`, `Proc`, `Method`
|
||||
@@ -117,7 +117,8 @@ Core mapping:
|
||||
|
||||
_Newest first._
|
||||
|
||||
- _(none yet)_
|
||||
- 2026-04-25: Phase 1 parser complete — `lib/ruby/parser.sx` (rb-parse/rb-parse-str) + `lib/ruby/tests/parse.sx` (83/83 tests). Program, method-def (all param shapes), class/module/sclass, blocks (do/brace), method calls (parens + no-parens + chains), const-path, assignment (=, op=, massign), binary/unary ops with precedence, array/hash literals, return/yield/break/next/redo/raise, indexing.
|
||||
- 2026-04-25: Phase 1 tokenizer complete — `lib/ruby/tokenizer.sx` + `lib/ruby/tests/tokenizer.sx` (107/107 tests). Keywords, identifiers (@ivar @@cvar $gvar), numbers (dec/hex/octal/binary/float), strings (dq with interpolation kept raw, sq), symbols, %w/%i literals, operators (all compound forms), punctuation, comments, line/col tracking.
|
||||
|
||||
## Blockers
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ Core mapping:
|
||||
## Roadmap
|
||||
|
||||
### Phase 1 — tokenizer + parser (the Dodekalogue)
|
||||
- [x] Tokenizer applying the 12 rules:
|
||||
- [ ] Tokenizer applying the 12 rules:
|
||||
1. Commands separated by `;` or newlines
|
||||
2. Words separated by whitespace within a command
|
||||
3. Double-quoted words: `\` escapes + `[…]` + `${…}` + `$var` substitution
|
||||
@@ -63,76 +63,64 @@ Core mapping:
|
||||
10. Order of substitution is left-to-right, single-pass
|
||||
11. Substitutions don't recurse — substituted text is not re-parsed
|
||||
12. The result of any substitution is the value, not a new script
|
||||
- [x] Parser: script = list of commands; command = list of words; word = literal string + list of substitutions
|
||||
- [x] Unit tests in `lib/tcl/tests/parse.sx`
|
||||
- [ ] Parser: script = list of commands; command = list of words; word = literal string + list of substitutions
|
||||
- [ ] Unit tests in `lib/tcl/tests/parse.sx`
|
||||
|
||||
### Phase 2 — sequential eval + core commands
|
||||
- [x] `tcl-eval-script`: walk command list, dispatch each first-word into command table
|
||||
- [x] Core commands: `set`, `unset`, `incr`, `append`, `lappend`, `puts`, `gets`, `expr`, `if`, `while`, `for`, `foreach`, `switch`, `break`, `continue`, `return`, `error`, `eval`, `subst`, `format`, `scan`
|
||||
- [x] `expr` is its own mini-language — operator precedence, function calls (`sin`, `sqrt`, `pow`, `abs`, `int`, `double`), variable substitution, command substitution
|
||||
- [x] String commands: `string length`, `string index`, `string range`, `string compare`, `string match`, `string toupper`, `string tolower`, `string trim`, `string map`, `string repeat`, `string first`, `string last`, `string is`, `string cat`
|
||||
- [x] List commands: `list`, `lindex`, `lrange`, `llength`, `lreverse`, `lsearch`, `lsort`, `lsort -integer/-real/-dictionary`, `lreplace`, `linsert`, `concat`, `split`, `join`
|
||||
- [x] Dict commands: `dict create`, `dict get`, `dict set`, `dict unset`, `dict exists`, `dict keys`, `dict values`, `dict size`, `dict for`, `dict update`, `dict merge`
|
||||
- [x] 60+ tests in `lib/tcl/tests/eval.sx`
|
||||
- [ ] `tcl-eval-script`: walk command list, dispatch each first-word into command table
|
||||
- [ ] Core commands: `set`, `unset`, `incr`, `append`, `lappend`, `puts`, `gets`, `expr`, `if`, `while`, `for`, `foreach`, `switch`, `break`, `continue`, `return`, `error`, `eval`, `subst`, `format`, `scan`
|
||||
- [ ] `expr` is its own mini-language — operator precedence, function calls (`sin`, `sqrt`, `pow`, `abs`, `int`, `double`), variable substitution, command substitution
|
||||
- [ ] String commands: `string length`, `string index`, `string range`, `string compare`, `string match`, `string toupper`, `string tolower`, `string trim`, `string map`, `string repeat`, `string first`, `string last`, `string is`, `string cat`
|
||||
- [ ] List commands: `list`, `lindex`, `lrange`, `llength`, `lreverse`, `lsearch`, `lsort`, `lsort -integer/-real/-dictionary`, `lreplace`, `linsert`, `concat`, `split`, `join`
|
||||
- [ ] Dict commands: `dict create`, `dict get`, `dict set`, `dict unset`, `dict exists`, `dict keys`, `dict values`, `dict size`, `dict for`, `dict update`, `dict merge`
|
||||
- [ ] 60+ tests in `lib/tcl/tests/eval.sx`
|
||||
|
||||
### Phase 3 — proc + uplevel + upvar (THE SHOWCASE)
|
||||
- [x] `proc name args body` — register user-defined command; args supports defaults `{name default}` and rest `args`
|
||||
- [x] Frame stack: each proc call pushes a frame with locals dict; pop on return
|
||||
- [x] `uplevel ?level? script` — evaluate `script` in level-N frame's env; default level is 1 (caller). `#0` is global, `#1` is relative-1
|
||||
- [x] `upvar ?level? otherVar localVar ?…?` — alias localVar to a variable in level-N frame; reads/writes go through the alias
|
||||
- [x] `info level`, `info level N`, `info frame`, `info vars`, `info locals`, `info globals`, `info commands`, `info procs`, `info args`, `info body`
|
||||
- [x] `global var ?…?` — alias to global frame (sugar for `upvar #0 var var`)
|
||||
- [x] `variable name ?value?` — namespace-scoped global
|
||||
- [x] Classic programs in `lib/tcl/tests/programs/`:
|
||||
- [x] `for-each-line.tcl` — define your own loop construct using `uplevel`
|
||||
- [x] `assert.tcl` — assertion macro that reports caller's line
|
||||
- [x] `with-temp-var.tcl` — scoped variable rebind via `upvar`
|
||||
- [x] `lib/tcl/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md`
|
||||
- [ ] `proc name args body` — register user-defined command; args supports defaults `{name default}` and rest `args`
|
||||
- [ ] Frame stack: each proc call pushes a frame with locals dict; pop on return
|
||||
- [ ] `uplevel ?level? script` — evaluate `script` in level-N frame's env; default level is 1 (caller). `#0` is global, `#1` is relative-1
|
||||
- [ ] `upvar ?level? otherVar localVar ?…?` — alias localVar to a variable in level-N frame; reads/writes go through the alias
|
||||
- [ ] `info level`, `info level N`, `info frame`, `info vars`, `info locals`, `info globals`, `info commands`, `info procs`, `info args`, `info body`
|
||||
- [ ] `global var ?…?` — alias to global frame (sugar for `upvar #0 var var`)
|
||||
- [ ] `variable name ?value?` — namespace-scoped global
|
||||
- [ ] Classic programs in `lib/tcl/tests/programs/`:
|
||||
- [ ] `for-each-line.tcl` — define your own loop construct using `uplevel`
|
||||
- [ ] `assert.tcl` — assertion macro that reports caller's line
|
||||
- [ ] `with-temp-var.tcl` — scoped variable rebind via `upvar`
|
||||
- [ ] `lib/tcl/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md`
|
||||
|
||||
### Phase 4 — control flow + error handling
|
||||
- [x] `return -code (ok|error|return|break|continue|N) -errorinfo … -errorcode … -level N value`
|
||||
- [x] `catch script ?resultVar? ?optionsVar?` — runs script, returns code; sets resultVar to return value/message; optionsVar to the dict
|
||||
- [x] `try script ?on code var body ...? ?trap pattern var body...? ?finally body?`
|
||||
- [x] `throw type message`
|
||||
- [x] `error message ?info? ?code?`
|
||||
- [x] Stack-trace with `errorInfo` / `errorCode`
|
||||
- [x] 30+ tests in `lib/tcl/tests/error.sx`
|
||||
- [ ] `return -code (ok|error|return|break|continue|N) -errorinfo … -errorcode … -level N value`
|
||||
- [ ] `catch script ?resultVar? ?optionsVar?` — runs script, returns code; sets resultVar to return value/message; optionsVar to the dict
|
||||
- [ ] `try script ?on code var body ...? ?trap pattern var body...? ?finally body?`
|
||||
- [ ] `throw type message`
|
||||
- [ ] `error message ?info? ?code?`
|
||||
- [ ] Stack-trace with `errorInfo` / `errorCode`
|
||||
- [ ] 30+ tests in `lib/tcl/tests/error.sx`
|
||||
|
||||
### Phase 5 — namespaces + ensembles
|
||||
- [x] `namespace eval ns body`, `namespace current`, `namespace which`, `namespace import`, `namespace export`, `namespace forget`, `namespace delete`
|
||||
- [x] Qualified names: `::ns::cmd`, `::ns::var`
|
||||
- [x] Ensembles: `namespace ensemble create -map { sub1 cmd1 sub2 cmd2 }`
|
||||
- [x] `namespace path` for resolution chain
|
||||
- [x] `proc` and `variable` work inside namespaces
|
||||
- [ ] `namespace eval ns body`, `namespace current`, `namespace which`, `namespace import`, `namespace export`, `namespace forget`, `namespace delete`
|
||||
- [ ] Qualified names: `::ns::cmd`, `::ns::var`
|
||||
- [ ] Ensembles: `namespace ensemble create -map { sub1 cmd1 sub2 cmd2 }`
|
||||
- [ ] `namespace path` for resolution chain
|
||||
- [ ] `proc` and `variable` work inside namespaces
|
||||
|
||||
### Phase 6 — coroutines + drive corpus
|
||||
- [x] `coroutine name cmd ?args…?` — start a coroutine; future calls to `name` resume it
|
||||
- [x] `yield ?value?` — suspend, return value to resumer
|
||||
- [x] `yieldto cmd ?args…?` — symmetric transfer
|
||||
- [x] `coroutine` semantics built on fibers (same delcc primitive as Ruby fibers)
|
||||
- [x] Classic programs: `event-loop.tcl` — cooperative scheduler with multiple coroutines
|
||||
- [x] System: `clock seconds`, `clock format`, `clock scan` (subset)
|
||||
- [x] File I/O: `open`, `close`, `read`, `gets`, `puts -nonewline`, `flush`, `eof`, `seek`, `tell`
|
||||
- [x] Drive corpus to 150+ green
|
||||
- [x] Idiom corpus — `lib/tcl/tests/idioms.sx` covering classic Welch/Jones idioms
|
||||
- [ ] `coroutine name cmd ?args…?` — start a coroutine; future calls to `name` resume it
|
||||
- [ ] `yield ?value?` — suspend, return value to resumer
|
||||
- [ ] `yieldto cmd ?args…?` — symmetric transfer
|
||||
- [ ] `coroutine` semantics built on fibers (same delcc primitive as Ruby fibers)
|
||||
- [ ] Classic programs: `event-loop.tcl` — cooperative scheduler with multiple coroutines
|
||||
- [ ] System: `clock seconds`, `clock format`, `clock scan` (subset)
|
||||
- [ ] File I/O: `open`, `close`, `read`, `gets`, `puts -nonewline`, `flush`, `eof`, `seek`, `tell`
|
||||
- [ ] Drive corpus to 150+ green
|
||||
- [ ] Idiom corpus — `lib/tcl/tests/idioms.sx` covering classic Welch/Jones idioms
|
||||
|
||||
## Progress log
|
||||
|
||||
_Newest first._
|
||||
|
||||
- 2026-05-06: Phase 6 coroutines+clock+file+idioms — generator coroutines, clock/file stubs, 20 coroutine + 20 idiom tests, event-loop.tcl, 329 tests green
|
||||
- 2026-05-06: Phase 5 namespaces+ensembles — namespace eval/current/which/exists/delete/import/ensemble, qualified names, 289 tests green (22 new namespace tests)
|
||||
- 2026-05-06: Phase 4 error handling — catch/try/throw/return-code/errorinfo/errorcode, 267 tests green (39 new error tests)
|
||||
- 2026-05-06: Phase 3 conformance.sh + classic programs — 3/3 PASS (for-each-line/assert/with-temp-var), 228 tests green
|
||||
- 2026-05-06: Phase 3 proc+uplevel+upvar+info+global — frame stack, isolated proc scope, alias-following var access, 225 tests green (67 parse + 158 eval)
|
||||
- 2026-05-06: Phase 2 dict commands — 13 subcommands (create/get/set/unset/exists/keys/values/size/for/update/merge/incr/append), 206 tests green (67 parse + 139 eval)
|
||||
- 2026-05-06: Phase 2 list commands — 12 commands (list/lindex/lrange/llength/lreverse/lsearch/lsort/lreplace/linsert/concat/split/join), 182 tests green (67 parse + 115 eval)
|
||||
- 2026-05-06: Phase 2 string commands — 16 subcommands (length/index/range/compare/match/toupper/tolower/trim/map/repeat/first/last/is/cat), 156 tests green (67 parse + 89 eval)
|
||||
- 2026-05-06: Phase 2 expr mini-language — recursive descent parser, operator precedence, parens, unary ops, pow/sqrt/abs/max/min/int/double, 127 tests green (67 parse + 60 eval)
|
||||
- 2026-04-26: Phase 2 core commands — if/while/for/foreach/switch/break/continue/return/error/unset/lappend/eval/expr + :code control flow, 107 tests green (67 parse + 40 eval)
|
||||
- 2026-04-26: Phase 2 eval engine — `lib/tcl/runtime.sx`, tcl-eval-script + set/puts/incr/append, 87 tests green (67 parse + 20 eval)
|
||||
- 2026-04-25: Phase 1 parser — `lib/tcl/parser.sx`, word-simple?/word-literal helpers, 67 tests green, commit 6ee05259
|
||||
- 2026-04-25: Phase 1 tokenizer (Dodekalogue) — `lib/tcl/tokenizer.sx`, 52 tests green, commit 666e29d5
|
||||
- _(none yet)_
|
||||
|
||||
## Blockers
|
||||
|
||||
|
||||
Reference in New Issue
Block a user