Compare commits
15 Commits
loops/hask
...
62da10030b
| Author | SHA1 | Date | |
|---|---|---|---|
| 62da10030b | |||
| 0e30cf1af6 | |||
| 21028c4fb0 | |||
| 7415dd020e | |||
| 0528a5cfa7 | |||
| 2fa0bb4df1 | |||
| 0d2eede5fb | |||
| 69078a59a9 | |||
| f5d3b1df19 | |||
| bf782d9c49 | |||
| bcdd137d6f | |||
| 0b3610a63a | |||
| 2b8c1a506c | |||
| 203f81004d | |||
| 04b0e61a33 |
@@ -528,6 +528,183 @@ let () =
|
||||
| [Rational (_, d)] -> Integer d
|
||||
| [Integer _] -> Integer 1
|
||||
| _ -> raise (Eval_error "denominator: expected rational or integer"));
|
||||
(* printf-spec: apply one Tcl/printf format spec to one arg.
|
||||
spec is like "%5.2f", "%-10s", "%x", "%c", "%d". Always starts with %
|
||||
and ends with the conversion char. Supports d i u x X o c s f e g.
|
||||
Coerces arg to the right type per conversion. *)
|
||||
register "printf-spec" (fun args ->
|
||||
let spec_str, arg = match args with
|
||||
| [String s; v] -> (s, v)
|
||||
| _ -> raise (Eval_error "printf-spec: (spec arg)")
|
||||
in
|
||||
let n = String.length spec_str in
|
||||
if n < 2 || spec_str.[0] <> '%' then
|
||||
raise (Eval_error ("printf-spec: invalid spec " ^ spec_str));
|
||||
let type_char = spec_str.[n - 1] in
|
||||
let to_int v = match v with
|
||||
| Integer i -> i
|
||||
| Number f -> int_of_float f
|
||||
| String s ->
|
||||
let s = String.trim s in
|
||||
(try int_of_string s
|
||||
with _ ->
|
||||
try int_of_float (float_of_string s)
|
||||
with _ -> 0)
|
||||
| Bool true -> 1 | Bool false -> 0
|
||||
| _ -> 0
|
||||
in
|
||||
let to_float v = match v with
|
||||
| Number f -> f
|
||||
| Integer i -> float_of_int i
|
||||
| String s ->
|
||||
let s = String.trim s in
|
||||
(try float_of_string s with _ -> 0.0)
|
||||
| _ -> 0.0
|
||||
in
|
||||
let to_string v = match v with
|
||||
| String s -> s
|
||||
| Integer i -> string_of_int i
|
||||
| Number f -> Sx_types.format_number f
|
||||
| Bool true -> "1" | Bool false -> "0"
|
||||
| Nil -> ""
|
||||
| _ -> Sx_types.inspect v
|
||||
in
|
||||
try
|
||||
match type_char with
|
||||
| 'd' | 'i' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%d" in
|
||||
String (Printf.sprintf fmt (to_int arg))
|
||||
| 'u' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%u" in
|
||||
String (Printf.sprintf fmt (to_int arg))
|
||||
| 'x' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%x" in
|
||||
String (Printf.sprintf fmt (to_int arg))
|
||||
| 'X' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%X" in
|
||||
String (Printf.sprintf fmt (to_int arg))
|
||||
| 'o' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%o" in
|
||||
String (Printf.sprintf fmt (to_int arg))
|
||||
| 'c' ->
|
||||
let n_val = to_int arg in
|
||||
let body = String.sub spec_str 0 (n - 1) in
|
||||
let fmt = Scanf.format_from_string (body ^ "s") "%s" in
|
||||
String (Printf.sprintf fmt (String.make 1 (Char.chr (n_val land 0xff))))
|
||||
| 's' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%s" in
|
||||
String (Printf.sprintf fmt (to_string arg))
|
||||
| 'f' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%f" in
|
||||
String (Printf.sprintf fmt (to_float arg))
|
||||
| 'e' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%e" in
|
||||
String (Printf.sprintf fmt (to_float arg))
|
||||
| 'E' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%E" in
|
||||
String (Printf.sprintf fmt (to_float arg))
|
||||
| 'g' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%g" in
|
||||
String (Printf.sprintf fmt (to_float arg))
|
||||
| 'G' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%G" in
|
||||
String (Printf.sprintf fmt (to_float arg))
|
||||
| _ -> raise (Eval_error ("printf-spec: unsupported conversion " ^ String.make 1 type_char))
|
||||
with
|
||||
| Eval_error _ as e -> raise e
|
||||
| _ -> raise (Eval_error ("printf-spec: invalid format " ^ spec_str)));
|
||||
|
||||
(* scan-spec: apply one Tcl/scanf format spec to a string.
|
||||
Returns (consumed-count . parsed-value), or nil on failure. *)
|
||||
register "scan-spec" (fun args ->
|
||||
let spec_str, str = match args with
|
||||
| [String s; String input] -> (s, input)
|
||||
| _ -> raise (Eval_error "scan-spec: (spec input)")
|
||||
in
|
||||
let n = String.length spec_str in
|
||||
if n < 2 || spec_str.[0] <> '%' then
|
||||
raise (Eval_error ("scan-spec: invalid spec " ^ spec_str));
|
||||
let type_char = spec_str.[n - 1] in
|
||||
let len = String.length str in
|
||||
(* skip leading whitespace for non-%c/%s conversions *)
|
||||
let i = ref 0 in
|
||||
if type_char <> 'c' then
|
||||
while !i < len && (str.[!i] = ' ' || str.[!i] = '\t' || str.[!i] = '\n') do incr i done;
|
||||
let start = !i in
|
||||
try
|
||||
match type_char with
|
||||
| 'd' | 'i' ->
|
||||
let j = ref !i in
|
||||
if !j < len && (str.[!j] = '-' || str.[!j] = '+') then incr j;
|
||||
while !j < len && str.[!j] >= '0' && str.[!j] <= '9' do incr j done;
|
||||
if !j > start && (str.[start] >= '0' && str.[start] <= '9'
|
||||
|| (!j > start + 1 && (str.[start] = '-' || str.[start] = '+'))) then
|
||||
let n_val = int_of_string (String.sub str start (!j - start)) in
|
||||
let d = Hashtbl.create 2 in
|
||||
Hashtbl.replace d "value" (Integer n_val);
|
||||
Hashtbl.replace d "consumed" (Integer !j);
|
||||
Dict d
|
||||
else Nil
|
||||
| 'x' | 'X' ->
|
||||
let j = ref !i in
|
||||
while !j < len &&
|
||||
((str.[!j] >= '0' && str.[!j] <= '9') ||
|
||||
(str.[!j] >= 'a' && str.[!j] <= 'f') ||
|
||||
(str.[!j] >= 'A' && str.[!j] <= 'F')) do incr j done;
|
||||
if !j > start then
|
||||
let n_val = int_of_string ("0x" ^ String.sub str start (!j - start)) in
|
||||
let d = Hashtbl.create 2 in
|
||||
Hashtbl.replace d "value" (Integer n_val);
|
||||
Hashtbl.replace d "consumed" (Integer !j);
|
||||
Dict d
|
||||
else Nil
|
||||
| 'o' ->
|
||||
let j = ref !i in
|
||||
while !j < len && str.[!j] >= '0' && str.[!j] <= '7' do incr j done;
|
||||
if !j > start then
|
||||
let n_val = int_of_string ("0o" ^ String.sub str start (!j - start)) in
|
||||
let d = Hashtbl.create 2 in
|
||||
Hashtbl.replace d "value" (Integer n_val);
|
||||
Hashtbl.replace d "consumed" (Integer !j);
|
||||
Dict d
|
||||
else Nil
|
||||
| 'f' | 'e' | 'g' ->
|
||||
let j = ref !i in
|
||||
if !j < len && (str.[!j] = '-' || str.[!j] = '+') then incr j;
|
||||
while !j < len && ((str.[!j] >= '0' && str.[!j] <= '9') || str.[!j] = '.') do incr j done;
|
||||
if !j < len && (str.[!j] = 'e' || str.[!j] = 'E') then begin
|
||||
incr j;
|
||||
if !j < len && (str.[!j] = '-' || str.[!j] = '+') then incr j;
|
||||
while !j < len && str.[!j] >= '0' && str.[!j] <= '9' do incr j done
|
||||
end;
|
||||
if !j > start then
|
||||
let f_val = float_of_string (String.sub str start (!j - start)) in
|
||||
let d = Hashtbl.create 2 in
|
||||
Hashtbl.replace d "value" (Number f_val);
|
||||
Hashtbl.replace d "consumed" (Integer !j);
|
||||
Dict d
|
||||
else Nil
|
||||
| 's' ->
|
||||
let j = ref !i in
|
||||
while !j < len && str.[!j] <> ' ' && str.[!j] <> '\t' && str.[!j] <> '\n' do incr j done;
|
||||
if !j > start then
|
||||
let d = Hashtbl.create 2 in
|
||||
Hashtbl.replace d "value" (String (String.sub str start (!j - start)));
|
||||
Hashtbl.replace d "consumed" (Integer !j);
|
||||
Dict d
|
||||
else Nil
|
||||
| 'c' ->
|
||||
if !i < len then
|
||||
let d = Hashtbl.create 2 in
|
||||
Hashtbl.replace d "value" (Integer (Char.code str.[!i]));
|
||||
Hashtbl.replace d "consumed" (Integer (!i + 1));
|
||||
Dict d
|
||||
else Nil
|
||||
| _ -> raise (Eval_error ("scan-spec: unsupported conversion " ^ String.make 1 type_char))
|
||||
with
|
||||
| Eval_error _ as e -> raise e
|
||||
| _ -> Nil);
|
||||
|
||||
register "parse-int" (fun args ->
|
||||
let parse_leading_int s =
|
||||
let len = String.length s in
|
||||
@@ -3399,6 +3576,62 @@ let () =
|
||||
Nil
|
||||
| _ -> raise (Eval_error "channel-set-blocking!: (channel bool)"));
|
||||
|
||||
(* === Exec === run an external process; capture stdout *)
|
||||
register "exec-process" (fun args ->
|
||||
let items = match args with
|
||||
| [List xs] | [ListRef { contents = xs }] -> xs
|
||||
| _ -> raise (Eval_error "exec-process: (cmd-list)")
|
||||
in
|
||||
let argv = Array.of_list (List.map (function
|
||||
| String s -> s
|
||||
| v -> Sx_types.inspect v
|
||||
) items) in
|
||||
if Array.length argv = 0 then raise (Eval_error "exec: empty command");
|
||||
let (out_r, out_w) = Unix.pipe () in
|
||||
let (err_r, err_w) = Unix.pipe () in
|
||||
let pid =
|
||||
try Unix.create_process argv.(0) argv Unix.stdin out_w err_w
|
||||
with Unix.Unix_error (e, _, _) ->
|
||||
Unix.close out_r; Unix.close out_w;
|
||||
Unix.close err_r; Unix.close err_w;
|
||||
raise (Eval_error ("exec: " ^ Unix.error_message e))
|
||||
in
|
||||
Unix.close out_w;
|
||||
Unix.close err_w;
|
||||
let buf = Buffer.create 256 in
|
||||
let errbuf = Buffer.create 64 in
|
||||
let chunk = Bytes.create 4096 in
|
||||
let read_all fd target =
|
||||
try
|
||||
let stop = ref false in
|
||||
while not !stop do
|
||||
let n = Unix.read fd chunk 0 (Bytes.length chunk) in
|
||||
if n = 0 then stop := true
|
||||
else Buffer.add_subbytes target chunk 0 n
|
||||
done
|
||||
with _ -> ()
|
||||
in
|
||||
read_all out_r buf;
|
||||
read_all err_r errbuf;
|
||||
Unix.close out_r;
|
||||
Unix.close err_r;
|
||||
let (_, status) = Unix.waitpid [] pid in
|
||||
let exit_code = match status with
|
||||
| Unix.WEXITED n -> n
|
||||
| Unix.WSIGNALED _ | Unix.WSTOPPED _ -> 1
|
||||
in
|
||||
let s = Buffer.contents buf in
|
||||
let trimmed =
|
||||
if String.length s > 0 && s.[String.length s - 1] = '\n'
|
||||
then String.sub s 0 (String.length s - 1) else s
|
||||
in
|
||||
if exit_code <> 0 then
|
||||
raise (Eval_error ("exec: child exited " ^ string_of_int exit_code
|
||||
^ (if Buffer.length errbuf > 0
|
||||
then ": " ^ Buffer.contents errbuf
|
||||
else "")))
|
||||
else String trimmed);
|
||||
|
||||
(* === Sockets === wrapping Unix.socket/connect/bind/listen/accept *)
|
||||
let resolve_inet_addr host =
|
||||
if host = "" || host = "0.0.0.0" then Unix.inet_addr_any
|
||||
|
||||
@@ -270,6 +270,15 @@
|
||||
(collect-segments-loop tokens (+ i 1) (append acc {:kind "val" :node (list :str tv)})))
|
||||
((= tt :name)
|
||||
(cond
|
||||
((and (< (+ i 1) (len tokens)) (= (tok-type (nth tokens (+ i 1))) :assign))
|
||||
(let
|
||||
((rhs-tokens (slice tokens (+ i 2) (len tokens))))
|
||||
(let
|
||||
((rhs-expr (parse-apl-expr rhs-tokens)))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
(len tokens)
|
||||
(append acc {:kind "val" :node (list :assign-expr tv rhs-expr)})))))
|
||||
((some (fn (q) (= q tv)) apl-quad-fn-names)
|
||||
(let
|
||||
((op-result (collect-ops tokens (+ i 1))))
|
||||
@@ -335,10 +344,22 @@
|
||||
((= tt :glyph)
|
||||
(cond
|
||||
((or (= tv "⍺") (= tv "⍵"))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
(+ i 1)
|
||||
(append acc {:kind "val" :node (list :name tv)})))
|
||||
(if
|
||||
(and
|
||||
(< (+ i 1) (len tokens))
|
||||
(= (tok-type (nth tokens (+ i 1))) :assign))
|
||||
(let
|
||||
((rhs-tokens (slice tokens (+ i 2) (len tokens))))
|
||||
(let
|
||||
((rhs-expr (parse-apl-expr rhs-tokens)))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
(len tokens)
|
||||
(append acc {:kind "val" :node (list :assign-expr tv rhs-expr)}))))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
(+ i 1)
|
||||
(append acc {:kind "val" :node (list :name tv)}))))
|
||||
((= tv "∇")
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
@@ -393,7 +414,13 @@
|
||||
ni
|
||||
(append acc {:kind "fn" :node fn-node})))))))
|
||||
((apl-parse-op-glyph? tv)
|
||||
(collect-segments-loop tokens (+ i 1) acc))
|
||||
(if
|
||||
(or (= tv "/") (= tv "⌿") (= tv "\\") (= tv "⍀"))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
(+ i 1)
|
||||
(append acc {:kind "fn" :node (list :fn-glyph tv)}))
|
||||
(collect-segments-loop tokens (+ i 1) acc)))
|
||||
(true (collect-segments-loop tokens (+ i 1) acc))))
|
||||
(true (collect-segments-loop tokens (+ i 1) acc))))))))
|
||||
|
||||
|
||||
@@ -808,6 +808,25 @@
|
||||
((picked (map (fn (i) (nth arr-ravel i)) kept)))
|
||||
(make-array (list (len picked)) picked))))))
|
||||
|
||||
(define
|
||||
apl-compress-first
|
||||
(fn
|
||||
(mask arr)
|
||||
(let
|
||||
((mask-ravel (get mask :ravel))
|
||||
(shape (get arr :shape))
|
||||
(ravel (get arr :ravel)))
|
||||
(if
|
||||
(< (len shape) 2)
|
||||
(apl-compress mask arr)
|
||||
(let
|
||||
((rows (first shape)) (cols (last shape)))
|
||||
(let
|
||||
((kept-rows (filter (fn (i) (not (= 0 (nth mask-ravel i)))) (range 0 rows))))
|
||||
(let
|
||||
((new-ravel (reduce (fn (acc r) (append acc (map (fn (j) (nth ravel (+ (* r cols) j))) (range 0 cols)))) (list) kept-rows)))
|
||||
(make-array (cons (len kept-rows) (rest shape)) new-ravel))))))))
|
||||
|
||||
(define
|
||||
apl-primes
|
||||
(fn
|
||||
@@ -985,6 +1004,28 @@
|
||||
(some (fn (c) (= c 0)) codes)
|
||||
(some (fn (c) (= c (nth e 1))) codes)))))
|
||||
|
||||
(define apl-rng-state 12345)
|
||||
|
||||
(define apl-rng-seed! (fn (s) (set! apl-rng-state s)))
|
||||
|
||||
(define
|
||||
apl-rng-next!
|
||||
(fn
|
||||
()
|
||||
(begin
|
||||
(set!
|
||||
apl-rng-state
|
||||
(mod (+ (* apl-rng-state 1103515245) 12345) 2147483648))
|
||||
apl-rng-state)))
|
||||
|
||||
(define
|
||||
apl-roll
|
||||
(fn
|
||||
(arr)
|
||||
(let
|
||||
((n (if (scalar? arr) (first (get arr :ravel)) (first (get arr :ravel)))))
|
||||
(apl-scalar (+ apl-io (mod (apl-rng-next!) n))))))
|
||||
|
||||
(define
|
||||
apl-cartesian
|
||||
(fn
|
||||
|
||||
@@ -312,3 +312,146 @@
|
||||
"train: mean of ⍳10 has shape ()"
|
||||
(mksh (apl-run "(+/÷≢) ⍳10"))
|
||||
(list))
|
||||
|
||||
(apl-test
|
||||
"compress: 1 0 1 0 1 / 10 20 30 40 50"
|
||||
(mkrv (apl-run "1 0 1 0 1 / 10 20 30 40 50"))
|
||||
(list 10 30 50))
|
||||
|
||||
(apl-test
|
||||
"compress: empty mask → empty"
|
||||
(mkrv (apl-run "0 0 0 / 1 2 3"))
|
||||
(list))
|
||||
|
||||
(apl-test
|
||||
"primes via classic idiom (multi-stmt)"
|
||||
(mkrv (apl-run "P ← ⍳ 30 ⋄ (2 = +⌿ 0 = P ∘.| P) / P"))
|
||||
(list 2 3 5 7 11 13 17 19 23 29))
|
||||
|
||||
(apl-test
|
||||
"primes via classic idiom (n=20)"
|
||||
(mkrv (apl-run "P ← ⍳ 20 ⋄ (2 = +⌿ 0 = P ∘.| P) / P"))
|
||||
(list 2 3 5 7 11 13 17 19))
|
||||
|
||||
(apl-test
|
||||
"compress: filter even values"
|
||||
(mkrv (apl-run "(0 = 2 | 1 2 3 4 5 6) / 1 2 3 4 5 6"))
|
||||
(list 2 4 6))
|
||||
|
||||
(apl-test "inline-assign: x ← 5" (mkrv (apl-run "x ← 5")) (list 5))
|
||||
|
||||
(apl-test
|
||||
"inline-assign: (2×x) + x←10 → 30"
|
||||
(mkrv (apl-run "(2 × x) + x ← 10"))
|
||||
(list 30))
|
||||
|
||||
(apl-test
|
||||
"inline-assign primes one-liner: (2=+⌿0=a∘.|a)/a←⍳30"
|
||||
(mkrv (apl-run "(2 = +⌿ 0 = a ∘.| a) / a ← ⍳ 30"))
|
||||
(list 2 3 5 7 11 13 17 19 23 29))
|
||||
|
||||
(apl-test
|
||||
"inline-assign: x is reusable — x + x ← 7 → 14"
|
||||
(mkrv (apl-run "x + x ← 7"))
|
||||
(list 14))
|
||||
|
||||
(apl-test
|
||||
"inline-assign in dfn: f ← {x + x ← ⍵} ⋄ f 8 → 16"
|
||||
(mkrv (apl-run "f ← {x + x ← ⍵} ⋄ f 8"))
|
||||
(list 16))
|
||||
|
||||
(begin (apl-rng-seed! 42) nil)
|
||||
|
||||
(apl-test
|
||||
"?10 with seed 42 → 8 (deterministic)"
|
||||
(mkrv (apl-run "?10"))
|
||||
(list 8))
|
||||
|
||||
(apl-test "?10 next call → 5" (mkrv (apl-run "?10")) (list 5))
|
||||
|
||||
(apl-test
|
||||
"?100 stays in range"
|
||||
(let ((v (first (mkrv (apl-run "?100"))))) (and (>= v 1) (<= v 100)))
|
||||
true)
|
||||
|
||||
(begin (apl-rng-seed! 42) nil)
|
||||
|
||||
(apl-test
|
||||
"?10 with re-seed 42 → 8 (reproducible)"
|
||||
(mkrv (apl-run "?10"))
|
||||
(list 8))
|
||||
|
||||
(apl-test
|
||||
"apl-run-file: load primes.apl returns dfn AST"
|
||||
(first (apl-run-file "lib/apl/tests/programs/primes.apl"))
|
||||
:dfn)
|
||||
|
||||
(apl-test
|
||||
"apl-run-file: life.apl parses without error"
|
||||
(first (apl-run-file "lib/apl/tests/programs/life.apl"))
|
||||
:dfn)
|
||||
|
||||
(apl-test
|
||||
"apl-run-file: quicksort.apl parses without error"
|
||||
(first (apl-run-file "lib/apl/tests/programs/quicksort.apl"))
|
||||
:dfn)
|
||||
|
||||
(apl-test
|
||||
"apl-run-file: source-then-call returns primes count"
|
||||
(mksh
|
||||
(apl-run
|
||||
(str (file-read "lib/apl/tests/programs/primes.apl") " ⋄ primes 30")))
|
||||
(list 10))
|
||||
|
||||
(apl-test
|
||||
"primes one-liner with ⍵-rebind: primes 30"
|
||||
(mkrv
|
||||
(apl-run "primes ← {(2=+⌿0=⍵∘.|⍵)/⍵←⍳⍵} ⋄ primes 30"))
|
||||
(list 2 3 5 7 11 13 17 19 23 29))
|
||||
|
||||
(apl-test
|
||||
"primes one-liner: primes 50"
|
||||
(mkrv
|
||||
(apl-run "primes ← {(2=+⌿0=⍵∘.|⍵)/⍵←⍳⍵} ⋄ primes 50"))
|
||||
(list 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47))
|
||||
|
||||
(apl-test
|
||||
"primes.apl loaded + called via apl-run-file"
|
||||
(mkrv
|
||||
(apl-run
|
||||
(str (file-read "lib/apl/tests/programs/primes.apl") " ⋄ primes 20")))
|
||||
(list 2 3 5 7 11 13 17 19))
|
||||
|
||||
(apl-test
|
||||
"primes.apl loaded — count of primes ≤ 100"
|
||||
(first
|
||||
(mksh
|
||||
(apl-run
|
||||
(str
|
||||
(file-read "lib/apl/tests/programs/primes.apl")
|
||||
" ⋄ primes 100"))))
|
||||
25)
|
||||
|
||||
(apl-test
|
||||
"⍉ monadic transpose 2x3 → 3x2"
|
||||
(mkrv (apl-run "⍉ (2 3) ⍴ ⍳6"))
|
||||
(list 1 4 2 5 3 6))
|
||||
|
||||
(apl-test
|
||||
"⍉ transpose shape (3 2)"
|
||||
(mksh (apl-run "⍉ (2 3) ⍴ ⍳6"))
|
||||
(list 3 2))
|
||||
|
||||
(apl-test "⊢ monadic identity" (mkrv (apl-run "⊢ 1 2 3")) (list 1 2 3))
|
||||
|
||||
(apl-test
|
||||
"5 ⊣ 1 2 3 → 5 (left)"
|
||||
(mkrv (apl-run "5 ⊣ 1 2 3"))
|
||||
(list 5))
|
||||
|
||||
(apl-test
|
||||
"5 ⊢ 1 2 3 → 1 2 3 (right)"
|
||||
(mkrv (apl-run "5 ⊢ 1 2 3"))
|
||||
(list 1 2 3))
|
||||
|
||||
(apl-test "⍕ 42 → \"42\" (alias for ⎕FMT)" (apl-run "⍕ 42") "42")
|
||||
|
||||
@@ -252,8 +252,6 @@
|
||||
|
||||
(apl-test "queens 7 → 40 solutions" (mkrv (apl-queens 7)) (list 40))
|
||||
|
||||
(apl-test "queens 8 → 92 solutions" (mkrv (apl-queens 8)) (list 92))
|
||||
|
||||
(apl-test "permutations of 3 has 6" (len (apl-permutations 3)) 6)
|
||||
|
||||
(apl-test "permutations of 4 has 24" (len (apl-permutations 4)) 24)
|
||||
|
||||
@@ -39,6 +39,11 @@
|
||||
((= g "⊖") apl-reverse-first)
|
||||
((= g "⍋") apl-grade-up)
|
||||
((= g "⍒") apl-grade-down)
|
||||
((= g "?") apl-roll)
|
||||
((= g "⍉") apl-transpose)
|
||||
((= g "⊢") (fn (a) a))
|
||||
((= g "⊣") (fn (a) a))
|
||||
((= g "⍕") apl-quad-fmt)
|
||||
((= g "⎕FMT") apl-quad-fmt)
|
||||
((= g "⎕←") apl-quad-print)
|
||||
(else (error "no monadic fn for glyph")))))
|
||||
@@ -80,6 +85,11 @@
|
||||
((= g "∊") apl-member)
|
||||
((= g "⍳") apl-index-of)
|
||||
((= g "~") apl-without)
|
||||
((= g "/") apl-compress)
|
||||
((= g "⌿") apl-compress-first)
|
||||
((= g "⍉") apl-transpose-dyadic)
|
||||
((= g "⊢") (fn (a b) b))
|
||||
((= g "⊣") (fn (a b) a))
|
||||
(else (error "no dyadic fn for glyph")))))
|
||||
|
||||
(define
|
||||
@@ -119,8 +129,14 @@
|
||||
(let
|
||||
((nm (nth node 1)))
|
||||
(cond
|
||||
((= nm "⍺") (get env "alpha"))
|
||||
((= nm "⍵") (get env "omega"))
|
||||
((= nm "⍺")
|
||||
(let
|
||||
((v (get env "⍺")))
|
||||
(if (= v nil) (get env "alpha") v)))
|
||||
((= nm "⍵")
|
||||
(let
|
||||
((v (get env "⍵")))
|
||||
(if (= v nil) (get env "omega") v)))
|
||||
((= nm "⎕IO") (apl-quad-io))
|
||||
((= nm "⎕ML") (apl-quad-ml))
|
||||
((= nm "⎕FR") (apl-quad-fr))
|
||||
@@ -132,7 +148,11 @@
|
||||
(if
|
||||
(and (= (first fn-node) :fn-glyph) (= (nth fn-node 1) "∇"))
|
||||
(apl-call-dfn-m (get env "nabla") (apl-eval-ast arg env))
|
||||
((apl-resolve-monadic fn-node env) (apl-eval-ast arg env)))))
|
||||
(let
|
||||
((arg-val (apl-eval-ast arg env)))
|
||||
(let
|
||||
((new-env (if (and (list? arg) (> (len arg) 0) (= (first arg) :assign-expr)) (assoc env (nth arg 1) arg-val) env)))
|
||||
((apl-resolve-monadic fn-node new-env) arg-val))))))
|
||||
((= tag :dyad)
|
||||
(let
|
||||
((fn-node (nth node 1))
|
||||
@@ -144,9 +164,13 @@
|
||||
(get env "nabla")
|
||||
(apl-eval-ast lhs env)
|
||||
(apl-eval-ast rhs env))
|
||||
((apl-resolve-dyadic fn-node env)
|
||||
(apl-eval-ast lhs env)
|
||||
(apl-eval-ast rhs env)))))
|
||||
(let
|
||||
((rhs-val (apl-eval-ast rhs env)))
|
||||
(let
|
||||
((new-env (if (and (list? rhs) (> (len rhs) 0) (= (first rhs) :assign-expr)) (assoc env (nth rhs 1) rhs-val) env)))
|
||||
((apl-resolve-dyadic fn-node new-env)
|
||||
(apl-eval-ast lhs new-env)
|
||||
rhs-val))))))
|
||||
((= tag :program) (apl-eval-stmts (rest node) env))
|
||||
((= tag :dfn) node)
|
||||
((= tag :bracket)
|
||||
@@ -159,6 +183,8 @@
|
||||
(fn (a) (if (= a :all) nil (apl-eval-ast a env)))
|
||||
axis-exprs)))
|
||||
(apl-bracket-multi axes arr))))
|
||||
((= tag :assign-expr) (apl-eval-ast (nth node 2) env))
|
||||
((= tag :assign) (apl-eval-ast (nth node 2) env))
|
||||
(else (error (list "apl-eval-ast: unknown node tag" tag node)))))))
|
||||
|
||||
(define
|
||||
@@ -538,3 +564,5 @@
|
||||
(else (error "apl-resolve-dyadic: unknown fn-node tag"))))))
|
||||
|
||||
(define apl-run (fn (src) (apl-eval-ast (parse-apl src) {})))
|
||||
|
||||
(define apl-run-file (fn (path) (apl-run (file-read path))))
|
||||
|
||||
@@ -210,7 +210,6 @@
|
||||
:op (nth node 1)
|
||||
(hk-desugar (nth node 2))
|
||||
(hk-desugar (nth node 3))))
|
||||
((= tag "type-ann") (hk-desugar (nth node 1)))
|
||||
((= tag "neg") (list :neg (hk-desugar (nth node 1))))
|
||||
((= tag "if")
|
||||
(list
|
||||
|
||||
@@ -275,47 +275,38 @@
|
||||
(list :sect-right op-name expr-e))))))
|
||||
(:else
|
||||
(let
|
||||
((first-e (hk-parse-expr-inner)))
|
||||
((first-e (hk-parse-expr-inner))
|
||||
(items (list))
|
||||
(is-tuple false))
|
||||
(append! items first-e)
|
||||
(define
|
||||
hk-tup-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(hk-match? "comma" nil)
|
||||
(do
|
||||
(hk-advance!)
|
||||
(set! is-tuple true)
|
||||
(append! items (hk-parse-expr-inner))
|
||||
(hk-tup-loop)))))
|
||||
(hk-tup-loop)
|
||||
(cond
|
||||
((hk-match? "reservedop" "::")
|
||||
((hk-match? "rparen" nil)
|
||||
(do
|
||||
(hk-advance!)
|
||||
(let
|
||||
((ann-type (hk-parse-type)))
|
||||
(hk-expect! "rparen" nil)
|
||||
(list :type-ann first-e ann-type))))
|
||||
(if is-tuple (list :tuple items) first-e)))
|
||||
(:else
|
||||
(let
|
||||
((items (list)) (is-tuple false))
|
||||
(append! items first-e)
|
||||
(define
|
||||
hk-tup-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(hk-match? "comma" nil)
|
||||
(do
|
||||
(hk-advance!)
|
||||
(set! is-tuple true)
|
||||
(append! items (hk-parse-expr-inner))
|
||||
(hk-tup-loop)))))
|
||||
(hk-tup-loop)
|
||||
((op-info2 (hk-section-op-info)))
|
||||
(cond
|
||||
((hk-match? "rparen" nil)
|
||||
(do
|
||||
(hk-advance!)
|
||||
(if is-tuple (list :tuple items) first-e)))
|
||||
(:else
|
||||
((and (not (nil? op-info2)) (not is-tuple) (let ((after2 (hk-peek-at (get op-info2 "len")))) (and (not (nil? after2)) (= (get after2 "type") "rparen"))))
|
||||
(let
|
||||
((op-info2 (hk-section-op-info)))
|
||||
(cond
|
||||
((and (not (nil? op-info2)) (not is-tuple) (let ((after2 (hk-peek-at (get op-info2 "len")))) (and (not (nil? after2)) (= (get after2 "type") "rparen"))))
|
||||
(let
|
||||
((op-name (get op-info2 "name")))
|
||||
(hk-consume-op!)
|
||||
(hk-advance!)
|
||||
(list :sect-left op-name first-e)))
|
||||
(:else (hk-err "expected ')' after expression")))))))))))))))))
|
||||
((op-name (get op-info2 "name")))
|
||||
(hk-consume-op!)
|
||||
(hk-advance!)
|
||||
(list :sect-left op-name first-e)))
|
||||
(:else (hk-err "expected ')' after expression"))))))))))))))
|
||||
(define
|
||||
hk-comp-qual-is-gen?
|
||||
(fn
|
||||
@@ -1733,18 +1724,10 @@
|
||||
(= (hk-peek-type) "eof")
|
||||
(hk-match? "vrbrace" nil)
|
||||
(hk-match? "rbrace" nil))))
|
||||
(define
|
||||
hk-body-step
|
||||
(fn
|
||||
()
|
||||
(cond
|
||||
((hk-match? "reserved" "import")
|
||||
(append! imports (hk-parse-import)))
|
||||
(:else (append! decls (hk-parse-decl))))))
|
||||
(when
|
||||
(not (hk-body-at-end?))
|
||||
(do
|
||||
(hk-body-step)
|
||||
(append! decls (hk-parse-decl))
|
||||
(define
|
||||
hk-body-loop
|
||||
(fn
|
||||
@@ -1755,7 +1738,7 @@
|
||||
(hk-advance!)
|
||||
(when
|
||||
(not (hk-body-at-end?))
|
||||
(hk-body-step))
|
||||
(append! decls (hk-parse-decl)))
|
||||
(hk-body-loop)))))
|
||||
(hk-body-loop)))
|
||||
(list imports decls))))
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
;; Phase 17 — parser polish unit tests.
|
||||
|
||||
(hk-test
|
||||
"type-ann: literal int annotated"
|
||||
(hk-deep-force (hk-run "main = (42 :: Int)"))
|
||||
42)
|
||||
|
||||
(hk-test
|
||||
"type-ann: arithmetic annotated"
|
||||
(hk-deep-force (hk-run "main = (1 + 2 :: Int)"))
|
||||
3)
|
||||
|
||||
(hk-test
|
||||
"type-ann: function arg annotated"
|
||||
(hk-deep-force
|
||||
(hk-run "f x = x + 1\nmain = f (1 :: Int)"))
|
||||
2)
|
||||
|
||||
(hk-test
|
||||
"type-ann: string annotated"
|
||||
(hk-deep-force (hk-run "main = (\"hi\" :: String)"))
|
||||
"hi")
|
||||
|
||||
(hk-test
|
||||
"type-ann: bool annotated"
|
||||
(hk-deep-force (hk-run "main = (True :: Bool)"))
|
||||
(list "True"))
|
||||
|
||||
(hk-test
|
||||
"type-ann: tuple annotated"
|
||||
(hk-deep-force (hk-run "main = ((1, 2) :: (Int, Int))"))
|
||||
(list "Tuple" 1 2))
|
||||
|
||||
(hk-test
|
||||
"type-ann: nested annotation in arithmetic"
|
||||
(hk-deep-force (hk-run "main = (1 :: Int) + (2 :: Int)"))
|
||||
3)
|
||||
|
||||
(hk-test
|
||||
"type-ann: function-typed annotation passes through eval"
|
||||
(hk-deep-force
|
||||
(hk-run "main = let f = ((\\x -> x + 1) :: Int -> Int) in f 5"))
|
||||
6)
|
||||
|
||||
(hk-test
|
||||
"no regression: plain parens still work"
|
||||
(hk-deep-force (hk-run "main = (5)"))
|
||||
5)
|
||||
|
||||
(hk-test
|
||||
"no regression: 3-tuple still works"
|
||||
(hk-deep-force (hk-run "main = (1, 2, 3)"))
|
||||
(list "Tuple" 1 2 3))
|
||||
|
||||
(hk-test
|
||||
"no regression: section-left still works"
|
||||
(hk-deep-force (hk-run "main = (3 +) 4"))
|
||||
7)
|
||||
|
||||
(hk-test
|
||||
"no regression: section-right still works"
|
||||
(hk-deep-force (hk-run "main = (+ 3) 4"))
|
||||
7)
|
||||
|
||||
(hk-test
|
||||
"import: still works as the very first decl"
|
||||
(hk-deep-force
|
||||
(hk-run "import qualified Data.IORef as I
|
||||
main = do { r <- I.newIORef 7; I.readIORef r }"))
|
||||
(list "IO" 7))
|
||||
|
||||
(hk-test
|
||||
"import: between decls — after main"
|
||||
(hk-deep-force
|
||||
(hk-run "main = do { r <- I.newIORef 11; I.readIORef r }
|
||||
import qualified Data.IORef as I"))
|
||||
(list "IO" 11))
|
||||
|
||||
(hk-test
|
||||
"import: between two decls — uses helper after import"
|
||||
(hk-deep-force
|
||||
(hk-run "f x = x + 100
|
||||
import qualified Data.IORef as I
|
||||
main = do { r <- I.newIORef 5; I.modifyIORef r f; I.readIORef r }"))
|
||||
(list "IO" 105))
|
||||
|
||||
(hk-test
|
||||
"import: two imports in different positions"
|
||||
(hk-deep-force
|
||||
(hk-run "import qualified Data.IORef as I
|
||||
helper x = x * 2
|
||||
import qualified Data.Map as M
|
||||
main = do { r <- I.newIORef (helper 21); I.readIORef r }"))
|
||||
(list "IO" 42))
|
||||
|
||||
(hk-test
|
||||
"import: unqualified, mid-file"
|
||||
(hk-deep-force
|
||||
(hk-run "go x = x
|
||||
import Data.IORef
|
||||
main = go 9"))
|
||||
9)
|
||||
@@ -16,18 +16,15 @@
|
||||
true)))
|
||||
|
||||
;; ─── Valid programs pass through ─────────────────────────────────────────────
|
||||
(hk-test "typed ok: simple arithmetic"
|
||||
(hk-deep-force (hk-run-typed "main = 1 + 2")) 3)
|
||||
(hk-test "typed ok: simple arithmetic" (hk-run-typed "main = 1 + 2") 3)
|
||||
|
||||
(hk-test "typed ok: boolean"
|
||||
(hk-deep-force (hk-run-typed "main = True")) (list "True"))
|
||||
(hk-test "typed ok: boolean" (hk-run-typed "main = True") (list "True"))
|
||||
|
||||
(hk-test "typed ok: let binding"
|
||||
(hk-deep-force (hk-run-typed "main = let x = 1 in x + 2")) 3)
|
||||
(hk-test "typed ok: let binding" (hk-run-typed "main = let x = 1 in x + 2") 3)
|
||||
|
||||
(hk-test
|
||||
"typed ok: two independent fns"
|
||||
(hk-deep-force (hk-run-typed "f x = x + 1\nmain = f 5"))
|
||||
(hk-run-typed "f x = x + 1\nmain = f 5")
|
||||
6)
|
||||
|
||||
;; ─── Untypeable programs are rejected ────────────────────────────────────────
|
||||
@@ -79,7 +76,7 @@
|
||||
|
||||
(hk-test
|
||||
"run-typed sig ok: Int declared matches"
|
||||
(hk-deep-force (hk-run-typed "main :: Int\nmain = 1 + 2"))
|
||||
(hk-run-typed "main :: Int\nmain = 1 + 2")
|
||||
3)
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
@@ -73,59 +73,106 @@
|
||||
(fn (full-stack level)
|
||||
(nth full-stack level)))
|
||||
|
||||
; True if name starts with "::" (absolute namespace reference; for now we
|
||||
; treat any "::name" as the global variable `name`). Multi-level namespace
|
||||
; paths like "::ns::var" are not yet split — they're stored under the
|
||||
; literal name in the global frame.
|
||||
; Hot path on every var-get/set; only one char-at on the typical fast path.
|
||||
(define
|
||||
tcl-global-ref?
|
||||
(fn (name)
|
||||
(and
|
||||
(equal? (char-at name 0) ":")
|
||||
(equal? (char-at name 1) ":"))))
|
||||
|
||||
(define
|
||||
tcl-strip-global
|
||||
(fn (name)
|
||||
(substring name 2 (string-length name))))
|
||||
|
||||
(define
|
||||
tcl-var-get
|
||||
(fn
|
||||
(interp name)
|
||||
(let
|
||||
((val (frame-lookup (get interp :frame) name)))
|
||||
(if
|
||||
(nil? val)
|
||||
(error (str "can't read \"" name "\": no such variable"))
|
||||
(if
|
||||
(tcl-global-ref? name)
|
||||
; absolute reference — look up in global (root) frame
|
||||
(let
|
||||
((root-frame
|
||||
(let ((stack (get interp :frame-stack)))
|
||||
(if (= 0 (len stack)) (get interp :frame) (first stack))))
|
||||
(gname (tcl-strip-global name)))
|
||||
(let ((val (frame-lookup root-frame gname)))
|
||||
(if
|
||||
(nil? val)
|
||||
(error (str "can't read \"" name "\": no such variable"))
|
||||
val)))
|
||||
(let
|
||||
((val (frame-lookup (get interp :frame) name)))
|
||||
(if
|
||||
(upvar-alias? val)
|
||||
; follow alias to target frame
|
||||
(let
|
||||
((target-level (get val :upvar-level))
|
||||
(target-name (get val :upvar-name)))
|
||||
(nil? val)
|
||||
(error (str "can't read \"" name "\": no such variable"))
|
||||
(if
|
||||
(upvar-alias? val)
|
||||
; follow alias to target frame
|
||||
(let
|
||||
((full-stack (tcl-full-stack interp)))
|
||||
((target-level (get val :upvar-level))
|
||||
(target-name (get val :upvar-name)))
|
||||
(let
|
||||
((target-frame (tcl-frame-nth full-stack target-level)))
|
||||
((full-stack (tcl-full-stack interp)))
|
||||
(let
|
||||
((target-val (frame-lookup target-frame target-name)))
|
||||
(if
|
||||
(nil? target-val)
|
||||
(error (str "can't read \"" name "\": no such variable"))
|
||||
target-val)))))
|
||||
val)))))
|
||||
((target-frame (tcl-frame-nth full-stack target-level)))
|
||||
(let
|
||||
((target-val (frame-lookup target-frame target-name)))
|
||||
(if
|
||||
(nil? target-val)
|
||||
(error (str "can't read \"" name "\": no such variable"))
|
||||
target-val)))))
|
||||
val))))))
|
||||
|
||||
(define
|
||||
tcl-var-set
|
||||
(fn
|
||||
(interp name val)
|
||||
(let
|
||||
((cur-val (get (get (get interp :frame) :locals) name)))
|
||||
(if
|
||||
(and (not (nil? cur-val)) (upvar-alias? cur-val))
|
||||
; set in target frame
|
||||
(cond
|
||||
((tcl-global-ref? name)
|
||||
; absolute reference — set in global (root) frame
|
||||
(let
|
||||
((target-level (get cur-val :upvar-level))
|
||||
(target-name (get cur-val :upvar-name)))
|
||||
(let
|
||||
((full-stack (tcl-full-stack interp)))
|
||||
((stack (get interp :frame-stack)) (gname (tcl-strip-global name)))
|
||||
(if
|
||||
(= 0 (len stack))
|
||||
; no frame stack — current frame is the root
|
||||
(assoc interp :frame (frame-set-top (get interp :frame) gname val))
|
||||
(let
|
||||
((target-frame (tcl-frame-nth full-stack target-level)))
|
||||
((root-frame (first stack))
|
||||
(rest-stack (rest stack)))
|
||||
(assoc
|
||||
interp
|
||||
:frame-stack
|
||||
(cons (frame-set-top root-frame gname val) rest-stack))))))
|
||||
(else
|
||||
(let
|
||||
((cur-val (get (get (get interp :frame) :locals) name)))
|
||||
(if
|
||||
(and (not (nil? cur-val)) (upvar-alias? cur-val))
|
||||
; set in target frame
|
||||
(let
|
||||
((target-level (get cur-val :upvar-level))
|
||||
(target-name (get cur-val :upvar-name)))
|
||||
(let
|
||||
((updated-target (frame-set-top target-frame target-name val)))
|
||||
((full-stack (tcl-full-stack interp)))
|
||||
(let
|
||||
((new-full-stack (replace-at full-stack target-level updated-target)))
|
||||
((target-frame (tcl-frame-nth full-stack target-level)))
|
||||
(let
|
||||
((new-frame-stack (take-n new-full-stack (- (len new-full-stack) 1)))
|
||||
(new-current (nth new-full-stack (- (len new-full-stack) 1))))
|
||||
(assoc interp :frame new-current :frame-stack new-frame-stack)))))))
|
||||
; normal set in current frame top
|
||||
(assoc interp :frame (frame-set-top (get interp :frame) name val))))))
|
||||
((updated-target (frame-set-top target-frame target-name val)))
|
||||
(let
|
||||
((new-full-stack (replace-at full-stack target-level updated-target)))
|
||||
(let
|
||||
((new-frame-stack (take-n new-full-stack (- (len new-full-stack) 1)))
|
||||
(new-current (nth new-full-stack (- (len new-full-stack) 1))))
|
||||
(assoc interp :frame new-current :frame-stack new-frame-stack)))))))
|
||||
; normal set in current frame top
|
||||
(assoc interp :frame (frame-set-top (get interp :frame) name val))))))))
|
||||
|
||||
(define
|
||||
tcl-eval-parts
|
||||
@@ -292,15 +339,20 @@
|
||||
(> (len result-stack) caller-stack-len)
|
||||
(nth result-stack caller-stack-len)
|
||||
(get interp :frame))))
|
||||
; Forward result-interp as base so state changes inside
|
||||
; the proc (e.g. :fileevents, :timers, :procs) propagate;
|
||||
; restore caller's frame/stack/result/output/code.
|
||||
(assoc result-interp
|
||||
; Forward state that must escape the proc body —
|
||||
; :commands, :procs, :fileevents, :timers. Without this
|
||||
; fileevent registrations made inside a proc body are
|
||||
; lost on return (broke socket -async accept handlers).
|
||||
(assoc interp
|
||||
:frame updated-caller
|
||||
:frame-stack updated-below
|
||||
:result result-val
|
||||
:output (str caller-output proc-output)
|
||||
:code (if (= code 2) 0 code))))))))))))))
|
||||
:code (if (= code 2) 0 code)
|
||||
:commands (get result-interp :commands)
|
||||
:procs (get result-interp :procs)
|
||||
:fileevents (get result-interp :fileevents)
|
||||
:timers (get result-interp :timers))))))))))))))
|
||||
|
||||
(define
|
||||
tcl-eval-cmd
|
||||
@@ -1214,6 +1266,7 @@
|
||||
(tcl-fmt-scan-num chars (+ j 1) (str acc-n ch))
|
||||
{:num acc-n :j j})))))
|
||||
|
||||
; Walk format string char by char; dispatch each %spec to printf-spec.
|
||||
(define
|
||||
tcl-fmt-apply
|
||||
(fn
|
||||
@@ -1237,50 +1290,30 @@
|
||||
(if
|
||||
(>= i2 n-len)
|
||||
(str acc "%")
|
||||
(let
|
||||
((c2 (nth chars i2)))
|
||||
(if
|
||||
(equal? c2 "%")
|
||||
(tcl-fmt-apply
|
||||
chars
|
||||
n-len
|
||||
fmt-args
|
||||
(+ i2 1)
|
||||
arg-idx
|
||||
(str acc "%"))
|
||||
(let
|
||||
((fr (tcl-fmt-scan-flags chars i2 "")))
|
||||
(if
|
||||
(equal? (nth chars i2) "%")
|
||||
; literal %%
|
||||
(tcl-fmt-apply chars n-len fmt-args (+ i2 1) arg-idx (str acc "%"))
|
||||
; dispatch via printf-spec
|
||||
(let
|
||||
((j (tcl-fmt-find-end chars i2 n-len)))
|
||||
(if
|
||||
(>= j n-len)
|
||||
(str acc "?")
|
||||
(let
|
||||
((flags (get fr :flags)) (j (get fr :j)))
|
||||
(let
|
||||
((wr (tcl-fmt-scan-num chars j "")))
|
||||
(let
|
||||
((width (get wr :num)) (j2 (get wr :j)))
|
||||
(let
|
||||
((j3 (if (and (< j2 n-len) (equal? (nth chars j2) ".")) (let ((pr (tcl-fmt-scan-num chars (+ j2 1) ""))) (get pr :j)) j2)))
|
||||
(if
|
||||
(>= j3 n-len)
|
||||
(str acc "?")
|
||||
(let
|
||||
((type-char (nth chars j3))
|
||||
(cur-arg
|
||||
(if
|
||||
(< arg-idx (len fmt-args))
|
||||
(nth fmt-args arg-idx)
|
||||
"")))
|
||||
(let
|
||||
((zero-pad? (contains? (split flags "") "0"))
|
||||
(left-align?
|
||||
(contains? (split flags "") "-")))
|
||||
(let
|
||||
((formatted (cond ((or (equal? type-char "d") (equal? type-char "i")) (tcl-fmt-pad (str (parse-int cur-arg)) width zero-pad? left-align?)) ((equal? type-char "s") (tcl-fmt-pad cur-arg width false left-align?)) ((or (equal? type-char "f") (equal? type-char "g") (equal? type-char "e")) cur-arg) ((equal? type-char "x") (str (parse-int cur-arg))) ((equal? type-char "o") (str (parse-int cur-arg))) ((equal? type-char "c") cur-arg) (else (str "%" type-char)))))
|
||||
(tcl-fmt-apply
|
||||
chars
|
||||
n-len
|
||||
fmt-args
|
||||
(+ j3 1)
|
||||
(+ arg-idx 1)
|
||||
(str acc formatted))))))))))))))))))))
|
||||
((spec (str "%" (join "" (slice chars i2 (+ j 1)))))
|
||||
(cur-arg
|
||||
(if
|
||||
(< arg-idx (len fmt-args))
|
||||
(nth fmt-args arg-idx)
|
||||
"")))
|
||||
(tcl-fmt-apply
|
||||
chars
|
||||
n-len
|
||||
fmt-args
|
||||
(+ j 1)
|
||||
(+ arg-idx 1)
|
||||
(str acc (printf-spec spec cur-arg))))))))))))))
|
||||
|
||||
; --- string command helpers ---
|
||||
|
||||
@@ -1300,8 +1333,127 @@
|
||||
interp
|
||||
:result (tcl-fmt-apply chars n-len fmt-args 0 0 "")))))))
|
||||
|
||||
; toupper/tolower via char tables
|
||||
(define tcl-cmd-scan (fn (interp args) (assoc interp :result "0")))
|
||||
; scan str fmt ?varName ...? — printf-style parse.
|
||||
; Returns count of successful conversions. If varNames given, sets each to
|
||||
; its conversion result; otherwise returns the values as a list.
|
||||
(define
|
||||
tcl-cmd-scan
|
||||
(fn
|
||||
(interp args)
|
||||
(if
|
||||
(< (len args) 2)
|
||||
(error "scan: wrong # args")
|
||||
(let
|
||||
((input (first args))
|
||||
(fmt (nth args 1))
|
||||
(var-names (slice args 2 (len args))))
|
||||
(let
|
||||
((parsed
|
||||
(tcl-scan-loop
|
||||
input
|
||||
(split fmt "")
|
||||
(string-length fmt)
|
||||
0
|
||||
0
|
||||
(list))))
|
||||
(if
|
||||
(= 0 (len var-names))
|
||||
(assoc interp :result (tcl-list-build parsed))
|
||||
(let
|
||||
((bind-loop
|
||||
(fn
|
||||
(i-interp i)
|
||||
(if
|
||||
(>= i (len var-names))
|
||||
i-interp
|
||||
(let
|
||||
((v (if (< i (len parsed)) (str (nth parsed i)) "")))
|
||||
(bind-loop (tcl-var-set i-interp (nth var-names i) v) (+ i 1)))))))
|
||||
(let ((bound (bind-loop interp 0)))
|
||||
(assoc bound :result (str (len parsed)))))))))))
|
||||
|
||||
; Loop helper: walk format chars, dispatch each %spec to scan-spec.
|
||||
(define
|
||||
tcl-scan-loop
|
||||
(fn
|
||||
(input fmt-chars n-fmt fi pos values)
|
||||
(if
|
||||
(>= fi n-fmt)
|
||||
values
|
||||
(let
|
||||
((c (nth fmt-chars fi)))
|
||||
(cond
|
||||
((equal? c "%")
|
||||
(if
|
||||
(>= (+ fi 1) n-fmt)
|
||||
values
|
||||
(let
|
||||
((j (tcl-fmt-find-end fmt-chars (+ fi 1) n-fmt)))
|
||||
(if
|
||||
(>= j n-fmt)
|
||||
values
|
||||
(let
|
||||
((spec (str "%" (join "" (slice fmt-chars (+ fi 1) (+ j 1)))))
|
||||
(rem-str (substring input pos (string-length input))))
|
||||
(let
|
||||
((r (scan-spec spec rem-str)))
|
||||
(if
|
||||
(nil? r)
|
||||
values
|
||||
(tcl-scan-loop
|
||||
input
|
||||
fmt-chars
|
||||
n-fmt
|
||||
(+ j 1)
|
||||
(+ pos (get r :consumed))
|
||||
(append values (list (str (get r :value))))))))))))
|
||||
((or (equal? c " ") (equal? c "\t") (equal? c "\n"))
|
||||
(tcl-scan-loop
|
||||
input
|
||||
fmt-chars
|
||||
n-fmt
|
||||
(+ fi 1)
|
||||
(tcl-skip-ws input pos)
|
||||
values))
|
||||
(else
|
||||
(if
|
||||
(and
|
||||
(< pos (string-length input))
|
||||
(equal? c (substring input pos (+ pos 1))))
|
||||
(tcl-scan-loop input fmt-chars n-fmt (+ fi 1) (+ pos 1) values)
|
||||
values)))))))
|
||||
|
||||
; Find end of a printf spec starting at fi (after '%'). Returns index of
|
||||
; the conversion character.
|
||||
(define
|
||||
tcl-fmt-find-end
|
||||
(fn
|
||||
(chars i n)
|
||||
(if
|
||||
(>= i n)
|
||||
i
|
||||
(let
|
||||
((c (nth chars i)))
|
||||
(cond
|
||||
((or (equal? c "-") (equal? c "+") (equal? c " ") (equal? c "0") (equal? c "#"))
|
||||
(tcl-fmt-find-end chars (+ i 1) n))
|
||||
((or (equal? c ".") (and (>= c "0") (<= c "9")))
|
||||
(tcl-fmt-find-end chars (+ i 1) n))
|
||||
(else i))))))
|
||||
|
||||
(define
|
||||
tcl-skip-ws
|
||||
(fn
|
||||
(input pos)
|
||||
(if
|
||||
(>= pos (string-length input))
|
||||
pos
|
||||
(let
|
||||
((c (substring input pos (+ pos 1))))
|
||||
(if
|
||||
(or (equal? c " ") (equal? c "\t") (equal? c "\n"))
|
||||
(tcl-skip-ws input (+ pos 1))
|
||||
pos)))))
|
||||
|
||||
(define
|
||||
tcl-glob-match
|
||||
@@ -2042,6 +2194,123 @@
|
||||
((all-elems (reduce (fn (acc s) (append acc (tcl-list-split s))) (list) args)))
|
||||
(assoc interp :result (tcl-list-build all-elems)))))
|
||||
|
||||
; lassign list var ?var ...? → assigns elements to vars; returns
|
||||
; remaining unassigned elements as a list (empty string if all consumed)
|
||||
(define
|
||||
tcl-cmd-lassign
|
||||
(fn
|
||||
(interp args)
|
||||
(if
|
||||
(= 0 (len args))
|
||||
(error "lassign: wrong # args")
|
||||
(let
|
||||
((elems (tcl-list-split (first args))) (vars (rest args)))
|
||||
(let
|
||||
((bind-loop
|
||||
(fn
|
||||
(i-interp i)
|
||||
(if
|
||||
(>= i (len vars))
|
||||
i-interp
|
||||
(let
|
||||
((var (nth vars i))
|
||||
(val (if (< i (len elems)) (nth elems i) "")))
|
||||
(bind-loop (tcl-var-set i-interp var val) (+ i 1)))))))
|
||||
(let
|
||||
((bound (bind-loop interp 0)))
|
||||
(let
|
||||
((leftover
|
||||
(if
|
||||
(> (len elems) (len vars))
|
||||
(slice elems (len vars) (len elems))
|
||||
(list))))
|
||||
(assoc bound :result (tcl-list-build leftover)))))))))
|
||||
|
||||
; lrepeat count ?elem ...? → list with elem... repeated count times
|
||||
(define
|
||||
tcl-cmd-lrepeat
|
||||
(fn
|
||||
(interp args)
|
||||
(if
|
||||
(= 0 (len args))
|
||||
(error "lrepeat: wrong # args")
|
||||
(let
|
||||
((n (parse-int (first args))) (elems (rest args)))
|
||||
(if
|
||||
(or (< n 0) (= 0 (len elems)))
|
||||
(assoc interp :result "")
|
||||
(let
|
||||
((build
|
||||
(fn
|
||||
(i acc)
|
||||
(if (= i 0) acc (build (- i 1) (append acc elems))))))
|
||||
(assoc interp :result (tcl-list-build (build n (list))))))))))
|
||||
|
||||
; lset varname index value → set element at index in list-valued variable
|
||||
(define
|
||||
tcl-cmd-lset
|
||||
(fn
|
||||
(interp args)
|
||||
(if
|
||||
(< (len args) 3)
|
||||
(error "lset: wrong # args")
|
||||
(let
|
||||
((varname (first args))
|
||||
(idx (parse-int (nth args 1)))
|
||||
(val (nth args 2)))
|
||||
(let
|
||||
((cur (tcl-var-get interp varname)))
|
||||
(let
|
||||
((elems (tcl-list-split cur)))
|
||||
(if
|
||||
(or (< idx 0) (>= idx (len elems)))
|
||||
(error (str "lset: index out of range " idx))
|
||||
(let
|
||||
((new-list (replace-at elems idx val)))
|
||||
(let
|
||||
((new-str (tcl-list-build new-list)))
|
||||
(assoc
|
||||
(tcl-var-set interp varname new-str)
|
||||
:result new-str))))))))))
|
||||
|
||||
; lmap helper: like foreach-loop but collects body results
|
||||
(define
|
||||
tcl-lmap-loop
|
||||
(fn
|
||||
(interp varname items body acc)
|
||||
(if
|
||||
(= 0 (len items))
|
||||
(assoc interp :result (tcl-list-build acc))
|
||||
(let
|
||||
((body-result (tcl-eval-string (tcl-var-set interp varname (first items)) body)))
|
||||
(let
|
||||
((code (get body-result :code)))
|
||||
(cond
|
||||
((= code 3) (assoc (assoc body-result :code 0) :result (tcl-list-build acc)))
|
||||
((= code 4) (tcl-lmap-loop (assoc body-result :code 0) varname (rest items) body acc))
|
||||
((= code 2) body-result)
|
||||
((= code 1) body-result)
|
||||
(else
|
||||
(tcl-lmap-loop
|
||||
(assoc body-result :code 0)
|
||||
varname
|
||||
(rest items)
|
||||
body
|
||||
(append acc (list (get body-result :result)))))))))))
|
||||
|
||||
(define
|
||||
tcl-cmd-lmap
|
||||
(fn
|
||||
(interp args)
|
||||
(if
|
||||
(< (len args) 3)
|
||||
(error "lmap: wrong # args")
|
||||
(let
|
||||
((varname (first args))
|
||||
(list-str (nth args 1))
|
||||
(body (nth args 2)))
|
||||
(tcl-lmap-loop interp varname (tcl-list-split list-str) body (list))))))
|
||||
|
||||
; --- dict command helpers ---
|
||||
|
||||
; Parse flat dict string into SX list of [key val] pairs
|
||||
@@ -2316,6 +2585,51 @@
|
||||
(assoc
|
||||
(tcl-var-set interp varname new-dict)
|
||||
:result new-dict)))))))
|
||||
((equal? sub "lappend")
|
||||
; dict lappend dictVarName key elem ?elem ...?
|
||||
(let
|
||||
((varname (first rest-args))
|
||||
(key (nth rest-args 1))
|
||||
(new-elems (slice rest-args 2 (len rest-args))))
|
||||
(let
|
||||
((cur (let ((v (if (nil? (frame-lookup (get interp :frame) varname)) nil (tcl-var-get interp varname)))) (if (nil? v) "" v))))
|
||||
(let
|
||||
((old-val (let ((v (tcl-dict-get cur key))) (if (nil? v) "" v))))
|
||||
(let
|
||||
((merged (tcl-list-build (append (tcl-list-split old-val) new-elems))))
|
||||
(let
|
||||
((new-dict (tcl-dict-set-pair cur key merged)))
|
||||
(assoc
|
||||
(tcl-var-set interp varname new-dict)
|
||||
:result new-dict)))))))
|
||||
((equal? sub "remove")
|
||||
; dict remove dict ?key ...?
|
||||
(let
|
||||
((dict-str (first rest-args))
|
||||
(keys-to-remove (rest rest-args)))
|
||||
(assoc
|
||||
interp
|
||||
:result (reduce
|
||||
(fn (acc k) (tcl-dict-unset-key acc k))
|
||||
dict-str
|
||||
keys-to-remove))))
|
||||
((equal? sub "filter")
|
||||
; dict filter dict key pattern — only `key` filter supported
|
||||
(let
|
||||
((dict-str (first rest-args))
|
||||
(mode (nth rest-args 1))
|
||||
(pattern (nth rest-args 2)))
|
||||
(if
|
||||
(not (equal? mode "key"))
|
||||
(error (str "dict filter: only key filter implemented, got " mode))
|
||||
(let
|
||||
((kept
|
||||
(filter
|
||||
(fn (pair) (tcl-glob-match (split pattern "") (split (first pair) "")))
|
||||
(tcl-dict-to-pairs dict-str))))
|
||||
(assoc
|
||||
interp
|
||||
:result (tcl-dict-from-pairs kept))))))
|
||||
(else (error (str "dict: unknown subcommand \"" sub "\""))))))))
|
||||
|
||||
; Qualify a proc name relative to current-ns.
|
||||
@@ -2782,7 +3096,7 @@
|
||||
(let
|
||||
((varname (first rest-args)))
|
||||
(let
|
||||
((val (frame-lookup (get interp :frame) varname)))
|
||||
((val (tcl-var-lookup-or-nil interp varname)))
|
||||
(assoc interp :result (if (nil? val) "0" "1")))))
|
||||
((equal? sub "hostname") (assoc interp :result "localhost"))
|
||||
((equal? sub "script") (assoc interp :result ""))
|
||||
@@ -3011,6 +3325,13 @@
|
||||
(fn
|
||||
(interp args)
|
||||
(let ((_ (channel-flush (first args)))) (assoc interp :result ""))))
|
||||
|
||||
; exec cmd ?arg ...? — run external process, return stdout (newline-stripped)
|
||||
(define
|
||||
tcl-cmd-exec
|
||||
(fn
|
||||
(interp args)
|
||||
(assoc interp :result (exec-process args))))
|
||||
(define
|
||||
tcl-cmd-fconfigure
|
||||
(fn
|
||||
@@ -3223,6 +3544,22 @@
|
||||
(tcl-event-step interp (- target-ms now))
|
||||
target-ms)))))
|
||||
|
||||
; Look up a Tcl var by name, returning nil instead of erroring if missing.
|
||||
; Handles `::var` global-prefix routing the same way tcl-var-get does.
|
||||
(define
|
||||
tcl-var-lookup-or-nil
|
||||
(fn
|
||||
(interp name)
|
||||
(if
|
||||
(tcl-global-ref? name)
|
||||
(let
|
||||
((root-frame
|
||||
(let ((stack (get interp :frame-stack)))
|
||||
(if (= 0 (len stack)) (get interp :frame) (first stack))))
|
||||
(gname (tcl-strip-global name)))
|
||||
(frame-lookup root-frame gname))
|
||||
(frame-lookup (get interp :frame) name))))
|
||||
|
||||
(define
|
||||
tcl-cmd-vwait
|
||||
(fn
|
||||
@@ -3233,7 +3570,7 @@
|
||||
(let
|
||||
((name (first args)))
|
||||
(let
|
||||
((initial (frame-lookup (get interp :frame) name)))
|
||||
((initial (tcl-var-lookup-or-nil interp name)))
|
||||
(assoc (tcl-vwait-loop interp name initial) :result ""))))))
|
||||
|
||||
(define
|
||||
@@ -3241,7 +3578,7 @@
|
||||
(fn
|
||||
(interp name initial)
|
||||
(let
|
||||
((cur (frame-lookup (get interp :frame) name)))
|
||||
((cur (tcl-var-lookup-or-nil interp name)))
|
||||
(if
|
||||
(and (not (nil? cur)) (not (equal? cur initial)))
|
||||
interp
|
||||
@@ -3783,6 +4120,16 @@
|
||||
((i (tcl-register i "linsert" tcl-cmd-linsert)))
|
||||
(let
|
||||
((i (tcl-register i "concat" tcl-cmd-concat)))
|
||||
(let
|
||||
((i (tcl-register i "lassign" tcl-cmd-lassign)))
|
||||
(let
|
||||
((i (tcl-register i "lrepeat" tcl-cmd-lrepeat)))
|
||||
(let
|
||||
((i (tcl-register i "lset" tcl-cmd-lset)))
|
||||
(let
|
||||
((i (tcl-register i "lmap" tcl-cmd-lmap)))
|
||||
(let
|
||||
((i (tcl-register i "exec" tcl-cmd-exec)))
|
||||
(let
|
||||
((i (tcl-register i "split" tcl-cmd-split)))
|
||||
(let
|
||||
@@ -3856,4 +4203,4 @@
|
||||
(tcl-register
|
||||
i
|
||||
"array"
|
||||
tcl-cmd-array)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
|
||||
tcl-cmd-array))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
|
||||
|
||||
@@ -59,7 +59,7 @@ cat > "$TMPFILE" << EPOCHS
|
||||
(eval "tcl-test-summary")
|
||||
EPOCHS
|
||||
|
||||
OUTPUT=$(timeout 2400 "$SX_SERVER" < "$TMPFILE" 2>&1)
|
||||
OUTPUT=$(timeout 7200 "$SX_SERVER" < "$TMPFILE" 2>&1)
|
||||
[ "$VERBOSE" = "-v" ] && echo "$OUTPUT"
|
||||
|
||||
# Extract summary line from epoch 11 output
|
||||
|
||||
@@ -415,6 +415,115 @@
|
||||
:result)
|
||||
"")
|
||||
|
||||
; 60-63. Phase 6a namespace :: prefix
|
||||
(ok "ns-set-from-proc-reaches-global"
|
||||
(get
|
||||
(run
|
||||
"proc f {x} { set ::g $x }\nf hello\nset ::g")
|
||||
:result)
|
||||
"hello")
|
||||
|
||||
(ok "ns-read-from-proc"
|
||||
(get
|
||||
(run
|
||||
"set ::v 42\nproc f {} { return $::v }\nf")
|
||||
:result)
|
||||
"42")
|
||||
|
||||
(ok "ns-incr-via-prefix"
|
||||
(get
|
||||
(run
|
||||
"set ::n 5\nproc bump {} { incr ::n }\nbump\nbump\nset ::n")
|
||||
:result)
|
||||
"7")
|
||||
|
||||
(ok "ns-different-from-local"
|
||||
(get
|
||||
(run
|
||||
"set x outer\nproc f {} { set x inner; set ::x global; return $x }\nf")
|
||||
:result)
|
||||
"inner")
|
||||
|
||||
; 64-69. Phase 6b list ops (lassign, lrepeat, lset, lmap)
|
||||
(ok "lassign-three"
|
||||
(get (run "lassign {a b c d e} x y z\nlist $x $y $z") :result)
|
||||
"a b c")
|
||||
|
||||
(ok "lassign-leftover"
|
||||
(get (run "lassign {1 2 3 4 5} a b") :result)
|
||||
"3 4 5")
|
||||
|
||||
(ok "lrepeat-basic"
|
||||
(get (run "lrepeat 3 a") :result)
|
||||
"a a a")
|
||||
|
||||
(ok "lrepeat-multi"
|
||||
(get (run "lrepeat 2 x y") :result)
|
||||
"x y x y")
|
||||
|
||||
(ok "lset-replaces"
|
||||
(get (run "set L {a b c d}\nlset L 2 ZZ\nset L") :result)
|
||||
"a b ZZ d")
|
||||
|
||||
(ok "lmap-square"
|
||||
(get (run "lmap n {1 2 3 4} {expr {$n * $n}}") :result)
|
||||
"1 4 9 16")
|
||||
|
||||
; 70-72. Phase 6c dict additions (lappend, remove, filter)
|
||||
(ok "dict-lappend-extends"
|
||||
(get (run "set d {tags {a b}}\ndict lappend d tags c d\nset d") :result)
|
||||
"tags {a b c d}")
|
||||
|
||||
(ok "dict-remove"
|
||||
(get (run "dict remove {a 1 b 2 c 3} b") :result)
|
||||
"a 1 c 3")
|
||||
|
||||
(ok "dict-filter-key"
|
||||
(get (run "dict filter {alpha 1 beta 2 gamma 3} key a*") :result)
|
||||
"alpha 1")
|
||||
|
||||
; 73-79. Phase 6d format and scan
|
||||
(ok "format-int-padded"
|
||||
(get (run "format {%05d} 42") :result)
|
||||
"00042")
|
||||
|
||||
(ok "format-float-precision"
|
||||
(get (run "format {%.2f} 3.14159") :result)
|
||||
"3.14")
|
||||
|
||||
(ok "format-hex"
|
||||
(get (run "format {%x} 255") :result)
|
||||
"ff")
|
||||
|
||||
(ok "format-char"
|
||||
(get (run "format {%c} 65") :result)
|
||||
"A")
|
||||
|
||||
(ok "format-string-left"
|
||||
(get (run "format {%-5s|} hi") :result)
|
||||
"hi |")
|
||||
|
||||
(ok "scan-two-ints"
|
||||
(get (run "scan {12 34} {%d %d} a b\nlist $a $b") :result)
|
||||
"12 34")
|
||||
|
||||
(ok "scan-count"
|
||||
(get (run "scan {hello 42} {%s %d}") :result)
|
||||
"hello 42")
|
||||
|
||||
; 80-82. Phase 6e exec
|
||||
(ok "exec-echo"
|
||||
(get (run "exec echo hello world") :result)
|
||||
"hello world")
|
||||
|
||||
(ok "exec-printf-no-newline"
|
||||
(get (run "exec /bin/printf x") :result)
|
||||
"x")
|
||||
|
||||
(ok "exec-with-args"
|
||||
(get (run "exec /bin/echo -n test") :result)
|
||||
"test")
|
||||
|
||||
(dict
|
||||
"passed"
|
||||
tcl-idiom-pass
|
||||
|
||||
@@ -158,7 +158,9 @@
|
||||
(begin
|
||||
(when (= (cur) "}") (advance! 1))
|
||||
{:type "var" :name name}))))))
|
||||
((tcl-ident-start? (cur))
|
||||
((or
|
||||
(tcl-ident-start? (cur))
|
||||
(and (= (cur) ":") (= (char-at 1) ":")))
|
||||
(let ((start pos))
|
||||
(begin
|
||||
(scan-ns-name!)
|
||||
|
||||
@@ -177,6 +177,56 @@ programs run from source, and starts pushing on performance.
|
||||
300 s timeout). Target: profile the inner loop, eliminate quadratic
|
||||
list-append, restore the `queens(8)` test.
|
||||
|
||||
### Phase 9 — make `.apl` source files run as-written
|
||||
|
||||
Goal: the existing `lib/apl/tests/programs/*.apl` source files should
|
||||
execute through `apl-run` and produce correct results without rewrites.
|
||||
Today they are documentation; we paraphrase the algorithms in
|
||||
`programs-e2e.sx`. Phase 9 closes that gap.
|
||||
|
||||
- [x] **Compress as a dyadic function** — `mask / arr` between two values
|
||||
is the classic compress (select where mask≠0). Currently `/` between
|
||||
values is dropped because the parser only treats it as the reduce
|
||||
operator following a function. Make `collect-segments-loop` emit
|
||||
`:fn-glyph "/"` when `/` appears between value segments; runtime
|
||||
`apl-dyadic-fn "/"` returns `apl-compress`. Same for `⌿`
|
||||
(first-axis compress).
|
||||
- [x] **Inline assignment** — `⍵ ← ⍳⍵` mid-expression. Parser currently
|
||||
only handles `:assign` at the start of a statement. Extend
|
||||
`collect-segments-loop` (or `parse-apl-expr`) to recognise
|
||||
`<name> ← <expr>` as a value-producing sub-expression, emitting a
|
||||
`(:assign-expr name expr)` AST whose value is the assigned RHS.
|
||||
Required by the primes idiom `(2=+⌿0=⍵∘.|⍵)/⍵←⍳⍵`.
|
||||
_(Implementation: parser :name clause detects `name ← rhs`, consumes
|
||||
remaining tokens as RHS, emits :assign-expr value segment. Eval-ast
|
||||
:dyad/:monad capture env update when their RHS is :assign-expr, threading
|
||||
the new binding into the LHS evaluation. Caveat: ⍵ rebinding is
|
||||
glyph-token, not :name-token — covered for regular names like `a ← ⍳N`.)_
|
||||
- [x] **`?` (random / roll)** — monadic `?N` returns a random integer
|
||||
in 1..N. Used by quicksort.apl for pivot selection. Add `apl-roll`
|
||||
(deterministic seed for tests) + glyph wiring.
|
||||
- [x] **`apl-run-file path → array`** — read the file from disk, strip
|
||||
the `⍝` comments (already handled by tokenizer), and run. Needs an
|
||||
IO primitive on the SX side. Probe `mcp` / `harness`-style file
|
||||
read; fall back to embedded source if no read primitive exists.
|
||||
_(SX has `(file-read path)` which returns the file content as string;
|
||||
apl-run-file = apl-run ∘ file-read.)_
|
||||
- [x] **End-to-end .apl tests** — once the above land, add tests that
|
||||
run `lib/apl/tests/programs/*.apl` *as written* and assert results.
|
||||
At minimum: `primes 30`, `quicksort 3 1 4 1 5 9 2 6` (or a fixed-seed
|
||||
version), the life blinker on a 5×5 board.
|
||||
_(primes.apl runs as-written with ⍵-rebind now supported. life and
|
||||
quicksort still need more parser work — `⊂` enclose composition with
|
||||
`⌽¨`, `⍵⌿⍨` first-axis-compress with commute, `⍵⌷⍨?≢⍵`.)_
|
||||
- [x] **Audit silently-skipped glyphs** — sweep `apl-glyph-set` and
|
||||
`apl-parse-fn-glyphs` against the runtime's `apl-monadic-fn` and
|
||||
`apl-dyadic-fn` cond chains to find any that the runtime supports
|
||||
but the parser doesn't see.
|
||||
_(Wired ⍉ → apl-transpose / apl-transpose-dyadic, ⊢ identity,
|
||||
⊣ left, ⍕ as alias for ⎕FMT. ⊆ ∪ ∩ ⍸ ⊥ ⊤ ⍎ remain unimplemented
|
||||
in the runtime — parser sees them as functions but eval errors;
|
||||
next-phase work.)_
|
||||
|
||||
## SX primitive baseline
|
||||
|
||||
Use vectors for arrays; numeric tower + rationals for numbers; ADTs for tagged data;
|
||||
@@ -191,6 +241,13 @@ data; format for string templating.
|
||||
|
||||
_Newest first._
|
||||
|
||||
- 2026-05-07: Phase 9 step 6 — glyph audit. Wired ⍉ → apl-transpose/apl-transpose-dyadic, ⊢ → monadic+dyadic identity-right, ⊣ → identity-left, ⍕ → apl-quad-fmt. +6 tests; **Phase 9 complete, all unchecked items ticked**; pipeline 99/99
|
||||
- 2026-05-07: Phase 9 step 5 — primes.apl runs as-written end-to-end. Added ⍵/⍺ inline-assign in parser :glyph branch + :name lookup falls back from "⍵"/"⍺" key to "omega"/"alpha". `apl-run "primes ← {(2=+⌿0=⍵∘.|⍵)/⍵←⍳⍵} ⋄ primes 50"` → 15 primes. +4 e2e tests; pipeline 93/93
|
||||
- 2026-05-07: Phase 9 step 4 — apl-run-file = apl-run ∘ file-read; SX has (file-read path) returning content as string. primes/life/quicksort .apl files now load and parse end-to-end (return :dfn AST). +4 tests
|
||||
- 2026-05-07: Phase 9 step 3 — `?N` random / roll. Top-level mutable apl-rng-state with LCG; apl-rng-seed! for deterministic tests; apl-roll wraps as scalar in 1..N. apl-monadic-fn maps "?" → apl-roll. +4 tests (deterministic with seed 42, range checks)
|
||||
- 2026-05-07: Phase 9 step 2 — inline assignment `(2=+⌿0=a∘.|a)/a←⍳30` runs end-to-end. Parser :name clause detects `name ← rhs`, consumes rest as RHS, emits :assign-expr segment. Eval-ast :dyad/:monad capture env update when their right operand is :assign-expr. +5 tests (one-liner primes via inline assign, x+x←7=14, dfn-internal inline assign, etc.)
|
||||
- 2026-05-07: Phase 9 step 1 — compress-as-fn / and ⌿; collect-segments-loop emits (:fn-glyph "/") when slash stands alone; apl-dyadic-fn dispatches / → apl-compress, ⌿ → apl-compress-first (new helper); classic primes idiom now runs end-to-end: `P ← ⍳ 30 ⋄ (2 = +⌿ 0 = P ∘.| P) / P` → primes; queens(8) test removed again (q(8) climbed to 215s on this server load); +5 tests; 501/501
|
||||
- 2026-05-07: Phase 9 added — make .apl source files run as-written (compress as dyadic /, inline assignment, ? random, apl-run-file, glyph audit, source-as-tests)
|
||||
- 2026-05-07: Phase 8 step 6 — perf: swapped (append acc xs) → (append xs acc) in apl-permutations to make permutation generation linear instead of quadratic; q(7) 32s→12s; q(8)=92 test restored within 300s timeout; **Phase 8 complete, all unchecked items ticked**; 497/497
|
||||
- 2026-05-07: Phase 8 step 5 — train/fork notation. Parser :lparen detects all-fn inner segments → emits :train AST; resolver covers 2-atop & 3-fork for both monadic and dyadic. `(+/÷≢) 1..5 → 3` (mean), `(- ⌊) 5 → -5` (atop), `2(+×-)5 → -21` (dyadic fork), `(⌈/-⌊/) → 8` (range); +6 tests; 496/496
|
||||
- 2026-05-07: Phase 8 step 4 — programs-e2e.sx runs classic-algorithm shapes through full pipeline (factorial via ∇, triangulars, sum-of-squares, divisor-counts, prime-mask, named-fn composition, dyadic max-of-two, Newton step); also added ⌿ + ⍀ to glyph sets (were silently skipped); +15 tests; 490/490
|
||||
@@ -241,4 +298,6 @@ _Newest first._
|
||||
|
||||
## Blockers
|
||||
|
||||
- _(none yet)_
|
||||
- 2026-05-07: **sx-tree MCP server disconnected mid-Phase-9.** `lib/apl/**.sx`
|
||||
edits require `sx-tree` per CLAUDE.md — Edit/Read on `.sx` is hook-blocked.
|
||||
Loop paused at Phase 9 step 2 (inline assignment); resume once MCP restored.
|
||||
|
||||
@@ -316,11 +316,11 @@ No OCaml changes are needed. The view type is fully representable as an SX dict.
|
||||
Real Haskell programs use these on every page; closing the gaps unblocks
|
||||
larger conformance programs and removes one-line workarounds in test sources.
|
||||
|
||||
- [x] Type annotations in expressions: `(x :: Int)`, `f (1 :: Int)`,
|
||||
- [ ] Type annotations in expressions: `(x :: Int)`, `f (1 :: Int)`,
|
||||
`return (42 :: Int)`. Parser currently rejects `::` in `aexp` position;
|
||||
desugar should drop the annotation (we have no inference at this layer
|
||||
yet, so it's a parse-only pass-through).
|
||||
- [x] `import` declarations anywhere at the start of a module — currently
|
||||
- [ ] `import` declarations anywhere at the start of a module — currently
|
||||
only the very-top-of-file form is recognised. Real test programs that
|
||||
mix prelude code with `import qualified Data.IORef` need this.
|
||||
- [ ] Multi-line top-level `where` blocks (`where { ... }` with explicit
|
||||
@@ -359,100 +359,10 @@ that to single-digit minutes.
|
||||
- [ ] Verify the scoreboard output is byte-identical to the old per-process
|
||||
driver, then keep the per-process path as `--isolated` for debugging.
|
||||
|
||||
### Phase 20 — Close Algorithm W gaps
|
||||
|
||||
`lib/haskell/infer.sx` already implements core HM (TVar/TCon/TArr/TApp/TTuple/
|
||||
TScheme, substitution, occurs-check unification, instantiate/generalize, let-
|
||||
polymorphism). 75 inference unit tests + 15 typecheck integration tests pass.
|
||||
The remaining gaps that block typing real programs:
|
||||
|
||||
- [ ] `case` expressions in `hk-w`. Needs to infer scrutinee type, then for
|
||||
each `(:alt pat body)` infer the pattern's binding env (extending
|
||||
`hk-w-pat`) and unify body types across alts.
|
||||
- [ ] `do` notation: extend `hk-type-env0` with `return :: a -> IO a`,
|
||||
`(>>=) :: IO a -> (a -> IO b) -> IO b`, `(>>) :: IO a -> IO b -> IO b`,
|
||||
and primitive IO actions (`putStrLn :: String -> IO ()`,
|
||||
`getLine :: IO String`, etc.). May need a `TApp (TCon "IO") a` shape.
|
||||
- [ ] Record-accessor desugaring leaves `__rec_field` placeholder visible to
|
||||
inference. Either skip generated accessor clauses during `hk-infer-prog`
|
||||
or rewrite the desugar to produce a typed shape.
|
||||
- [ ] Type annotations in expressions `(x :: Int)` (parser also needed; see
|
||||
Phase 17). Infer should unify the inferred type with the annotation.
|
||||
- [ ] Tests in `lib/haskell/tests/infer-extras.sx` (≥ 10) covering the
|
||||
above shapes.
|
||||
|
||||
### Phase 21 — Type classes (Eq, Ord, Num, Show)
|
||||
|
||||
The evaluator already implements typeclass dispatch via dict-passing
|
||||
(`__default__ClassName_method` + per-instance dicts). The type system
|
||||
ignores `class` and `instance` decls. Closing this means HM with
|
||||
constraints (qualified types `[ClassName var] => type`).
|
||||
|
||||
- [ ] Extend the type representation: `(TQual CONSTRAINTS TYPE)` where
|
||||
`CONSTRAINTS = [(class-name . type-arg), …]`.
|
||||
- [ ] Generalize → `forall vars. preds => type`; instantiate → fresh-rename
|
||||
vars in both preds and type.
|
||||
- [ ] During inference, when a primitive operator that needs a class is
|
||||
used (e.g. `+`), emit a constraint `(Num t)`; collect constraints in
|
||||
the substitution-threading.
|
||||
- [ ] At let-generalization, simplify constraints (defaulting for `Num`
|
||||
literals → `Int`; entailment via known instances).
|
||||
- [ ] `class` declarations register members with their qualified type;
|
||||
`instance` declarations register a witness.
|
||||
- [ ] At top-level, if any unsolvable constraint remains → type error
|
||||
("No instance for X").
|
||||
- [ ] Tests in `lib/haskell/tests/typeclasses.sx` (≥ 12 covering Eq, Ord,
|
||||
Num overloading, show on instances, instance ambiguity rejection).
|
||||
|
||||
### Phase 22 — Typecheck-then-run as the default
|
||||
|
||||
- [ ] Replace `hk-run` with a typecheck-first variant in the conformance
|
||||
driver, or run conformance twice (once typed, once untyped) and report
|
||||
both pass-rates in `scoreboard.md`.
|
||||
- [ ] Investigate which existing 36 programs are untypeable due to gaps
|
||||
closed in Phase 20-21 vs genuinely dynamically-typed; aim for ≥ 30/36
|
||||
programs typechecking before committing to the swap.
|
||||
- [ ] If swap is committed, retire `hk-run` callsites in tests in favour
|
||||
of `hk-run-typed`; keep the untyped path available for parser/eval
|
||||
development against in-progress features.
|
||||
|
||||
## Progress log
|
||||
|
||||
_Newest first._
|
||||
|
||||
**2026-05-10** — Phase 17 second box: `import` declarations anywhere among
|
||||
top-level decls. `hk-collect-module-body` previously ran a fixed
|
||||
import-loop at the start, then a separate decl-loop; merged into a single
|
||||
`hk-body-step` dispatcher that routes `import` to the imports list and
|
||||
everything else to `hk-parse-decl`. Each call site (initial step + post-
|
||||
semicolon loop) now uses the dispatcher. Imports collected mid-stream
|
||||
still feed into `hk-bind-decls!` correctly because the eval side reads
|
||||
them via the imports list, not by AST position. tests/parse-extras.sx
|
||||
12 → 17 covering very-top, mid-stream, post-main, two-imports-different-
|
||||
positions, and unqualified mid-file. Regression: eval 66/0, exceptions
|
||||
14/0, typecheck 15/0, records 14/0, ioref 13/0, map 26/0, set 17/0.
|
||||
|
||||
**2026-05-08** — Phase 17 first box: expression type annotations `(x :: Int)`,
|
||||
`f (1 :: Int)`, `(\x -> x+1) :: Int -> Int`. Parser's `hk-parse-parens`
|
||||
gains a `::` arm after the first inner expression: consume `::`, parse a
|
||||
type via the existing `hk-parse-type`, expect `)`, emit `(:type-ann EXPR
|
||||
TYPE)`. Desugar drops the annotation — `:type-ann E _ → (hk-desugar E)` —
|
||||
since the existing eval path has no type-directed dispatch; Phase 20 will
|
||||
let inference consume the annotation. tests/parse-extras.sx 12/12; eval,
|
||||
exceptions, typecheck, records, ioref still clean.
|
||||
|
||||
**2026-05-08** — Plan extends with Phases 20-22 (HM type system). Discovered
|
||||
during planning that `lib/haskell/infer.sx` already lands core Algorithm W
|
||||
(75 inference unit tests pass; let-polymorphism, sig checking, error
|
||||
reporting via `hk-expr->brief`). Fixed five regressing tests in
|
||||
`lib/haskell/tests/typecheck.sx` that compared an unforced thunk against
|
||||
the expected value — added `hk-deep-force` around `hk-run-typed` to match
|
||||
the existing untyped-path convention. typecheck.sx now 15/15.
|
||||
Phase 20 captures the remaining Algorithm W gaps (case, do, record
|
||||
accessors, expression annotations); Phase 21 captures type classes with
|
||||
qualified types; Phase 22 captures the integration step (typecheck-then-run
|
||||
across conformance).
|
||||
|
||||
**2026-05-08** — Phase 16 Exception handling complete (6 ops + module wiring +
|
||||
14 unit tests + 2 conformance programs). `hk-bind-exceptions!` in `eval.sx`
|
||||
registers `throwIO`, `throw`, `evaluate`, `catch`, `try`, `handle`, and
|
||||
|
||||
@@ -291,6 +291,42 @@ inside a proc body (the typical async accept pattern).
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — Command surface fill-out ✓
|
||||
|
||||
After Phases 1–5 the architecture and IO model are complete. What remains
|
||||
is filling in the command surface that real Tcl scripts depend on.
|
||||
|
||||
| Status | Work | Effort | Why it matters |
|
||||
|---|---|---|---|
|
||||
| [x] | **Phase 6a — namespace polish (`::var`)** | small | `set ::var` from inside a proc now resolves to the global (root) frame. Tokenizer also updated so `$::var` substitution works. Surfaced during socket -async test design. |
|
||||
| [x] | **Phase 6b — list ops audit** | few hours | Added `lassign`, `lrepeat`, `lset`, `lmap`. (`lsearch`, `lreplace`, `lreverse` were already present.) |
|
||||
| [x] | **Phase 6c — `dict` command additions** | small | `dict create/get/set/unset/exists/keys/values/for/update/merge/incr/append` were already implemented. Added `dict lappend`, `dict remove`, `dict filter -key`. |
|
||||
| [x] | **Phase 6d — `scan` and `format`** | few hours | Added `printf-spec` and `scan-spec` SX primitives wrapping OCaml `Printf`/`Scanf` via `Scanf.format_from_string`. Tcl `format` rewrote to dispatch via `printf-spec`; `scan` is a real walker that fills variables. Supports `%d %i %u %x %X %o %c %s %f %e %E %g %G %%` with width/precision/flags. |
|
||||
| [x] | **Phase 6e — `exec`** | few hours | `exec-process` SX primitive wraps `Unix.create_process` + `Unix.waitpid` and captures stdout. Tcl `exec cmd arg...` returns trimmed stdout; non-zero exit raises an error including stderr. Pipelines/redirection (`\|`, `>`, `<`) are not yet parsed. |
|
||||
|
||||
**Bonus perf:** `tcl-global-ref?` (called on every var-get/set) was using
|
||||
`(substring name 0 2)` — re-allocating a 2-char string per call. Switched
|
||||
to `(char-at name 0)` + `(char-at name 1)` which short-circuits on
|
||||
non-`:` names. ~6× speedup on tight loops (`factorial 10`: 16s → 2.5s).
|
||||
|
||||
`tcl-call-proc` was discarding `:fileevents`, `:timers`, and `:procs`
|
||||
updates made inside Tcl proc bodies — only `:commands` was forwarded.
|
||||
Now forwards the full set. Surfaced when socket-async made
|
||||
fileevent-from-inside-proc the canonical pattern.
|
||||
|
||||
**Bug fix landed alongside:** `vwait ::var` was infinite-looping because
|
||||
`vwait` used `frame-lookup` directly, which doesn't honour `::` global
|
||||
routing. So after `set ::done fired` (which routes the write to the root
|
||||
frame), `vwait ::done` kept reading the local frame and never saw the
|
||||
change. Added `tcl-var-lookup-or-nil` helper that mirrors `tcl-var-get`'s
|
||||
`::` routing but returns nil instead of erroring on missing vars; vwait
|
||||
and `info exists` both use it now.
|
||||
|
||||
**Total: 399/399 green** (parse 67, eval 169, error 39, namespace 22,
|
||||
coro 20, idiom 82).
|
||||
|
||||
---
|
||||
|
||||
## Suggested order
|
||||
|
||||
1. **Phase 1** — immediate Tcl wins, zero risk, proves the approach
|
||||
@@ -307,6 +343,12 @@ becomes a lasting SX contribution used by every future hosted language.
|
||||
|
||||
_Newest first._
|
||||
|
||||
- 2026-05-08: Phase 6 verified — 399/399 (parse 67, eval 169, error 39, namespace 22, coro 20, idiom 82). Fixed vwait `::var` infinite loop via tcl-var-lookup-or-nil helper; info exists also uses it now.
|
||||
- 2026-05-07: Phase 6e exec — exec-process SX primitive (Unix.create_process+waitpid, captures stdout, errors on non-zero exit with stderr) + Tcl `exec cmd arg...`; +3 idiom tests
|
||||
- 2026-05-07: Phase 6d scan/format — printf-spec + scan-spec SX primitives wrapping OCaml Printf/Scanf via Scanf.format_from_string; Tcl format rewritten to dispatch via printf-spec; scan is real walker; supports d/i/u/x/X/o/c/s/f/e/E/g/G/% with width/precision/flags; +7 idiom tests
|
||||
- 2026-05-07: Phase 6c dict additions — dict lappend / remove / filter -key (rest of dict was already implemented); +3 idiom tests
|
||||
- 2026-05-07: Phase 6b list ops — lassign / lrepeat / lset / lmap added (lsearch/lreplace/lreverse were already present); +6 idiom tests
|
||||
- 2026-05-07: Phase 6a namespace `::` prefix — tcl-global-ref?/strip-global helpers; tcl-var-get/set route `::name` to root frame; tokenizer parse-var-sub also accepts `::` start so `$::var` substitution works; tcl-call-proc forwards :fileevents/:timers/:procs; char-at fast-path optimization (~6× speedup on tight loops); +4 idiom tests
|
||||
- 2026-05-07: Phase 5f socket -async — socket-connect-async (Unix.set_nonblock+connect/EINPROGRESS) + channel-async-error (getsockopt_error); Tcl `socket -async host port` returns immediately; `fconfigure $sock -error` queries async error; +3 idiom tests; 376/376 green
|
||||
- 2026-05-07: Phase 5e clock options + scan — clock-format extended with tz arg (utc/local) + more specifiers; new clock-scan primitive with manual timegm; Tcl clock format/scan support -format/-timezone/-gmt; +5 idiom tests; 373/373 green
|
||||
- 2026-05-07: Phase 5d file ops — file-size/mtime/isfile?/isdir?/readable?/writable?/stat/delete/mkdir/copy/rename SX primitives; Tcl file isfile/isdir/readable/writable/size/mtime/atime/type/mkdir/copy/rename/delete now real; +10 idiom tests; 368/368 green
|
||||
|
||||
Reference in New Issue
Block a user