diff --git a/hosts/ocaml/lib/sx_primitives.ml b/hosts/ocaml/lib/sx_primitives.ml index a61634f1..08709251 100644 --- a/hosts/ocaml/lib/sx_primitives.ml +++ b/hosts/ocaml/lib/sx_primitives.ml @@ -3632,6 +3632,148 @@ let () = else ""))) else String trimmed); + (* exec-pipeline: takes a list of words like Tcl `exec` would receive. + Recognizes `|` as a stage separator and `> file`, `>> file`, `< file`, + `2>@1` (stderr→stdout), `2> file`. Returns trimmed stdout of the last + stage; raises Eval_error if the last stage exits non-zero. *) + register "exec-pipeline" (fun args -> + let items = match args with + | [List xs] | [ListRef { contents = xs }] -> xs + | _ -> raise (Eval_error "exec-pipeline: (word-list)") + in + let words = List.map (function + | String s -> s + | v -> Sx_types.inspect v + ) items in + if words = [] then raise (Eval_error "exec: empty command"); + let split_stages ws = + let rec loop acc cur = function + | [] -> List.rev (List.rev cur :: acc) + | "|" :: rest -> loop (List.rev cur :: acc) [] rest + | w :: rest -> loop acc (w :: cur) rest + in + loop [] [] ws + in + let extract_redirs ws = + let in_path = ref None in + let out_path = ref None in + let out_append = ref false in + let err_path = ref None in + let merge_err = ref false in + let cleaned = ref [] in + let rec loop = function + | [] -> () + | "<" :: p :: rest -> in_path := Some p; loop rest + | ">" :: p :: rest -> out_path := Some p; out_append := false; loop rest + | ">>" :: p :: rest -> out_path := Some p; out_append := true; loop rest + | "2>@1" :: rest -> merge_err := true; loop rest + | "2>" :: p :: rest -> err_path := Some p; loop rest + | w :: rest -> cleaned := w :: !cleaned; loop rest + in + loop ws; + (List.rev !cleaned, !in_path, !out_path, !out_append, !err_path, !merge_err) + in + let stages = List.map extract_redirs (split_stages words) in + if stages = [] then raise (Eval_error "exec: no stages"); + let n = List.length stages in + let pipes = Array.init (max 0 (n - 1)) (fun _ -> Unix.pipe ()) in + let (final_r, final_w) = Unix.pipe () in + let (errstash_r, errstash_w) = Unix.pipe () in + let pids = ref [] in + let close_safe fd = try Unix.close fd with _ -> () in + let open_in_redir = function + | None -> Unix.stdin + | Some path -> + (try Unix.openfile path [Unix.O_RDONLY] 0o644 + with Unix.Unix_error (e, _, _) -> + raise (Eval_error ("exec: open <" ^ path ^ ": " ^ Unix.error_message e))) + in + let open_out_redir path append = + let flags = Unix.O_WRONLY :: Unix.O_CREAT :: (if append then [Unix.O_APPEND] else [Unix.O_TRUNC]) in + try Unix.openfile path flags 0o644 + with Unix.Unix_error (e, _, _) -> + raise (Eval_error ("exec: open >" ^ path ^ ": " ^ Unix.error_message e)) + in + let stages_arr = Array.of_list stages in + (try + Array.iteri (fun i (cleaned, ip, op, app, ep, merge) -> + if cleaned = [] then raise (Eval_error "exec: empty stage in pipeline"); + let argv = Array.of_list cleaned in + let stdin_fd = + if i = 0 then open_in_redir ip + else fst pipes.(i - 1) + in + let stdout_fd = + if i = n - 1 then + (match op with + | None -> final_w + | Some path -> open_out_redir path app) + else snd pipes.(i) + in + let stderr_fd = + if merge then stdout_fd + else (match ep with + | None -> if i = n - 1 then errstash_w else Unix.stderr + | Some path -> open_out_redir path false) + in + let pid = + try Unix.create_process argv.(0) argv stdin_fd stdout_fd stderr_fd + with Unix.Unix_error (e, _, _) -> + raise (Eval_error ("exec: " ^ argv.(0) ^ ": " ^ Unix.error_message e)) + in + pids := pid :: !pids; + if i > 0 then close_safe (fst pipes.(i - 1)); + if i < n - 1 then close_safe (snd pipes.(i)); + if i = 0 && ip <> None then close_safe stdin_fd; + if i = n - 1 && op <> None then close_safe stdout_fd; + if not merge && ep <> None then close_safe stderr_fd + ) stages_arr + with e -> + close_safe final_r; close_safe final_w; + close_safe errstash_r; close_safe errstash_w; + Array.iter (fun (a,b) -> close_safe a; close_safe b) pipes; + raise e); + close_safe final_w; + close_safe errstash_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 r = Unix.read fd chunk 0 (Bytes.length chunk) in + if r = 0 then stop := true + else Buffer.add_subbytes target chunk 0 r + done + with _ -> () + in + read_all final_r buf; + read_all errstash_r errbuf; + close_safe final_r; + close_safe errstash_r; + let exit_codes = List.rev_map (fun pid -> + let (_, st) = Unix.waitpid [] pid in + match st with + | Unix.WEXITED c -> c + | _ -> 1 + ) !pids in + let final_code = match List.rev exit_codes with + | [] -> 0 + | last :: _ -> last + 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 final_code <> 0 then + raise (Eval_error ("exec: pipeline last stage exited " ^ string_of_int final_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 diff --git a/lib/tcl/runtime.sx b/lib/tcl/runtime.sx index 666e92d7..4bb268af 100644 --- a/lib/tcl/runtime.sx +++ b/lib/tcl/runtime.sx @@ -972,6 +972,15 @@ ((equal? code-str "continue") (= rc 4)) (else (= rc (parse-int code-str)))))) +; trap pattern is a list; matches errorcode list if pattern is a prefix. +(define tcl-try-trap-matches? (fn (pattern-str errorcode-str rc) (if (not (= rc 1)) false (let ((pat-elems (tcl-list-split pattern-str)) (ec-elems (tcl-list-split errorcode-str))) (if (> (len pat-elems) (len ec-elems)) false (let ((all-eq? (fn (i lim) (if (>= i lim) true (if (equal? (nth pat-elems i) (nth ec-elems i)) (all-eq? (+ i 1) lim) false))))) (all-eq? 0 (len pat-elems)))))))) + +; Brace if needs quoting for inclusion in a flat dict string. +(define tcl-try-brace-if-needed (fn (s) (if (or (equal? s "") (contains? (split s "") " ")) (str "{" s "}") s))) + +; Build the -options dict that try clause varlist captures as 2nd arg. +(define tcl-try-build-opts (fn (rc rei rec) (str "-code " rc " -level 0 -errorcode " (tcl-try-brace-if-needed rec) " -errorinfo " (tcl-try-brace-if-needed rei)))) + (define tcl-cmd-try (fn @@ -979,7 +988,7 @@ (let ((script (first args)) (rest-args (rest args))) (let - ((parse-clauses (fn (remaining acc) (if (= 0 (len remaining)) acc (let ((kw (first remaining))) (cond ((equal? kw "on") (if (< (len remaining) 4) acc (parse-clauses (slice remaining 4 (len remaining)) (append acc (list {:body (nth remaining 3) :code (nth remaining 1) :type "on" :var (nth remaining 2)}))))) ((equal? kw "finally") (if (< (len remaining) 2) acc (parse-clauses (slice remaining 2 (len remaining)) (append acc (list {:body (nth remaining 1) :type "finally"}))))) (else acc)))))) + ((parse-clauses (fn (remaining acc) (if (= 0 (len remaining)) acc (let ((kw (first remaining))) (cond ((equal? kw "on") (if (< (len remaining) 4) acc (parse-clauses (slice remaining 4 (len remaining)) (append acc (list {:body (nth remaining 3) :code (nth remaining 1) :type "on" :var (nth remaining 2)}))))) ((equal? kw "trap") (if (< (len remaining) 4) acc (parse-clauses (slice remaining 4 (len remaining)) (append acc (list {:body (nth remaining 3) :pattern (nth remaining 1) :type "trap" :var (nth remaining 2)}))))) ((equal? kw "finally") (if (< (len remaining) 2) acc (parse-clauses (slice remaining 2 (len remaining)) (append acc (list {:body (nth remaining 1) :type "finally"}))))) (else acc)))))) (clauses (parse-clauses rest-args (list)))) (let ((sub-interp (assoc interp :code 0 :result "")) @@ -989,9 +998,11 @@ (let ((rc (get result-interp :code)) (rv (get result-interp :result)) + (rei (get result-interp :errorinfo)) + (rec (get result-interp :errorcode)) (sub-output (get result-interp :output))) (let - ((find-clause (fn (cs) (if (= 0 (len cs)) nil (let ((c (first cs))) (if (and (equal? (get c :type) "on") (tcl-try-code-matches? (get c :code) rc)) c (find-clause (rest cs))))))) + ((find-clause (fn (cs) (if (= 0 (len cs)) nil (let ((c (first cs))) (cond ((and (equal? (get c :type) "on") (tcl-try-code-matches? (get c :code) rc)) c) ((and (equal? (get c :type) "trap") (tcl-try-trap-matches? (get c :pattern) rec rc)) c) (else (find-clause (rest cs)))))))) (matched (find-clause clauses)) (finally-clause (reduce @@ -1001,7 +1012,7 @@ nil clauses))) (let - ((after-handler (if (nil? matched) (assoc result-interp :output (str caller-output sub-output)) (let ((handler-interp (assoc result-interp :code 0 :output (str caller-output sub-output)))) (let ((bound-interp (if (equal? (get matched :var) "") handler-interp (tcl-var-set handler-interp (get matched :var) rv)))) (tcl-eval-string bound-interp (get matched :body))))))) + ((after-handler (if (nil? matched) (assoc result-interp :output (str caller-output sub-output)) (let ((handler-interp (assoc result-interp :code 0 :output (str caller-output sub-output))) (vars (tcl-list-split (get matched :var)))) (let ((bound1 (if (>= (len vars) 1) (tcl-var-set handler-interp (first vars) rv) handler-interp))) (let ((bound2 (if (>= (len vars) 2) (tcl-var-set bound1 (nth vars 1) (tcl-try-build-opts rc rei rec)) bound1))) (tcl-eval-string bound2 (get matched :body)))))))) (let ((final-result (if (nil? finally-clause) after-handler (let ((fi (tcl-eval-string (assoc after-handler :code 0) (get finally-clause :body)))) (if (= (get fi :code) 0) (assoc fi :code (get after-handler :code) :result (get after-handler :result)) fi))))) final-result)))))))))) @@ -1806,6 +1817,46 @@ (equal? s "off")) "1" "0")) + ((equal? class "true") + (if + (or (equal? s "1") (equal? s "true") (equal? s "yes") (equal? s "on")) + "1" + "0")) + ((equal? class "false") + (if + (or (equal? s "0") (equal? s "false") (equal? s "no") (equal? s "off")) + "1" + "0")) + ((equal? class "xdigit") + (if + (= n 0) + "0" + (if + (reduce + (fn + (ok c) + (and + ok + (or + (tcl-expr-digit? c) + (or + (and (>= c "a") (<= c "f")) + (and (>= c "A") (<= c "F")))))) + true + chars) + "1" + "0"))) + ((equal? class "ascii") + (if + (= n 0) + "1" + (if + (reduce + (fn (ok c) (and ok (and (>= c " ") (<= c "~")))) + true + chars) + "1" + "0"))) (else "0"))))) ; Build a Tcl list string from an SX list of string elements @@ -1948,6 +1999,66 @@ ((class (first rest-args)) (s (nth rest-args 1))) (assoc interp :result (tcl-string-is class s)))) ((equal? sub "cat") (assoc interp :result (join "" rest-args))) + ((equal? sub "equal") + ; string equal ?-nocase? ?-length n? s1 s2 + (let + ((nocase? + (reduce (fn (a w) (or a (equal? w "-nocase"))) false rest-args)) + (length-pos + (let ((find-loop (fn (i) (cond ((>= i (- (len rest-args) 1)) -1) ((equal? (nth rest-args i) "-length") i) (else (find-loop (+ i 1))))))) (find-loop 0))) + (cleaned (filter (fn (w) (not (equal? w "-nocase"))) rest-args))) + (let + ((cleaned2 + (if + (>= length-pos 0) + (filter + (fn (w) (and (not (equal? w "-length")) (not (equal? w (nth rest-args (+ length-pos 1)))))) + cleaned) + cleaned))) + (if + (< (len cleaned2) 2) + (error "string equal: wrong # args") + (let + ((s1 (first cleaned2)) (s2 (nth cleaned2 1))) + (let + ((c1 (if nocase? (join "" (map tcl-downcase-char (split s1 ""))) s1)) + (c2 (if nocase? (join "" (map tcl-downcase-char (split s2 ""))) s2))) + (assoc interp :result (if (equal? c1 c2) "1" "0")))))))) + ((equal? sub "totitle") + (let + ((s (first rest-args))) + (let + ((chars (split s ""))) + (assoc + interp + :result (if + (= 0 (len chars)) + "" + (str + (tcl-upcase-char (first chars)) + (join "" (map tcl-downcase-char (rest chars))))))))) + ((equal? sub "reverse") + (let + ((s (first rest-args))) + (assoc interp :result (join "" (reverse (split s "")))))) + ((equal? sub "replace") + ; string replace s first last ?newstring? + (let + ((s (first rest-args)) + (n (string-length (first rest-args))) + (fi (parse-int (nth rest-args 1))) + (li (parse-int (nth rest-args 2))) + (newstr (if (> (len rest-args) 3) (nth rest-args 3) ""))) + (let + ((f (if (< fi 0) 0 fi)) + (l (if (>= li n) (- n 1) li))) + (if + (or (> f l) (>= f n)) + (assoc interp :result s) + (let + ((before (substring s 0 f)) + (after (substring s (+ l 1) n))) + (assoc interp :result (str before newstr after))))))) (else (error (str "string: unknown subcommand: " sub)))))))) ; Resolve "end" index to numeric value given list length @@ -3326,12 +3437,37 @@ (interp args) (let ((_ (channel-flush (first args)))) (assoc interp :result "")))) -; exec cmd ?arg ...? — run external process, return stdout (newline-stripped) +; exec cmd ?arg ...? ?| cmd2 arg ...? ?> file? ?< file? ?2>@1? +; Runs external process(es), returns stdout. Pipelines via |, stdout +; redirection >/>>, stdin redirection <, stderr-to-stdout via 2>@1, +; stderr redirection 2> file. Routes through exec-pipeline if any +; shell metacharacter is present, else exec-process. (define tcl-cmd-exec (fn (interp args) - (assoc interp :result (exec-process args)))) + (let + ((has-pipeline? + (reduce + (fn + (acc w) + (or + acc + (or + (equal? w "|") + (or + (equal? w ">") + (or + (equal? w ">>") + (or + (equal? w "<") + (or (equal? w "2>") (equal? w "2>@1")))))))) + false + args))) + (assoc + interp + :result + (if has-pipeline? (exec-pipeline args) (exec-process args)))))) (define tcl-cmd-fconfigure (fn @@ -3590,6 +3726,246 @@ (interp args) (assoc (tcl-event-step interp 0) :result ""))) +; ============================================================ +; TclOO — minimal oo::class / oo::object (Phase 7d) +; ============================================================ + +; Class storage: interp :classes is a dict {name: class-record} +; class-record: {:methods (dict name => {:args :body}) :ctor :dtor :super} +; Object storage: interp :oo-objects is a dict {objname: {:class :slots}} +; Counter: interp :oo-counter — int for unique object names. + +; Extract a literal value from a parsed Tcl word (compound, braced, quoted). +; Returns nil if the word is not literal (e.g., contains $-substitution). +(define + tcl-oo-word-value + (fn + (word) + (let + ((t (get word :type))) + (cond + ((equal? t "braced") (get word :value)) + ((equal? t "quoted") (get word :value)) + ((equal? t "compound") + (let + ((parts (get word :parts))) + (let + ((all-text? + (reduce + (fn (a p) (and a (equal? (get p :type) "text"))) + true + parts))) + (if + all-text? + (join "" (map (fn (p) (get p :value)) parts)) + nil)))) + (else (get word :value)))))) + +; Recursive scan over parsed Tcl commands building a class record. +; Top-level so the recursive call resolves correctly. +(define + tcl-oo-scan-class-body + (fn + (cmds rec) + (if + (= 0 (len cmds)) + rec + (let + ((cmd (first cmds))) + (let + ((words (get cmd :words))) + (if + (= 0 (len words)) + (tcl-oo-scan-class-body (rest cmds) rec) + (let + ((kw (tcl-oo-word-value (first words)))) + (cond + ((equal? kw "superclass") + (tcl-oo-scan-class-body + (rest cmds) + (assoc rec :super (tcl-oo-word-value (nth words 1))))) + ((equal? kw "constructor") + (tcl-oo-scan-class-body + (rest cmds) + (assoc rec :ctor {:args (tcl-oo-word-value (nth words 1)) :body (tcl-oo-word-value (nth words 2))}))) + ((equal? kw "destructor") + (tcl-oo-scan-class-body + (rest cmds) + (assoc rec :dtor {:body (tcl-oo-word-value (nth words 1))}))) + ((equal? kw "method") + (let + ((mname (tcl-oo-word-value (nth words 1))) + (margs (tcl-oo-word-value (nth words 2))) + (mbody (tcl-oo-word-value (nth words 3)))) + (tcl-oo-scan-class-body + (rest cmds) + (assoc + rec + :methods + (assoc (or (get rec :methods) {}) mname {:args margs :body mbody}))))) + (else (tcl-oo-scan-class-body (rest cmds) rec)))))))))) + +; Parse class body — a Tcl script with commands `superclass NAME`, +; `constructor {args} {body}`, `destructor {body}`, `method NAME {args} {body}`. +; Returns a class record dict. +(define + tcl-oo-parse-class-body + (fn + (body) + (tcl-oo-scan-class-body + (tcl-tokenize body) + {:methods {} :ctor nil :dtor nil :super nil}))) + +; Find a method by walking class chain (this->super->...). +; Returns the method record {:args :body} or nil. +(define + tcl-oo-find-method + (fn + (interp class-name mname) + (let + ((classes (or (get interp :classes) {}))) + (let + ((cls (get classes class-name))) + (if + (nil? cls) + nil + (let + ((m (get (or (get cls :methods) {}) mname))) + (if + (not (nil? m)) + m + (let + ((super (get cls :super))) + (if + (nil? super) + nil + (tcl-oo-find-method interp super mname)))))))))) + +; Dispatch a method call on object. Sets up `self`, `my`, `class` in proc body. +(define + tcl-oo-call-method + (fn + (interp objname mname call-args) + (let + ((objects (or (get interp :oo-objects) {}))) + (let + ((obj (get objects objname))) + (if + (nil? obj) + (error (str "oo: no such object: " objname)) + (let + ((cls-name (get obj :class))) + (let + ((m (tcl-oo-find-method interp cls-name mname))) + (if + (nil? m) + (error (str "oo: object \"" objname "\" has no method \"" mname "\"")) + ; Wrap method as a proc-call; bind self, args + (let + ((pdef {:args (get m :args) :body (get m :body)})) + (let + ((interp-with-self (tcl-var-set interp "self" objname))) + (tcl-call-proc interp-with-self mname pdef call-args))))))))))) + +; Dispatcher registered as the object's command: handles `obj method ?args ...?` +; Uses a closure-style approach by having the dispatcher take obj-name baked in. +(define + tcl-oo-make-obj-dispatcher + (fn + (objname) + (fn + (interp args) + (if + (= 0 (len args)) + (error (str "oo: " objname ": method name required")) + (tcl-oo-call-method interp objname (first args) (rest args)))))) + +; Class dispatcher — handles `ClsName new ?args ...?` and similar. +(define + tcl-oo-make-class-dispatcher + (fn + (cname) + (fn + (interp args) + (if + (= 0 (len args)) + (error (str "oo: class " cname ": subcommand required")) + (let + ((sub (first args)) (rest-args (rest args))) + (cond + ((equal? sub "new") + ; Allocate object; call constructor if present + (let + ((counter (or (get interp :oo-counter) 0))) + (let + ((objname (str "::oo::object" counter))) + (let + ((classes (or (get interp :classes) {}))) + (let + ((cls (get classes cname))) + (let + ((interp1 + (assoc + interp + :oo-counter + (+ counter 1) + :oo-objects + (assoc (or (get interp :oo-objects) {}) objname {:class cname :slots {}})))) + (let + ((dispatcher (tcl-oo-make-obj-dispatcher objname))) + (let + ((interp2 (tcl-register interp1 objname dispatcher))) + (let + ((interp3 + (if + (nil? (get cls :ctor)) + interp2 + (let + ((ctor (get cls :ctor))) + (let + ((interp-with-self (tcl-var-set interp2 "self" objname))) + (let + ((cr (tcl-call-proc interp-with-self "constructor" ctor rest-args))) + (assoc cr :result objname))))))) + (assoc interp3 :result objname)))))))))) + (else (error (str "oo: class " cname " unknown subcommand: " sub))))))))) + +; oo::class create NAME body +; args = (create NAME body) +(define + tcl-cmd-oo-class + (fn + (interp args) + (if + (< (len args) 2) + (error "oo::class: wrong # args") + (let + ((sub (first args))) + (cond + ((equal? sub "create") + (let + ((cname (nth args 1)) + (body (if (> (len args) 2) (nth args 2) ""))) + (let + ((rec (tcl-oo-parse-class-body body))) + (let + ((classes (or (get interp :classes) {}))) + (let + ((with-class (assoc interp :classes (assoc classes cname rec)))) + (let + ((dispatcher (tcl-oo-make-class-dispatcher cname))) + (assoc + (tcl-register with-class cname dispatcher) + :result cname))))))) + (else (error (str "oo::class: unknown subcommand: " sub)))))))) + +; oo::object — placeholder; rarely used directly +(define + tcl-cmd-oo-object + (fn + (interp args) + (error "oo::object: not implemented as direct command"))) + ; ============================================================ ; Socket: TCP client and server (Phase 5c) ; ============================================================ @@ -4192,6 +4568,10 @@ ((i (tcl-register i "socket" tcl-cmd-socket))) (let ((i (tcl-register i "_sock-do-accept" tcl-cmd-_sock-do-accept))) + (let + ((i (tcl-register i "oo::class" tcl-cmd-oo-class))) + (let + ((i (tcl-register i "oo::object" tcl-cmd-oo-object))) (let ((i (tcl-register i "file" tcl-cmd-file))) (let @@ -4203,4 +4583,4 @@ (tcl-register i "array" - tcl-cmd-array)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) + tcl-cmd-array)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) diff --git a/lib/tcl/tests/idioms.sx b/lib/tcl/tests/idioms.sx index 7b8e1160..35370dac 100644 --- a/lib/tcl/tests/idioms.sx +++ b/lib/tcl/tests/idioms.sx @@ -524,6 +524,159 @@ (get (run "exec /bin/echo -n test") :result) "test") + ; 83-87. Phase 7a try/trap with varlist + (ok "try-trap-prefix-match" + (get + (run + "try {throw {ARITH DIVZERO} divide-by-zero} trap {ARITH} {res} {set caught $res}") + :result) + "divide-by-zero") + + (ok "try-trap-full-pattern" + (get + (run + "try {throw {FOO BAR} bad} trap {FOO BAR} {res} {return matched-foo-bar}") + :result) + "matched-foo-bar") + + (ok "try-on-error-opts" + (get + (run + "try {error oops} on error {res opts} {dict get $opts -code}") + :result) + "1") + + (ok "try-trap-no-match-falls-through" + (get + (run + "set caught notrun\ncatch {try {throw {NOPE} bad} trap {OTHER} {r} {set caught matched}}\nset caught") + :result) + "notrun") + + (ok "try-trap-then-on-error" + (get + (run + "try {error generic} trap {SPECIFIC} {r} {return trap-fired} on error {r} {return on-error-fired}") + :result) + "on-error-fired") + + ; 88-92. Phase 7b exec pipelines + redirection + (ok "exec-pipeline-tr" + (get (run "exec echo hello world | tr a-z A-Z") :result) + "HELLO WORLD") + + (ok "exec-pipeline-wc" + (get (run "exec /bin/echo abc | wc -c") :result) + "4") + + (ok "exec-redirect-stdout" + (get + (run + "set f /tmp/tcl-7b-out.txt\nexec echo hello > $f\nset r [exec cat $f]\nfile delete $f\nreturn $r") + :result) + "hello") + + (ok "exec-redirect-stdin" + (get + (run + "set f /tmp/tcl-7b-in.txt\nset c [open $f w]\nputs -nonewline $c hi\nclose $c\nset r [exec cat < $f]\nfile delete $f\nreturn $r") + :result) + "hi") + + (ok "exec-pipeline-three-stages" + (get (run "exec echo {alpha beta gamma} | tr { } \\n | wc -l") :result) + "3") + + ; 93-99. Phase 7c string command audit + (ok "string-equal" + (get (run "string equal hello hello") :result) + "1") + + (ok "string-equal-nocase" + (get (run "string equal -nocase HELLO hello") :result) + "1") + + (ok "string-totitle" + (get (run "string totitle hello") :result) + "Hello") + + (ok "string-reverse" + (get (run "string reverse hello") :result) + "olleh") + + (ok "string-replace" + (get (run "string replace hello 1 3 ZZZ") :result) + "hZZZo") + + (ok "string-is-xdigit-yes" + (get (run "string is xdigit ff00aa") :result) + "1") + + (ok "string-is-true-yes" + (get (run "string is true yes") :result) + "1") + + ; 100-105. Phase 7e regexp anchoring/boundary audit + (ok "regexp-anchor-start" + (get (run "regexp {^hello} hello-world") :result) + "1") + + (ok "regexp-anchor-end" + (get (run "regexp {world$} hello-world") :result) + "1") + + (ok "regexp-word-boundary" + (get (run "regexp {\\bword\\b} \"the word here\"") :result) + "1") + + (ok "regexp-nocase" + (get (run "regexp -nocase {HELLO} hello") :result) + "1") + + (ok "regexp-capture-var" + (get (run "regexp {[0-9]+} abc123def captured\nset captured") :result) + "123") + + (ok "regsub-all" + (get (run "regsub -all {[0-9]+} a1b22c333 X") :result) + "aXbXcX") + + ; 106-110. Phase 7d TclOO basics + (ok "oo-class-method" + (get + (run + "oo::class create C {\nmethod get {} { return 42 }\n}\nset c [C new]\n$c get") + :result) + "42") + + (ok "oo-constructor" + (get + (run + "oo::class create G {\nconstructor {n} { set ::gname $n }\nmethod hello {} { return [string cat \"hi \" $::gname] }\n}\nset g [G new World]\n$g hello") + :result) + "hi World") + + (ok "oo-inheritance-overridden" + (get + (run + "oo::class create Animal {\nmethod sound {} { return generic }\n}\noo::class create Dog {\nsuperclass Animal\nmethod sound {} { return woof }\n}\nset d [Dog new]\n$d sound") + :result) + "woof") + + (ok "oo-inheritance-inherited" + (get + (run + "oo::class create Animal {\nmethod sound {} { return generic }\n}\noo::class create Cat {\nsuperclass Animal\n}\nset c [Cat new]\n$c sound") + :result) + "generic") + + (ok "oo-multiple-instances" + (get + (run + "oo::class create N {\nconstructor {x} { set ::nval $x }\nmethod get {} { return $::nval }\n}\nset a [N new 1]\nset b [N new 99]\n$b get") + :result) + "99") + (dict "passed" tcl-idiom-pass