diff --git a/bin/joltc b/bin/joltc index 55e6664..0ccce51 100755 --- a/bin/joltc +++ b/bin/joltc @@ -1,14 +1,19 @@ #!/bin/sh -# joltc (jolt-9phg, inc9b) — the pure-Chez jolt runtime. NO Janet. +# joltc — the pure-Chez jolt runtime. NO Janet. # -# Runs a Clojure expression through the zero-Janet spine on Chez, using the -# checked-in bootstrap seed (host/chez/seed/). With only Chez installed and no -# Janet anywhere, this compiles and evaluates jolt code end to end. +# Compiles and evaluates jolt (Clojure) on Chez using the checked-in bootstrap +# seed (host/chez/seed/). With only Chez installed it runs jolt end to end: # -# bin/joltc -e "(+ 1 2)" +# joltc -e "(+ 1 2)" evaluate an expression +# joltc run -m app.core [args] resolve deps.edn, run a namespace's -main +# joltc -M:alias [args] run an alias's :main-opts +# joltc repl | path | REPL, print roots, or a deps.edn task # -# (bin/jolt-chez is the older Janet-hosted -e path still used as the corpus oracle; -# joltc is the fully self-hosted runtime.) +# The launcher cd's to the jolt repo root so the runtime's relative loads work; +# the user's original cwd (the project dir, where deps.edn lives) is passed in +# JOLT_PWD. root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" +JOLT_PWD="${JOLT_PWD:-$PWD}" +export JOLT_PWD cd "$root" || exit 1 exec chez --script host/chez/cli.ss "$@" diff --git a/host/chez/cli.ss b/host/chez/cli.ss index b69c90a..91a2250 100644 --- a/host/chez/cli.ss +++ b/host/chez/cli.ss @@ -1,24 +1,16 @@ -;; cli.ss (jolt-9phg, Phase 3 inc9b) — the pure-Chez jolt runtime. NO Janet. +;; cli.ss (jolt-9phg / jolt-90sp) — the pure-Chez jolt runtime. NO Janet. ;; -;; This is the zero-Janet runtime counterpart to bootstrap.ss (the zero-Janet -;; build). It loads the checked-in seed (host/chez/seed/{prelude,image}.ss — the -;; bootstrap compiler) and the zero-Janet spine, reads a Clojure expression, and -;; compiles+evals it ON CHEZ (read -> analyze -> IR -> emit -> eval). With the seed -;; checked in, a clone of the repo runs jolt with only Chez installed — no Janet at -;; build or run time. +;; Loads the checked-in seed (host/chez/seed/{prelude,image}.ss — the bootstrap +;; compiler) and the zero-Janet spine, then either evaluates a -e expression or +;; dispatches a CLI command (run/-M/repl/path/task) through jolt.main. The loader +;; (loader.ss) turns `require` into real file loading off the source roots, so a +;; multi-file project with deps.edn dependencies runs end to end. ;; -;; Run from the repo root: -;; chez --script host/chez/cli.ss -e "EXPR" +;; Run from the repo root (bin/joltc cd's there); the project dir is JOLT_PWD. (import (chezscheme)) (define cli-args (cdr (command-line))) ; drop the script name -(unless (and (= (length cli-args) 2) (string=? (car cli-args) "-e")) - (display "usage: cli.ss -e EXPR\n") - (exit 2)) -(define cli-expr (cadr cli-args)) -;; Assemble the zero-Janet spine from the checked-in seed, exactly as -;; driver/program-zero-janet does — but with no Janet generating the program. (load "host/chez/rt.ss") (set-chez-ns! "clojure.core") (load "host/chez/seed/prelude.ss") @@ -27,11 +19,23 @@ (load "host/chez/host-contract.ss") (load "host/chez/seed/image.ss") (load "host/chez/compile-eval.ss") +(load "host/chez/loader.ss") -;; Compile + eval on Chez; print the result (blank for nil, like the spine). Wrap -;; in (do ...) so a multi-form -e string evaluates every form and returns the last, -;; matching Clojure's -e (jolt-compile-eval reads a single form). -(let ((result (jolt-final-str - (jolt-compile-eval (string-append "(do " cli-expr ")") "user")))) - (unless (string=? result "") - (display result) (newline))) +;; jolt.main + jolt.deps live under jolt-core; keep them (and src/jolt) on the +;; roots so the CLI's own namespaces — and any jolt.* an app pulls in — resolve. +;; A project's resolved deps roots are prepended to these by jolt.main. +(set-source-roots! (list "jolt-core" "src/jolt")) + +(cond + ;; -e EXPR — evaluate one expression and print it (blank for nil). Wrapped in + ;; (do …) so a multi-form string evaluates every form and returns the last. + ((and (= (length cli-args) 2) (string=? (car cli-args) "-e")) + (let ((result (jolt-final-str + (jolt-compile-eval (string-append "(do " (cadr cli-args) ")") "user")))) + (unless (string=? result "") + (display result) (newline)))) + ;; otherwise dispatch the argv through jolt.main/-main + (else + (load-namespace "jolt.main") + (let ((mainv (var-deref "jolt.main" "-main"))) + (apply jolt-invoke mainv cli-args)))) diff --git a/host/chez/loader.ss b/host/chez/loader.ss new file mode 100644 index 0000000..7f7cad2 --- /dev/null +++ b/host/chez/loader.ss @@ -0,0 +1,182 @@ +;; loader.ss (jolt-90sp) — file-based namespace loading + a shell primitive. +;; +;; The corpus/CLI spine compiles one program at a time; namespaces declared in +;; that program see each other because a top-level (do …) unrolls. A real project +;; spans many FILES, so `require` must locate a namespace's source on the search +;; roots and load it — transitively, once each. This is the piece the Phase-3 +;; "cross-ns load is deferred" note left open (ns.ss). +;; +;; Loaded by cli.ss AFTER compile-eval.ss (it calls jolt-compile-eval-form). The +;; gates load compile-eval.ss but NOT this file, so the corpus/unit/sci runners +;; keep their alias-only `require` and are unaffected. + +;; --- search roots ----------------------------------------------------------- +;; An ordered list of directory strings. `require` searches them left to right. +;; The CLI seeds this with the project's resolved deps roots (jolt.deps) plus the +;; jolt-core roots so jolt.main/jolt.deps themselves load. +(define source-roots '(".")) +(define (set-source-roots! roots) (set! source-roots roots)) +(define (get-source-roots) source-roots) + +;; --- namespace -> file path ------------------------------------------------- +;; "app.commonmark-test" -> "app/commonmark_test": split on '.', munge '-'->'_' +;; per segment, join with '/'. Matches Clojure's ns->file munging. +(define (ns-seg-munge seg) + (list->string (map (lambda (c) (if (char=? c #\-) #\_ c)) (string->list seg)))) +(define (ns-name->rel name) + (let loop ((cs (string->list name)) (seg '()) (segs '())) + (cond + ((null? cs) + (let ((all (reverse (cons (list->string (reverse seg)) segs)))) + (let join ((xs all) (acc "")) + (cond ((null? xs) acc) + ((string=? acc "") (join (cdr xs) (ns-seg-munge (car xs)))) + (else (join (cdr xs) (string-append acc "/" (ns-seg-munge (car xs))))))))) + ((char=? (car cs) #\.) + (loop (cdr cs) '() (cons (list->string (reverse seg)) segs))) + (else (loop (cdr cs) (cons (car cs) seg) segs))))) + +(define (find-ns-file name) + (let ((rel (ns-name->rel name))) + (let loop ((roots source-roots)) + (if (null? roots) #f + (let ((clj (string-append (car roots) "/" rel ".clj")) + (cljc (string-append (car roots) "/" rel ".cljc"))) + (cond ((file-exists? clj) clj) + ((file-exists? cljc) cljc) + (else (loop (cdr roots))))))))) + +;; --- the loaded set --------------------------------------------------------- +;; Seeded with every namespace that already has vars at load time — the baked +;; prelude/image (clojure.core, clojure.string, jolt.analyzer, …). A `require` of +;; one of those then no-ops instead of hunting for a (nonexistent) source file. +(define loaded-ns (make-hashtable string-hash string=?)) +(vector-for-each (lambda (c) (hashtable-set! loaded-ns (var-cell-ns c) #t)) + (hashtable-values var-table)) + +;; Read every form from a file and compile+eval it in turn. The first form is +;; normally (ns …), which expands to (in-ns …) and switches the current ns, so +;; later forms compile in that namespace — (chez-current-ns) is re-read each step. +(define (load-jolt-file path) + (let loop ((src (read-file-string path))) + (let ((pn (jolt-parse-next src))) + (unless (jolt-nil? pn) + (jolt-compile-eval-form (jolt-nth pn 0) (chez-current-ns)) + (loop (jolt-nth pn 1)))))) + +;; load-namespace: load `name`'s source once. Marked loaded BEFORE eval so a +;; dependency cycle terminates (Clojure's behavior). The caller's current ns is +;; restored afterward, since loading the file switched it. +(define (load-namespace name) + (unless (hashtable-ref loaded-ns name #f) + (hashtable-set! loaded-ns name #t) + (let ((file (find-ns-file name))) + (if (not file) + (begin + (hashtable-delete! loaded-ns name) + (error #f (string-append "Could not locate " (ns-name->rel name) + ".clj (or .cljc) on the source roots") name)) + (let ((saved (chez-current-ns))) + (load-jolt-file file) + (set-chez-ns! saved) + (def-var! "clojure.core" "*ns*" (intern-ns! saved))))))) + +;; load-file: load an explicit path (a `run FILE`), in the current ns. +(define (jolt-load-file path) + (load-jolt-file path) + jolt-nil) + +;; The target ns name of a require/use spec ([ns …] / (ns …) / bare ns). +(define (spec-target-name spec) + (let ((items (cond ((pvec? spec) (seq->list spec)) + ((or (cseq? spec) (empty-list-t? spec)) (seq->list spec)) + ((symbol-t? spec) (list spec)) + (else '())))) + (and (pair? items) (symbol-t? (car items)) (symbol-t-name (car items))))) + +;; --- require/use that LOAD --------------------------------------------------- +;; Override the alias-only versions from natives-str.ss. Load each spec's target +;; (no-op if baked/already loaded), THEN register its :as/:refer under the caller +;; ns (chez-register-spec! reads the current ns, restored by load-namespace). +(define (loader-require . specs) + (for-each + (lambda (s) + (let ((target (spec-target-name s))) + (when target (load-namespace target))) + (chez-register-spec! (chez-current-ns) s)) + specs) + jolt-nil) +(def-var! "clojure.core" "require" loader-require) + +(define (loader-use . specs) + (for-each + (lambda (spec) + (let ((target (spec-target-name spec))) + (when target (load-namespace target))) + (chez-register-spec! (chez-current-ns) spec) + (let* ((items (cond ((pvec? spec) (seq->list spec)) + ((or (cseq? spec) (empty-list-t? spec)) (seq->list spec)) + ((symbol-t? spec) (list spec)) + (else '()))) + (target (and (pair? items) (symbol-t? (car items)) (symbol-t-name (car items)))) + (filtered (let scan ((xs (if (pair? items) (cdr items) '()))) + (cond ((null? xs) #f) + ((and (keyword? (car xs)) + (member (keyword-t-name (car xs)) '("only" "refer"))) #t) + (else (scan (cdr xs))))))) + (when (and target (not filtered)) + (chez-register-refer-all! (chez-current-ns) target)))) + specs) + jolt-nil) +(def-var! "clojure.core" "use" loader-use) + +(def-var! "clojure.core" "load-file" jolt-load-file) +;; load: each arg is a "/"-rooted resource path like "/app/extra"; load the file +;; for it relative to the search roots (strip the leading slash, try .clj/.cljc). +(define (jolt-load . paths) + (for-each + (lambda (p) + (let* ((rel (if (and (> (string-length p) 0) (char=? (string-ref p 0) #\/)) + (substring p 1 (string-length p)) p))) + (let loop ((roots source-roots)) + (if (null? roots) + (error #f "Could not locate resource on source roots" p) + (let ((clj (string-append (car roots) "/" rel ".clj")) + (cljc (string-append (car roots) "/" rel ".cljc"))) + (cond ((file-exists? clj) (load-jolt-file clj)) + ((file-exists? cljc) (load-jolt-file cljc)) + (else (loop (cdr roots))))))))) + paths) + jolt-nil) +(def-var! "clojure.core" "load" jolt-load) + +;; --- shell primitive (jolt.host/sh, sh-out) --------------------------------- +;; `sh` runs `sh -c CMD`, inheriting stdout/stderr (so git progress shows), and +;; returns the exit code. `sh-out` captures stdout to a string (exit ignored) for +;; commands whose output we parse (git rev-parse). Used by jolt.deps for git. +(define (jolt-sh cmd) (system cmd)) +(def-var! "jolt.host" "sh" jolt-sh) + +(define (jolt-sh-out cmd) + (call-with-values + (lambda () (open-process-ports (string-append "exec sh -c " (sh-quote cmd)) + (buffer-mode block) (native-transcoder))) + (lambda (stdin stdout stderr pid) + (close-port stdin) + (let ((out (get-string-all stdout))) + (close-port stdout) (close-port stderr) + (if (eof-object? out) "" out))))) +(define (sh-quote s) ; single-quote for the outer sh -c + (string-append "'" + (apply string-append + (map (lambda (c) (if (char=? c #\') "'\\''" (string c))) (string->list s))) + "'")) +(def-var! "jolt.host" "sh-out" jolt-sh-out) + +;; Expose source-root control + ns loading to Clojure (jolt.main / jolt.deps). +(def-var! "jolt.host" "set-source-roots!" + (lambda (roots) (set-source-roots! (seq->list roots)) jolt-nil)) +(def-var! "jolt.host" "source-roots" (lambda () (list->cseq source-roots))) +(def-var! "jolt.host" "load-namespace" (lambda (n) (load-namespace n) jolt-nil)) +(def-var! "jolt.host" "file-exists?" (lambda (p) (if (file-exists? p) #t #f))) +(def-var! "jolt.host" "getenv" (lambda (n) (let ((v (getenv n))) (if v v jolt-nil)))) diff --git a/jolt-core/jolt/deps.clj b/jolt-core/jolt/deps.clj new file mode 100644 index 0000000..1474b26 --- /dev/null +++ b/jolt-core/jolt/deps.clj @@ -0,0 +1,125 @@ +(ns jolt.deps + "Resolve a deps.edn into an ordered list of source roots — git + local deps + only, no Maven. A reduced tools.deps: :paths, :deps (`:git/url`+`:git/sha` / + `:local/root`), :aliases (:extra-paths / :extra-deps / :main-opts), :tasks. + + The deps walk is breadth-first so a top-level coordinate registers before any + transitive one (a top-level pin wins). Git deps clone into a sha-immutable + cache ($JOLT_GITLIBS, else ~/.jolt/gitlibs) shared across projects. Resolution + shells out to `git` through jolt.host/sh; nothing here touches the JVM." + (:require [clojure.edn :as edn] + [clojure.string :as str])) + +;; --- small host seams ------------------------------------------------------- +(defn- getenv [n] (jolt.host/getenv n)) +(defn- file-exists? [p] (jolt.host/file-exists? p)) +(defn- sh [cmd] (jolt.host/sh cmd)) ; exit code, inherits stdout/stderr +(defn- sh-out [cmd] (jolt.host/sh-out cmd)) ; captured stdout +(defn- warn [& xs] (println (str "[jolt.deps] " (apply str xs)))) + +(defn- read-edn [path] + (when (file-exists? path) + (try (edn/read-string (slurp path)) + (catch :default e (warn "could not read " path ": " (ex-message e)) nil)))) + +(defn- abspath [dir p] + (if (str/starts-with? p "/") p (str dir "/" p))) + +;; --- git cache -------------------------------------------------------------- +(defn- gitlibs-dir [] + (or (getenv "JOLT_GITLIBS") + (str (or (getenv "HOME") ".") "/.jolt/gitlibs"))) + +(defn- alnum? [c] + (let [n (int c)] + (or (and (>= n 48) (<= n 57)) ; 0-9 + (and (>= n 65) (<= n 90)) ; A-Z + (and (>= n 97) (<= n 122))))) ; a-z +(defn- sanitize [s] + (str/join (map (fn [c] (if (or (alnum? c) (= c \.) (= c \-)) c \_)) (seq s)))) + +(defn- ensure-git + "Clone url at sha into the cache (once); return the checkout dir." + [url sha] + (let [dir (str (gitlibs-dir) "/" (sanitize url) "/" sha)] + (if (file-exists? dir) + dir + (do + (warn "fetching " url " @ " (subs sha 0 (min 12 (count sha)))) + (sh (str "mkdir -p " (pr-str dir))) + (when-not (zero? (sh (str "git clone --quiet " (pr-str url) " " (pr-str dir)))) + (throw (ex-info (str "git clone failed: " url) {:url url}))) + (when-not (zero? (sh (str "git -C " (pr-str dir) " checkout --quiet " (pr-str sha)))) + (throw (ex-info (str "git checkout failed: " sha " in " url) {:url url :sha sha}))) + ;; submodules are pinned in the checkout; pull them if the dep uses any. + (sh (str "git -C " (pr-str dir) " submodule update --init --recursive --quiet")) + dir)))) + +;; --- coordinate -> root dir ------------------------------------------------- +(defn- coord-root + "The on-disk root directory for one dependency coordinate, or nil to skip." + [coord spec base-dir] + (cond + (:local/root spec) (abspath base-dir (:local/root spec)) + (and (:git/url spec) (:git/sha spec)) + (let [checkout (ensure-git (:git/url spec) (:git/sha spec))] + (if-let [root (:deps/root spec)] (str checkout "/" root) checkout)) + (:jolt/module spec) + (do (warn "skipping janet dependency " coord " (:jolt/module is obsolete on Chez)") nil) + :else + (do (warn "skipping unsupported coordinate " coord " " (pr-str spec)) nil))) + +(defn- dep-source-roots + "Source roots a resolved dep contributes: its deps.edn :paths (default [\"src\"]) + resolved under its root dir." + [root] + (let [edn (read-edn (str root "/deps.edn")) + paths (or (:paths edn) ["src"])] + (map #(abspath root %) paths))) + +(defn- resolve-deps + "Breadth-first walk of a deps map; returns an ordered, de-duplicated list of + source-root directories. `base-dir` resolves :local/root and is replaced by a + dep's own root as the walk descends." + [deps base-dir] + (loop [queue (mapv (fn [[c s]] [c s base-dir]) (seq deps)) + seen #{} + roots []] + (if (empty? queue) + (distinct roots) + (let [[coord spec bd] (first queue) + queue (subvec (vec queue) 1)] + (if (contains? seen coord) + (recur queue seen roots) + (let [root (coord-root coord spec bd)] + (if (nil? root) + (recur queue (conj seen coord) roots) + (let [edn (read-edn (str root "/deps.edn")) + child (mapv (fn [[c s]] [c s root]) (seq (:deps edn)))] + (recur (into queue child) + (conj seen coord) + (into roots (dep-source-roots root))))))))))) + +;; --- public ----------------------------------------------------------------- +(defn resolve-project + "Resolve `project-dir`'s deps.edn with the selected alias keywords. Returns + {:roots [...] :main-opts [...] :tasks {...}}; :main-opts is the last selected + alias's, else nil." + ([project-dir] (resolve-project project-dir [])) + ([project-dir alias-kws] + (let [edn (read-edn (str project-dir "/deps.edn")) + aliases (:aliases edn) + selected (keep #(get aliases %) alias-kws) + extra-paths (mapcat :extra-paths selected) + extra-deps (apply merge (map :extra-deps selected)) + main-opts (some :main-opts (reverse selected)) + project-paths (concat (or (:paths edn) ["src"]) extra-paths) + project-roots (map #(abspath project-dir %) project-paths) + all-deps (merge (:deps edn) extra-deps) + dep-roots (resolve-deps all-deps project-dir)] + {:roots (vec (distinct (concat project-roots dep-roots))) + :main-opts main-opts + :tasks (:tasks edn)}))) + +(defn has-deps-edn? [project-dir] + (file-exists? (str project-dir "/deps.edn"))) diff --git a/jolt-core/jolt/main.clj b/jolt-core/jolt/main.clj new file mode 100644 index 0000000..fb46c93 --- /dev/null +++ b/jolt-core/jolt/main.clj @@ -0,0 +1,106 @@ +(ns jolt.main + "The jolt CLI dispatch: resolve a project's deps.edn, set the source roots, and + run a namespace's -main, a file, a deps.edn task, or a REPL. Driven by cli.ss, + which hands it the raw argv; the project directory is JOLT_PWD (the user's cwd + before the launcher cd'd to the jolt repo)." + (:require [jolt.deps :as deps] + [clojure.string :as str])) + +(defn- project-dir [] (or (jolt.host/getenv "JOLT_PWD") ".")) + +;; Apply a resolved project's roots on top of the current (jolt-core) roots so app +;; namespaces resolve while jolt.* stays loadable. +(defn- apply-roots! [roots] + (jolt.host/set-source-roots! (vec (distinct (concat roots (jolt.host/source-roots)))))) + +(defn- run-ns + "Require ns-name and invoke its -main with the string app args." + [ns-name app-args] + (require (symbol ns-name)) + (if-let [mainv (ns-resolve (symbol ns-name) (symbol "-main"))] + (apply (deref mainv) app-args) + (throw (ex-info (str "namespace " ns-name " has no -main") {:ns ns-name})))) + +;; main-opts is a vector like ["-m" "app.core"] (optionally trailing args). Apply +;; it with the user-supplied extra args appended. +(defn- apply-main-opts [main-opts extra-args] + (cond + (and (seq main-opts) (= "-m" (first main-opts))) + (run-ns (second main-opts) (concat (drop 2 main-opts) extra-args)) + :else + (throw (ex-info (str "unsupported :main-opts " (pr-str main-opts)) {})))) + +(defn- parse-aliases [s] ; "-M:a:b" / ":a:b" -> [:a :b] + (let [s (if (str/starts-with? s "-") (subs s 2) s)] + (->> (str/split s #":") (remove str/blank?) (map keyword) vec))) + +;; run [-m NS args… | FILE] +(defn- cmd-run [more] + (let [{:keys [roots]} (deps/resolve-project (project-dir))] + (apply-roots! roots) + (cond + (= "-m" (first more)) (run-ns (second more) (drop 2 more)) + (seq more) (do (load-file (first more)) nil) + :else (throw (ex-info "run needs -m NS or a FILE" {}))))) + +;; -M:alias… — resolve with the aliases, run their :main-opts +(defn- cmd-M [arg more] + (let [aliases (parse-aliases arg) + {:keys [roots main-opts]} (deps/resolve-project (project-dir) aliases)] + (apply-roots! roots) + (if main-opts + (apply-main-opts main-opts more) + (throw (ex-info (str "alias(es) " (pr-str aliases) " have no :main-opts") {}))))) + +;; -A:alias… — add the aliases' paths/deps, then run the remaining argv as a command +(defn- cmd-A [arg more] + (let [aliases (parse-aliases arg) + {:keys [roots]} (deps/resolve-project (project-dir) aliases)] + (apply-roots! roots) + (when (seq more) (run-ns (second more) (drop 2 more))))) + +(defn- cmd-path [] + (let [{:keys [roots]} (deps/resolve-project (project-dir))] + (println (str/join ":" roots)))) + +(defn- repl [] + (println ";; jolt repl — ^D to exit") + (loop [] + (print "user=> ") (flush) + (let [line (read-line)] + (when line + (try (println (pr-str (load-string line))) + (catch :default e (println "error:" (ex-message e)))) + (recur))))) + +;; A deps.edn :tasks entry: a string is a shell command; a map is {:main-opts …}. +(defn- run-task [name more] + (let [{:keys [roots tasks]} (deps/resolve-project (project-dir)) + task (get tasks (symbol name))] + (cond + (nil? task) (throw (ex-info (str "unknown command or task: " name) {:name name})) + (string? task) (jolt.host/sh task) + (map? task) (do (apply-roots! roots) (apply-main-opts (:main-opts task) more)) + :else (throw (ex-info (str "bad task " name) {}))))) + +(defn- usage [] + (println "usage: jolt [args]") + (println " run -m NS [args] resolve deps.edn, load NS, call its -main") + (println " run FILE load a Clojure file") + (println " -M:alias [args] run the alias's :main-opts") + (println " -A:alias [args] add the alias's paths/deps") + (println " repl start a line REPL") + (println " path print the resolved source roots") + (println " run a deps.edn :tasks entry")) + +(defn -main [& args] + (let [[cmd & more] args] + (cond + (nil? cmd) (usage) + (= cmd "run") (cmd-run more) + (= cmd "repl") (repl) + (= cmd "path") (cmd-path) + (str/starts-with? cmd "-M") (cmd-M cmd more) + (str/starts-with? cmd "-A") (cmd-A cmd more) + (= cmd "-m") (cmd-run (cons "-m" more)) + :else (run-task cmd more))))