fix: destructuring, #() and int-typed loop variables
Compiling a real program turned up four defects, each with a test: - fn parameters could not destructure; they now bind a plain parameter and hand the pattern to the same code let uses. loop variables likewise. - :as, :or and nested patterns were unsupported in every binding form. - #(f x) read as (fn [a] f x) — a body of three forms whose value was the last one — so every anonymous function silently returned its argument. It is now one call, and %1..%9 work. - a loop variable or parameter rebound by an inner form could still be held as a raw int64, because the int analysis probed recur values in the outer scope; such names are now demoted. A rest binding is nil rather than () once exhausted, as on the JVM, and the string builtins reject a non-string instead of reading through a nil object. Also: a program that defines -main gets it called with the command-line arguments, which are also *command-line-args*; builds enable -d:ssl so the HTTP primitive can reach https. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
59d1739 parent: 2377830 added
examples/binding_forms.clj +30 -0 | new file mode 100644 | ||
| @@ -0,0 +1,30 @@ | ||
| 1 | +(ns binding-forms) | |
| 2 | + | |
| 3 | +;; destructuring in fn parameters, including multiple arities | |
| 4 | +(println ((fn [[a b] c] [a b c]) [1 2] 3)) ; [1 2 3] | |
| 5 | +(println (map (fn [[k v]] (str k v)) [[:a 1] [:b 2]])) ; (:a1 :b2) | |
| 6 | +(defn g ([[a b]] (+ a b)) ([x y] (* x y))) | |
| 7 | +(println (g [3 4]) (g 3 4)) ; 7 12 | |
| 8 | +(println ((fn [{:keys [x y]}] [x y]) {:x 1 :y 2})) ; [1 2] | |
| 9 | + | |
| 10 | +;; :as, :or, nesting, and a rest that is nil once exhausted | |
| 11 | +(let [[a b & more :as all] [1 2 3 4]] (println a b more all)) | |
| 12 | +(let [[a & r] [1]] (println r (nil? r))) ; nil true | |
| 13 | +(let [{:keys [x y] :or {y 9} :as m} {:x 1}] (println x y m)) | |
| 14 | +(let [[[a b] [c]] [[1 2] [3]]] (println a b c)) ; 1 2 3 | |
| 15 | +(println ((fn [& xs] xs)) ((fn [& xs] xs) 1)) ; nil (1) | |
| 16 | + | |
| 17 | +;; loop variables destructure too, and recur assigns the whole form | |
| 18 | +(println (loop [[t & more] [1 2 3] acc []] | |
| 19 | + (if (nil? t) acc (recur more (conj acc t))))) ; [1 2 3] | |
| 20 | + | |
| 21 | +;; #() is one call, and takes numbered arguments | |
| 22 | +(println (map #(format "%02x" %) [227 176])) ; (e3 b0) | |
| 23 | +(println (map #(* 2 %) [1 2 3])) ; (2 4 6) | |
| 24 | +(println (map #(+ %1 %2) [1 2] [10 20])) ; (11 22) | |
| 25 | +(println (#(vector % %) 7)) ; [7 7] | |
| 26 | + | |
| 27 | +;; a rebound loop variable is still a Value, not a raw int | |
| 28 | +(println (loop [i 0 acc 0] | |
| 29 | + (if (= i 3) acc | |
| 30 | + (let [[acc] [(+ acc i)]] (recur (inc i) acc))))) ; 3 | |
| new file mode 100644 | |||
| @@ -0,0 +1,30 @@ | |||
| 1 | +(ns binding-forms) | ||
| 2 | + | ||
| 3 | +;; destructuring in fn parameters, including multiple arities | ||
| 4 | +(println ((fn [[a b] c] [a b c]) [1 2] 3)) ; [1 2 3] | ||
| 5 | +(println (map (fn [[k v]] (str k v)) [[:a 1] [:b 2]])) ; (:a1 :b2) | ||
| 6 | +(defn g ([[a b]] (+ a b)) ([x y] (* x y))) | ||
| 7 | +(println (g [3 4]) (g 3 4)) ; 7 12 | ||
| 8 | +(println ((fn [{:keys [x y]}] [x y]) {:x 1 :y 2})) ; [1 2] | ||
| 9 | + | ||
| 10 | +;; :as, :or, nesting, and a rest that is nil once exhausted | ||
| 11 | +(let [[a b & more :as all] [1 2 3 4]] (println a b more all)) | ||
| 12 | +(let [[a & r] [1]] (println r (nil? r))) ; nil true | ||
| 13 | +(let [{:keys [x y] :or {y 9} :as m} {:x 1}] (println x y m)) | ||
| 14 | +(let [[[a b] [c]] [[1 2] [3]]] (println a b c)) ; 1 2 3 | ||
| 15 | +(println ((fn [& xs] xs)) ((fn [& xs] xs) 1)) ; nil (1) | ||
| 16 | + | ||
| 17 | +;; loop variables destructure too, and recur assigns the whole form | ||
| 18 | +(println (loop [[t & more] [1 2 3] acc []] | ||
| 19 | + (if (nil? t) acc (recur more (conj acc t))))) ; [1 2 3] | ||
| 20 | + | ||
| 21 | +;; #() is one call, and takes numbered arguments | ||
| 22 | +(println (map #(format "%02x" %) [227 176])) ; (e3 b0) | ||
| 23 | +(println (map #(* 2 %) [1 2 3])) ; (2 4 6) | ||
| 24 | +(println (map #(+ %1 %2) [1 2] [10 20])) ; (11 22) | ||
| 25 | +(println (#(vector % %) 7)) ; [7 7] | ||
| 26 | + | ||
| 27 | +;; a rebound loop variable is still a Value, not a raw int | ||
| 28 | +(println (loop [i 0 acc 0] | ||
| 29 | + (if (= i 3) acc | ||
| 30 | + (let [[acc] [(+ acc i)]] (recur (inc i) acc))))) ; 3 | ||
added
examples/cli_args.clj +7 -0 | new file mode 100644 | ||
| @@ -0,0 +1,7 @@ | ||
| 1 | +(ns cli-args) | |
| 2 | + | |
| 3 | +;; A program with -main gets it called with the command line, which is also | |
| 4 | +;; *command-line-args*. | |
| 5 | +(defn -main [& argv] | |
| 6 | + (println "args:" (count argv) (vec argv)) | |
| 7 | + (println "same:" (= (seq argv) (seq *command-line-args*)))) | |
| new file mode 100644 | |||
| @@ -0,0 +1,7 @@ | |||
| 1 | +(ns cli-args) | ||
| 2 | + | ||
| 3 | +;; A program with -main gets it called with the command line, which is also | ||
| 4 | +;; *command-line-args*. | ||
| 5 | +(defn -main [& argv] | ||
| 6 | + (println "args:" (count argv) (vec argv)) | ||
| 7 | + (println "same:" (= (seq argv) (seq *command-line-args*)))) | ||
modified
src/app_runtime.nim +2 -0 | @@ -18,10 +18,12 @@ proc setVar*(name: string, v: Value): Value {.discardable, importc: "clonim_set_ | ||
| 18 | 18 | proc cellGet*(c: VarCell): Value {.importc: "clonim_cell_get".} |
| 19 | 19 | proc cellIs*(c: VarCell, v: Value): bool {.importc: "clonim_cell_is".} |
| 20 | 20 | proc getVar*(name: string): Value {.importc: "clonim_get_var".} |
| 21 | +proc hasVar*(name: string): bool {.importc: "clonim_has_var".} | |
| 21 | 22 | proc call*(f: Value, args: openArray[Value]): Value {.importc: "clonim_call".} |
| 22 | 23 | proc argAt*(args: openArray[Value], i: int): Value {.importc: "clonim_arg_at".} |
| 23 | 24 | proc restArgs*(args: openArray[Value], i: int): Value {.importc: "clonim_rest_args".} |
| 24 | 25 | proc seqDrop*(v: Value, n: int): Value {.importc: "clonim_seq_drop".} |
| 26 | +proc seqDropOrNil*(v: Value, n: int): Value {.importc: "clonim_seq_drop_or_nil".} | |
| 25 | 27 | proc toSeq*(v: Value): seq[Value] {.importc: "clonim_to_seq".} |
| 26 | 28 | proc truthy*(v: Value): bool {.importc: "clonim_truthy".} |
| 27 | 29 | |
| @@ -18,10 +18,12 @@ proc setVar*(name: string, v: Value): Value {.discardable, importc: "clonim_set_ | |||
| 18 | proc cellGet*(c: VarCell): Value {.importc: "clonim_cell_get".} | 18 | proc cellGet*(c: VarCell): Value {.importc: "clonim_cell_get".} |
| 19 | proc cellIs*(c: VarCell, v: Value): bool {.importc: "clonim_cell_is".} | 19 | proc cellIs*(c: VarCell, v: Value): bool {.importc: "clonim_cell_is".} |
| 20 | proc getVar*(name: string): Value {.importc: "clonim_get_var".} | 20 | proc getVar*(name: string): Value {.importc: "clonim_get_var".} |
| 21 | +proc hasVar*(name: string): bool {.importc: "clonim_has_var".} | ||
| 21 | proc call*(f: Value, args: openArray[Value]): Value {.importc: "clonim_call".} | 22 | proc call*(f: Value, args: openArray[Value]): Value {.importc: "clonim_call".} |
| 22 | proc argAt*(args: openArray[Value], i: int): Value {.importc: "clonim_arg_at".} | 23 | proc argAt*(args: openArray[Value], i: int): Value {.importc: "clonim_arg_at".} |
| 23 | proc restArgs*(args: openArray[Value], i: int): Value {.importc: "clonim_rest_args".} | 24 | proc restArgs*(args: openArray[Value], i: int): Value {.importc: "clonim_rest_args".} |
| 24 | proc seqDrop*(v: Value, n: int): Value {.importc: "clonim_seq_drop".} | 25 | proc seqDrop*(v: Value, n: int): Value {.importc: "clonim_seq_drop".} |
| 26 | +proc seqDropOrNil*(v: Value, n: int): Value {.importc: "clonim_seq_drop_or_nil".} | ||
| 25 | proc toSeq*(v: Value): seq[Value] {.importc: "clonim_to_seq".} | 27 | proc toSeq*(v: Value): seq[Value] {.importc: "clonim_to_seq".} |
| 26 | proc truthy*(v: Value): bool {.importc: "clonim_truthy".} | 28 | proc truthy*(v: Value): bool {.importc: "clonim_truthy".} |
| 27 | 29 | ||
modified
src/clonim.nim +3 -0 | @@ -87,6 +87,8 @@ proc runtimeLib(release: bool): string = | ||
| 87 | 87 | "--nimcache:" & installed.parentDir / |
| 88 | 88 | (if release: "nimcache-release" else: "nimcache-debug"), |
| 89 | 89 | "-o:" & installed] |
| 90 | + # The HTTP primitive speaks TLS; without this it can only reach http://. | |
| 91 | + args.add "-d:ssl" | |
| 90 | 92 | if release: args.add "-d:release" |
| 91 | 93 | args.add source |
| 92 | 94 | let (output, code) = execCmdEx(args.mapIt(quoteShell(it)).join(" ")) |
| @@ -203,6 +205,7 @@ proc main() = | ||
| 203 | 205 | nimCmd.add "--passL:-Wl,--allow-multiple-definition" |
| 204 | 206 | nimCmd.add "--passL:" & rtLib |
| 205 | 207 | nimCmd.add "--passL:-lm" |
| 208 | + nimCmd.add "-d:ssl" | |
| 206 | 209 | if release: |
| 207 | 210 | # `-d:release` disables Nim runtime checks but keeps DWARF and symbol |
| 208 | 211 | # tables by default. Native deliverables should not carry that metadata. |
| @@ -87,6 +87,8 @@ proc runtimeLib(release: bool): string = | |||
| 87 | "--nimcache:" & installed.parentDir / | 87 | "--nimcache:" & installed.parentDir / |
| 88 | (if release: "nimcache-release" else: "nimcache-debug"), | 88 | (if release: "nimcache-release" else: "nimcache-debug"), |
| 89 | "-o:" & installed] | 89 | "-o:" & installed] |
| 90 | + # The HTTP primitive speaks TLS; without this it can only reach http://. | ||
| 91 | + args.add "-d:ssl" | ||
| 90 | if release: args.add "-d:release" | 92 | if release: args.add "-d:release" |
| 91 | args.add source | 93 | args.add source |
| 92 | let (output, code) = execCmdEx(args.mapIt(quoteShell(it)).join(" ")) | 94 | let (output, code) = execCmdEx(args.mapIt(quoteShell(it)).join(" ")) |
| @@ -203,6 +205,7 @@ proc main() = | |||
| 203 | nimCmd.add "--passL:-Wl,--allow-multiple-definition" | 205 | nimCmd.add "--passL:-Wl,--allow-multiple-definition" |
| 204 | nimCmd.add "--passL:" & rtLib | 206 | nimCmd.add "--passL:" & rtLib |
| 205 | nimCmd.add "--passL:-lm" | 207 | nimCmd.add "--passL:-lm" |
| 208 | + nimCmd.add "-d:ssl" | ||
| 206 | if release: | 209 | if release: |
| 207 | # `-d:release` disables Nim runtime checks but keeps DWARF and symbol | 210 | # `-d:release` disables Nim runtime checks but keeps DWARF and symbol |
| 208 | # tables by default. Native deliverables should not carry that metadata. | 211 | # tables by default. Native deliverables should not carry that metadata. |
modified
src/compiler.nim +175 -48 | @@ -255,6 +255,7 @@ type | ||
| 255 | 255 | params: seq[string] |
| 256 | 256 | restParam: string |
| 257 | 257 | body: seq[Value] |
| 258 | + destructures: seq[(Value, string)] | |
| 258 | 259 | |
| 259 | 260 | proc parseParams(v: Value): FnClause = |
| 260 | 261 | if v.isNil or v.kind != kVector: err("Parameter list must be a vector, got: " & prStr(v)) |
| @@ -264,9 +265,20 @@ proc parseParams(v: Value): FnClause = | ||
| 264 | 265 | let p = v.items[i] |
| 265 | 266 | if isSym(p, "&"): |
| 266 | 267 | if i + 1 >= v.items.len: err("Missing symbol after &") |
| 267 | - result.restParam = symName(v.items[i + 1]) | |
| 268 | + let rest = v.items[i + 1] | |
| 269 | + if rest.kind == kSymbol: result.restParam = symName(rest) | |
| 270 | + else: | |
| 271 | + result.restParam = "destr__rest" | |
| 272 | + result.destructures.add (rest, result.restParam) | |
| 268 | 273 | break |
| 269 | - result.params.add symName(p) | |
| 274 | + if p.kind == kSymbol: | |
| 275 | + result.params.add symName(p) | |
| 276 | + else: | |
| 277 | + # A destructuring parameter binds a plain one, then lets the existing | |
| 278 | + # let machinery take it apart at the top of the body. | |
| 279 | + let nm = "destr__" & $i | |
| 280 | + result.params.add nm | |
| 281 | + result.destructures.add (p, nm) | |
| 270 | 282 | inc i |
| 271 | 283 | |
| 272 | 284 | proc genClauseBody(cl: FnClause, name, selfIdent: string, env: Env, c: Ctx, |
| @@ -320,6 +332,51 @@ proc collectRecurs(forms: seq[Value], into: var seq[seq[Value]]) = | ||
| 320 | 332 | if coreName(head.s) in ["loop", "loop*", "fn", "fn*", "defn", "defn-"]: continue |
| 321 | 333 | collectRecurs(f.items, into) |
| 322 | 334 | |
| 335 | +proc patternNames(v: Value, into: var HashSet[string]) = | |
| 336 | + ## Names a binding pattern introduces. Lenient: the resolver has already | |
| 337 | + ## rejected malformed patterns, and a name too many only costs an | |
| 338 | + ## optimisation. | |
| 339 | + if v.isNil: return | |
| 340 | + case v.kind | |
| 341 | + of kSymbol: (if v.s != "&": into.incl v.s) | |
| 342 | + of kVector: | |
| 343 | + for x in v.items: patternNames(x, into) | |
| 344 | + of kMap: | |
| 345 | + for (k, value) in v.pairs: | |
| 346 | + if k.kind == kKeyword: | |
| 347 | + case k.s | |
| 348 | + of "keys", "syms", "strs": | |
| 349 | + for x in value.items: (if x.kind == kSymbol: into.incl x.s) | |
| 350 | + of "as": patternNames(value, into) | |
| 351 | + else: discard | |
| 352 | + else: patternNames(k, into) | |
| 353 | + else: discard | |
| 354 | + | |
| 355 | +proc collectBoundSymbols(v: Value, into: var HashSet[string]) = | |
| 356 | + ## Every name a binding form inside these forms introduces. A loop variable | |
| 357 | + ## or parameter that some inner form rebinds cannot be held as a raw int64: | |
| 358 | + ## the int analysis probes recur values in the outer scope, where the name | |
| 359 | + ## still looks integral even though the recur sees the inner Value binding. | |
| 360 | + if v.isNil or v.kind != kList or v.items.len == 0: return | |
| 361 | + let head = v.items[0] | |
| 362 | + if head.kind == kSymbol: | |
| 363 | + let h = coreName(head.s) | |
| 364 | + if h in ["let", "let*", "loop", "loop*", "when-let", "if-let", "doseq", | |
| 365 | + "dotimes", "binding"] and v.items.len > 1 and v.items[1].kind == kVector: | |
| 366 | + var i = 0 | |
| 367 | + while i < v.items[1].items.len: | |
| 368 | + patternNames(v.items[1].items[i], into) | |
| 369 | + i += 2 | |
| 370 | + elif h in ["fn", "fn*", "defn", "defn-"]: | |
| 371 | + for x in v.items: | |
| 372 | + if x.kind == kVector: patternNames(x, into) | |
| 373 | + elif x.kind == kList and x.items.len > 0 and x.items[0].kind == kVector: | |
| 374 | + patternNames(x.items[0], into) | |
| 375 | + for x in v.items: collectBoundSymbols(x, into) | |
| 376 | + | |
| 377 | +proc shadowedNames(forms: seq[Value]): HashSet[string] = | |
| 378 | + for f in forms: collectBoundSymbols(f, result) | |
| 379 | + | |
| 323 | 380 | proc usesIntPrim(c: Ctx, forms: seq[Value]): bool = |
| 324 | 381 | ## Whether an int-specialised twin could differ from the generic proc at all. |
| 325 | 382 | ## Emitting one for a fn that never does arithmetic just doubles the work Nim |
| @@ -377,6 +434,9 @@ proc genFn(name: string, clauses: seq[FnClause], selfIdent: string, env: Env, | ||
| 377 | 434 | probe.ints.incl p |
| 378 | 435 | probe.intFns[key] = iprc |
| 379 | 436 | var recurSafe = true |
| 437 | + let shadowed = shadowedNames(cl.body) | |
| 438 | + for p in cl.params: | |
| 439 | + if p in shadowed: recurSafe = false | |
| 380 | 440 | var myRecurs: seq[seq[Value]] = @[] |
| 381 | 441 | collectRecurs(cl.body, myRecurs) |
| 382 | 442 | for r in myRecurs: |
| @@ -442,6 +502,15 @@ proc genFn(name: string, clauses: seq[FnClause], selfIdent: string, env: Env, | ||
| 442 | 502 | c.pop |
| 443 | 503 | c.line(")") |
| 444 | 504 | |
| 505 | +proc withDestructuring(cl: FnClause): FnClause = | |
| 506 | + result = cl | |
| 507 | + if cl.destructures.len == 0: return | |
| 508 | + var bs: seq[Value] = @[] | |
| 509 | + for (pattern, nm) in cl.destructures: | |
| 510 | + bs.add pattern | |
| 511 | + bs.add mkSymbol(nm) | |
| 512 | + result.body = @[mkList(@[mkSymbol("let"), mkVector(bs)] & cl.body)] | |
| 513 | + | |
| 445 | 514 | proc genFnForm(args: seq[Value], env: Env, c: Ctx, dst: string, defName: string, |
| 446 | 515 | directEnv: Env = nil, cell = "") = |
| 447 | 516 | ## (fn name? [params] body...) or (fn name? ([params] body...) ...) |
| @@ -454,14 +523,14 @@ proc genFnForm(args: seq[Value], env: Env, c: Ctx, dst: string, defName: string, | ||
| 454 | 523 | if i < args.len and args[i].kind == kVector: |
| 455 | 524 | var cl = parseParams(args[i]) |
| 456 | 525 | cl.body = args[i + 1 .. ^1] |
| 457 | - clauses.add cl | |
| 526 | + clauses.add withDestructuring(cl) | |
| 458 | 527 | else: |
| 459 | 528 | while i < args.len: |
| 460 | 529 | let cf = args[i] |
| 461 | 530 | if cf.kind != kList or cf.items.len == 0: err("Bad fn arity form: " & prStr(cf)) |
| 462 | 531 | var cl = parseParams(cf.items[0]) |
| 463 | 532 | cl.body = cf.items[1 .. ^1] |
| 464 | - clauses.add cl | |
| 533 | + clauses.add withDestructuring(cl) | |
| 465 | 534 | inc i |
| 466 | 535 | if clauses.len == 0: err("fn requires at least one arity") |
| 467 | 536 | if name.len > 0: |
| @@ -473,6 +542,70 @@ proc genFnForm(args: seq[Value], env: Env, c: Ctx, dst: string, defName: string, | ||
| 473 | 542 | else: |
| 474 | 543 | genFn("fn", clauses, "", env, c, dst) |
| 475 | 544 | |
| 545 | +proc bindPattern(target: Value, v: string, lenv: Env, c: Ctx) = | |
| 546 | + ## Bind one binding form -- a symbol or a destructuring pattern -- to the | |
| 547 | + ## value already computed into `v`. | |
| 548 | + if target.kind == kSymbol: | |
| 549 | + let id = c.gensym("l" & mangle(target.s)) | |
| 550 | + c.line("var " & id & ": Value = " & v) | |
| 551 | + lenv.locals[target.s] = id | |
| 552 | + elif target.kind == kVector: | |
| 553 | + # sequential destructuring: [a b & rest :as whole] | |
| 554 | + var idx = 0 | |
| 555 | + var j = 0 | |
| 556 | + while j < target.items.len: | |
| 557 | + let p = target.items[j] | |
| 558 | + if p.kind == kKeyword and p.s == "as": | |
| 559 | + if j + 1 >= target.items.len: err("Missing symbol after :as") | |
| 560 | + bindPattern(target.items[j + 1], v, lenv, c) | |
| 561 | + j += 2 | |
| 562 | + continue | |
| 563 | + if isSym(p, "&"): | |
| 564 | + if j + 1 >= target.items.len: err("Missing binding form after &") | |
| 565 | + let id = c.gensym("lrest") | |
| 566 | + c.line("var " & id & ": Value = seqDropOrNil(" & v & ", " & $idx & ")") | |
| 567 | + bindPattern(target.items[j + 1], id, lenv, c) | |
| 568 | + j += 2 | |
| 569 | + continue | |
| 570 | + let id = c.gensym("lnth") | |
| 571 | + c.line("var " & id & ": Value = call(getVar(\"clojure.core/nth\"), [" & v & ", mkInt(" & | |
| 572 | + $idx & "), NilV])") | |
| 573 | + bindPattern(p, id, lenv, c) | |
| 574 | + inc idx; inc j | |
| 575 | + elif target.kind == kMap: | |
| 576 | + # associative destructuring: {a :a, :keys [b c], :or {b 1}, :as m} | |
| 577 | + var defaults = NilV | |
| 578 | + for (k, valForm) in target.pairs: | |
| 579 | + if k.kind == kKeyword and k.s == "or": defaults = valForm | |
| 580 | + proc bindKey(nm: string, key: string) = | |
| 581 | + let id = c.gensym("l" & mangle(nm)) | |
| 582 | + var default = "NilV" | |
| 583 | + if defaults.kind == kMap: | |
| 584 | + for (dk, dv) in defaults.pairs: | |
| 585 | + if dk.kind == kSymbol and dk.s == nm: default = genExpr(dv, lenv, c) | |
| 586 | + c.line("var " & id & ": Value = call(getVar(\"clojure.core/get\"), [" & v & | |
| 587 | + ", mkKeyword(" & nimStr(key) & "), " & default & "])") | |
| 588 | + lenv.locals[nm] = id | |
| 589 | + for (k, valForm) in target.pairs: | |
| 590 | + if k.kind == kKeyword: | |
| 591 | + case k.s | |
| 592 | + of "or": discard | |
| 593 | + of "as": bindPattern(valForm, v, lenv, c) | |
| 594 | + of "keys": | |
| 595 | + for ks in valForm.items: bindKey(symName(ks), symName(ks)) | |
| 596 | + of "strs": | |
| 597 | + for ks in valForm.items: bindKey(symName(ks), symName(ks)) | |
| 598 | + else: err("Unsupported destructuring directive: :" & k.s) | |
| 599 | + elif valForm.kind == kKeyword and k.kind == kSymbol: | |
| 600 | + bindKey(symName(k), valForm.s) | |
| 601 | + else: | |
| 602 | + let id = c.gensym("l" & mangle(symName(k))) | |
| 603 | + let kv = genExpr(valForm, lenv, c) | |
| 604 | + c.line("var " & id & ": Value = call(getVar(\"clojure.core/get\"), [" & v & ", " & kv & "])") | |
| 605 | + lenv.locals[symName(k)] = id | |
| 606 | + else: | |
| 607 | + err("Unsupported binding form: " & prStr(target)) | |
| 608 | + | |
| 476 | 609 | proc genLet(bindings: Value, body: seq[Value], dst: string, env: Env, c: Ctx) = |
| 477 | 610 | if bindings.isNil or bindings.kind != kVector: |
| 478 | 611 | err("let requires a vector for its bindings") |
| @@ -481,57 +614,35 @@ proc genLet(bindings: Value, body: seq[Value], dst: string, env: Env, c: Ctx) = | ||
| 481 | 614 | let lenv = newEnv(env) |
| 482 | 615 | var i = 0 |
| 483 | 616 | while i < bindings.items.len: |
| 484 | - let target = bindings.items[i] | |
| 485 | - let initForm = bindings.items[i + 1] | |
| 486 | - let v = genExpr(initForm, lenv, c) | |
| 487 | - if target.kind == kSymbol: | |
| 488 | - let id = c.gensym("l" & mangle(target.s)) | |
| 489 | - c.line("var " & id & ": Value = " & v) | |
| 490 | - lenv.locals[target.s] = id | |
| 491 | - elif target.kind == kVector: | |
| 492 | - # sequential destructuring: [a b & rest] | |
| 493 | - var idx = 0 | |
| 494 | - var j = 0 | |
| 495 | - while j < target.items.len: | |
| 496 | - let p = target.items[j] | |
| 497 | - if isSym(p, "&"): | |
| 498 | - let restSym = symName(target.items[j + 1]) | |
| 499 | - let id = c.gensym("l" & mangle(restSym)) | |
| 500 | - c.line("var " & id & ": Value = seqDrop(" & v & ", " & $idx & ")") | |
| 501 | - lenv.locals[restSym] = id | |
| 502 | - break | |
| 503 | - let id = c.gensym("l" & mangle(symName(p))) | |
| 504 | - c.line("var " & id & ": Value = call(getVar(\"clojure.core/nth\"), [" & v & ", mkInt(" & | |
| 505 | - $idx & "), NilV])") | |
| 506 | - lenv.locals[symName(p)] = id | |
| 507 | - inc idx; inc j | |
| 508 | - elif target.kind == kMap: | |
| 509 | - # associative destructuring: {a :a, :keys [b c]} | |
| 510 | - for (k, valForm) in target.pairs: | |
| 511 | - if k.kind == kKeyword and k.s == "keys": | |
| 512 | - for ks in valForm.items: | |
| 513 | - let nm = symName(ks) | |
| 514 | - let id = c.gensym("l" & mangle(nm)) | |
| 515 | - c.line("var " & id & ": Value = call(getVar(\"clojure.core/get\"), [" & v & | |
| 516 | - ", mkKeyword(" & nimStr(nm) & ")])") | |
| 517 | - lenv.locals[nm] = id | |
| 518 | - else: | |
| 519 | - let nm = symName(k) | |
| 520 | - let id = c.gensym("l" & mangle(nm)) | |
| 521 | - let kv = genExpr(valForm, lenv, c) | |
| 522 | - c.line("var " & id & ": Value = call(getVar(\"clojure.core/get\"), [" & v & ", " & kv & "])") | |
| 523 | - lenv.locals[nm] = id | |
| 524 | - else: | |
| 525 | - err("Unsupported binding form: " & prStr(target)) | |
| 617 | + let v = genExpr(bindings.items[i + 1], lenv, c) | |
| 618 | + bindPattern(bindings.items[i], v, lenv, c) | |
| 526 | 619 | i += 2 |
| 527 | 620 | genBody(body, dst, lenv, c) |
| 528 | 621 | |
| 529 | 622 | proc genLoop(bindings: Value, body: seq[Value], dst: string, env: Env, c: Ctx) = |
| 530 | 623 | if bindings.isNil or bindings.kind != kVector or bindings.items.len mod 2 != 0: |
| 531 | 624 | err("loop requires an even-sized binding vector") |
| 625 | + # A destructuring loop variable becomes a plain one that recur can assign, | |
| 626 | + # taken apart again by a let at the top of the body. | |
| 627 | + var bindings = bindings | |
| 628 | + var body = body | |
| 629 | + var patterns: seq[Value] = @[] | |
| 630 | + var i = 0 | |
| 631 | + while i < bindings.items.len: | |
| 632 | + if bindings.items[i].kind != kSymbol: | |
| 633 | + let nm = mkSymbol("loop__" & $i) | |
| 634 | + patterns.add bindings.items[i] | |
| 635 | + patterns.add nm | |
| 636 | + var xs = bindings.items | |
| 637 | + xs[i] = nm | |
| 638 | + bindings = mkVector(xs) | |
| 639 | + i += 2 | |
| 640 | + if patterns.len > 0: | |
| 641 | + body = @[mkList(@[mkSymbol("let"), mkVector(patterns)] & body)] | |
| 642 | + | |
| 532 | 643 | var names: seq[string] = @[] |
| 533 | 644 | var inits: seq[Value] = @[] |
| 534 | - var i = 0 | |
| 645 | + i = 0 | |
| 535 | 646 | while i < bindings.items.len: |
| 536 | 647 | names.add symName(bindings.items[i]) |
| 537 | 648 | inits.add bindings.items[i + 1] |
| @@ -545,8 +656,9 @@ proc genLoop(bindings: Value, body: seq[Value], dst: string, env: Env, c: Ctx) = | ||
| 545 | 656 | collectRecurs(body, recurs) |
| 546 | 657 | for r in recurs: |
| 547 | 658 | if r.len != names.len: recurs = @[]; break # arity error, reported later |
| 659 | + let shadowed = shadowedNames(body) | |
| 548 | 660 | var isInt: seq[bool] = @[] |
| 549 | - for n in names: isInt.add true | |
| 661 | + for n in names: isInt.add n notin shadowed | |
| 550 | 662 | var probeIdents: seq[string] = @[] |
| 551 | 663 | for n in names: probeIdents.add "probe" |
| 552 | 664 | while true: |
| @@ -1160,6 +1272,7 @@ proc genInto(f: Value, dst: string, env: Env, c: Ctx) = | ||
| 1160 | 1272 | # ------------------------------------------------------------- entry point |
| 1161 | 1273 | const preamble = """ |
| 1162 | 1274 | ## Generated by clonim. Do not edit. |
| 1275 | +import std/os | |
| 1163 | 1276 | import app_runtime |
| 1164 | 1277 | |
| 1165 | 1278 | proc cljMain() = |
| @@ -1191,6 +1304,20 @@ proc compileForms*(forms: seq[Value]): string = | ||
| 1191 | 1304 | c.line("var " & t & ": Value = NilV") |
| 1192 | 1305 | genInto(f, t, env, c) |
| 1193 | 1306 | c.line("discard " & t) |
| 1307 | + # A program that defines -main gets it called with the command-line | |
| 1308 | + # arguments once the top-level forms have run, as on the JVM. | |
| 1309 | + var mainVar = "" | |
| 1310 | + for name, _ in c.defCounts: | |
| 1311 | + if name == "-main" or name.endsWith("/-main"): mainVar = name | |
| 1312 | + c.line("var clonimArgv: seq[Value] = @[]") | |
| 1313 | + c.line("for a in commandLineParams(): clonimArgv.add mkStr(a)") | |
| 1314 | + c.line("setVar(\"clojure.core/*command-line-args*\", " & | |
| 1315 | + "(if clonimArgv.len == 0: NilV else: mkList(clonimArgv)))") | |
| 1316 | + if mainVar.len > 0: | |
| 1317 | + c.line("if hasVar(" & nimStr(mainVar) & "):") | |
| 1318 | + c.push | |
| 1319 | + c.line("discard call(getVar(" & nimStr(mainVar) & "), clonimArgv)") | |
| 1320 | + c.pop | |
| 1194 | 1321 | var src = preamble & " initClonimRuntime()\n registerCore()\n registerNamespaceCore()\n" & c.prelude.join("\n") & "\n" & |
| 1195 | 1322 | c.body.join("\n") & "\n\n" |
| 1196 | 1323 | src &= """ |
| @@ -255,6 +255,7 @@ type | |||
| 255 | params: seq[string] | 255 | params: seq[string] |
| 256 | restParam: string | 256 | restParam: string |
| 257 | body: seq[Value] | 257 | body: seq[Value] |
| 258 | + destructures: seq[(Value, string)] | ||
| 258 | 259 | ||
| 259 | proc parseParams(v: Value): FnClause = | 260 | proc parseParams(v: Value): FnClause = |
| 260 | if v.isNil or v.kind != kVector: err("Parameter list must be a vector, got: " & prStr(v)) | 261 | if v.isNil or v.kind != kVector: err("Parameter list must be a vector, got: " & prStr(v)) |
| @@ -264,9 +265,20 @@ proc parseParams(v: Value): FnClause = | |||
| 264 | let p = v.items[i] | 265 | let p = v.items[i] |
| 265 | if isSym(p, "&"): | 266 | if isSym(p, "&"): |
| 266 | if i + 1 >= v.items.len: err("Missing symbol after &") | 267 | if i + 1 >= v.items.len: err("Missing symbol after &") |
| 267 | - result.restParam = symName(v.items[i + 1]) | 268 | + let rest = v.items[i + 1] |
| 269 | + if rest.kind == kSymbol: result.restParam = symName(rest) | ||
| 270 | + else: | ||
| 271 | + result.restParam = "destr__rest" | ||
| 272 | + result.destructures.add (rest, result.restParam) | ||
| 268 | break | 273 | break |
| 269 | - result.params.add symName(p) | 274 | + if p.kind == kSymbol: |
| 275 | + result.params.add symName(p) | ||
| 276 | + else: | ||
| 277 | + # A destructuring parameter binds a plain one, then lets the existing | ||
| 278 | + # let machinery take it apart at the top of the body. | ||
| 279 | + let nm = "destr__" & $i | ||
| 280 | + result.params.add nm | ||
| 281 | + result.destructures.add (p, nm) | ||
| 270 | inc i | 282 | inc i |
| 271 | 283 | ||
| 272 | proc genClauseBody(cl: FnClause, name, selfIdent: string, env: Env, c: Ctx, | 284 | proc genClauseBody(cl: FnClause, name, selfIdent: string, env: Env, c: Ctx, |
| @@ -320,6 +332,51 @@ proc collectRecurs(forms: seq[Value], into: var seq[seq[Value]]) = | |||
| 320 | if coreName(head.s) in ["loop", "loop*", "fn", "fn*", "defn", "defn-"]: continue | 332 | if coreName(head.s) in ["loop", "loop*", "fn", "fn*", "defn", "defn-"]: continue |
| 321 | collectRecurs(f.items, into) | 333 | collectRecurs(f.items, into) |
| 322 | 334 | ||
| 335 | +proc patternNames(v: Value, into: var HashSet[string]) = | ||
| 336 | + ## Names a binding pattern introduces. Lenient: the resolver has already | ||
| 337 | + ## rejected malformed patterns, and a name too many only costs an | ||
| 338 | + ## optimisation. | ||
| 339 | + if v.isNil: return | ||
| 340 | + case v.kind | ||
| 341 | + of kSymbol: (if v.s != "&": into.incl v.s) | ||
| 342 | + of kVector: | ||
| 343 | + for x in v.items: patternNames(x, into) | ||
| 344 | + of kMap: | ||
| 345 | + for (k, value) in v.pairs: | ||
| 346 | + if k.kind == kKeyword: | ||
| 347 | + case k.s | ||
| 348 | + of "keys", "syms", "strs": | ||
| 349 | + for x in value.items: (if x.kind == kSymbol: into.incl x.s) | ||
| 350 | + of "as": patternNames(value, into) | ||
| 351 | + else: discard | ||
| 352 | + else: patternNames(k, into) | ||
| 353 | + else: discard | ||
| 354 | + | ||
| 355 | +proc collectBoundSymbols(v: Value, into: var HashSet[string]) = | ||
| 356 | + ## Every name a binding form inside these forms introduces. A loop variable | ||
| 357 | + ## or parameter that some inner form rebinds cannot be held as a raw int64: | ||
| 358 | + ## the int analysis probes recur values in the outer scope, where the name | ||
| 359 | + ## still looks integral even though the recur sees the inner Value binding. | ||
| 360 | + if v.isNil or v.kind != kList or v.items.len == 0: return | ||
| 361 | + let head = v.items[0] | ||
| 362 | + if head.kind == kSymbol: | ||
| 363 | + let h = coreName(head.s) | ||
| 364 | + if h in ["let", "let*", "loop", "loop*", "when-let", "if-let", "doseq", | ||
| 365 | + "dotimes", "binding"] and v.items.len > 1 and v.items[1].kind == kVector: | ||
| 366 | + var i = 0 | ||
| 367 | + while i < v.items[1].items.len: | ||
| 368 | + patternNames(v.items[1].items[i], into) | ||
| 369 | + i += 2 | ||
| 370 | + elif h in ["fn", "fn*", "defn", "defn-"]: | ||
| 371 | + for x in v.items: | ||
| 372 | + if x.kind == kVector: patternNames(x, into) | ||
| 373 | + elif x.kind == kList and x.items.len > 0 and x.items[0].kind == kVector: | ||
| 374 | + patternNames(x.items[0], into) | ||
| 375 | + for x in v.items: collectBoundSymbols(x, into) | ||
| 376 | + | ||
| 377 | +proc shadowedNames(forms: seq[Value]): HashSet[string] = | ||
| 378 | + for f in forms: collectBoundSymbols(f, result) | ||
| 379 | + | ||
| 323 | proc usesIntPrim(c: Ctx, forms: seq[Value]): bool = | 380 | proc usesIntPrim(c: Ctx, forms: seq[Value]): bool = |
| 324 | ## Whether an int-specialised twin could differ from the generic proc at all. | 381 | ## Whether an int-specialised twin could differ from the generic proc at all. |
| 325 | ## Emitting one for a fn that never does arithmetic just doubles the work Nim | 382 | ## Emitting one for a fn that never does arithmetic just doubles the work Nim |
| @@ -377,6 +434,9 @@ proc genFn(name: string, clauses: seq[FnClause], selfIdent: string, env: Env, | |||
| 377 | probe.ints.incl p | 434 | probe.ints.incl p |
| 378 | probe.intFns[key] = iprc | 435 | probe.intFns[key] = iprc |
| 379 | var recurSafe = true | 436 | var recurSafe = true |
| 437 | + let shadowed = shadowedNames(cl.body) | ||
| 438 | + for p in cl.params: | ||
| 439 | + if p in shadowed: recurSafe = false | ||
| 380 | var myRecurs: seq[seq[Value]] = @[] | 440 | var myRecurs: seq[seq[Value]] = @[] |
| 381 | collectRecurs(cl.body, myRecurs) | 441 | collectRecurs(cl.body, myRecurs) |
| 382 | for r in myRecurs: | 442 | for r in myRecurs: |
| @@ -442,6 +502,15 @@ proc genFn(name: string, clauses: seq[FnClause], selfIdent: string, env: Env, | |||
| 442 | c.pop | 502 | c.pop |
| 443 | c.line(")") | 503 | c.line(")") |
| 444 | 504 | ||
| 505 | +proc withDestructuring(cl: FnClause): FnClause = | ||
| 506 | + result = cl | ||
| 507 | + if cl.destructures.len == 0: return | ||
| 508 | + var bs: seq[Value] = @[] | ||
| 509 | + for (pattern, nm) in cl.destructures: | ||
| 510 | + bs.add pattern | ||
| 511 | + bs.add mkSymbol(nm) | ||
| 512 | + result.body = @[mkList(@[mkSymbol("let"), mkVector(bs)] & cl.body)] | ||
| 513 | + | ||
| 445 | proc genFnForm(args: seq[Value], env: Env, c: Ctx, dst: string, defName: string, | 514 | proc genFnForm(args: seq[Value], env: Env, c: Ctx, dst: string, defName: string, |
| 446 | directEnv: Env = nil, cell = "") = | 515 | directEnv: Env = nil, cell = "") = |
| 447 | ## (fn name? [params] body...) or (fn name? ([params] body...) ...) | 516 | ## (fn name? [params] body...) or (fn name? ([params] body...) ...) |
| @@ -454,14 +523,14 @@ proc genFnForm(args: seq[Value], env: Env, c: Ctx, dst: string, defName: string, | |||
| 454 | if i < args.len and args[i].kind == kVector: | 523 | if i < args.len and args[i].kind == kVector: |
| 455 | var cl = parseParams(args[i]) | 524 | var cl = parseParams(args[i]) |
| 456 | cl.body = args[i + 1 .. ^1] | 525 | cl.body = args[i + 1 .. ^1] |
| 457 | - clauses.add cl | 526 | + clauses.add withDestructuring(cl) |
| 458 | else: | 527 | else: |
| 459 | while i < args.len: | 528 | while i < args.len: |
| 460 | let cf = args[i] | 529 | let cf = args[i] |
| 461 | if cf.kind != kList or cf.items.len == 0: err("Bad fn arity form: " & prStr(cf)) | 530 | if cf.kind != kList or cf.items.len == 0: err("Bad fn arity form: " & prStr(cf)) |
| 462 | var cl = parseParams(cf.items[0]) | 531 | var cl = parseParams(cf.items[0]) |
| 463 | cl.body = cf.items[1 .. ^1] | 532 | cl.body = cf.items[1 .. ^1] |
| 464 | - clauses.add cl | 533 | + clauses.add withDestructuring(cl) |
| 465 | inc i | 534 | inc i |
| 466 | if clauses.len == 0: err("fn requires at least one arity") | 535 | if clauses.len == 0: err("fn requires at least one arity") |
| 467 | if name.len > 0: | 536 | if name.len > 0: |
| @@ -473,6 +542,70 @@ proc genFnForm(args: seq[Value], env: Env, c: Ctx, dst: string, defName: string, | |||
| 473 | else: | 542 | else: |
| 474 | genFn("fn", clauses, "", env, c, dst) | 543 | genFn("fn", clauses, "", env, c, dst) |
| 475 | 544 | ||
| 545 | +proc bindPattern(target: Value, v: string, lenv: Env, c: Ctx) = | ||
| 546 | + ## Bind one binding form -- a symbol or a destructuring pattern -- to the | ||
| 547 | + ## value already computed into `v`. | ||
| 548 | + if target.kind == kSymbol: | ||
| 549 | + let id = c.gensym("l" & mangle(target.s)) | ||
| 550 | + c.line("var " & id & ": Value = " & v) | ||
| 551 | + lenv.locals[target.s] = id | ||
| 552 | + elif target.kind == kVector: | ||
| 553 | + # sequential destructuring: [a b & rest :as whole] | ||
| 554 | + var idx = 0 | ||
| 555 | + var j = 0 | ||
| 556 | + while j < target.items.len: | ||
| 557 | + let p = target.items[j] | ||
| 558 | + if p.kind == kKeyword and p.s == "as": | ||
| 559 | + if j + 1 >= target.items.len: err("Missing symbol after :as") | ||
| 560 | + bindPattern(target.items[j + 1], v, lenv, c) | ||
| 561 | + j += 2 | ||
| 562 | + continue | ||
| 563 | + if isSym(p, "&"): | ||
| 564 | + if j + 1 >= target.items.len: err("Missing binding form after &") | ||
| 565 | + let id = c.gensym("lrest") | ||
| 566 | + c.line("var " & id & ": Value = seqDropOrNil(" & v & ", " & $idx & ")") | ||
| 567 | + bindPattern(target.items[j + 1], id, lenv, c) | ||
| 568 | + j += 2 | ||
| 569 | + continue | ||
| 570 | + let id = c.gensym("lnth") | ||
| 571 | + c.line("var " & id & ": Value = call(getVar(\"clojure.core/nth\"), [" & v & ", mkInt(" & | ||
| 572 | + $idx & "), NilV])") | ||
| 573 | + bindPattern(p, id, lenv, c) | ||
| 574 | + inc idx; inc j | ||
| 575 | + elif target.kind == kMap: | ||
| 576 | + # associative destructuring: {a :a, :keys [b c], :or {b 1}, :as m} | ||
| 577 | + var defaults = NilV | ||
| 578 | + for (k, valForm) in target.pairs: | ||
| 579 | + if k.kind == kKeyword and k.s == "or": defaults = valForm | ||
| 580 | + proc bindKey(nm: string, key: string) = | ||
| 581 | + let id = c.gensym("l" & mangle(nm)) | ||
| 582 | + var default = "NilV" | ||
| 583 | + if defaults.kind == kMap: | ||
| 584 | + for (dk, dv) in defaults.pairs: | ||
| 585 | + if dk.kind == kSymbol and dk.s == nm: default = genExpr(dv, lenv, c) | ||
| 586 | + c.line("var " & id & ": Value = call(getVar(\"clojure.core/get\"), [" & v & | ||
| 587 | + ", mkKeyword(" & nimStr(key) & "), " & default & "])") | ||
| 588 | + lenv.locals[nm] = id | ||
| 589 | + for (k, valForm) in target.pairs: | ||
| 590 | + if k.kind == kKeyword: | ||
| 591 | + case k.s | ||
| 592 | + of "or": discard | ||
| 593 | + of "as": bindPattern(valForm, v, lenv, c) | ||
| 594 | + of "keys": | ||
| 595 | + for ks in valForm.items: bindKey(symName(ks), symName(ks)) | ||
| 596 | + of "strs": | ||
| 597 | + for ks in valForm.items: bindKey(symName(ks), symName(ks)) | ||
| 598 | + else: err("Unsupported destructuring directive: :" & k.s) | ||
| 599 | + elif valForm.kind == kKeyword and k.kind == kSymbol: | ||
| 600 | + bindKey(symName(k), valForm.s) | ||
| 601 | + else: | ||
| 602 | + let id = c.gensym("l" & mangle(symName(k))) | ||
| 603 | + let kv = genExpr(valForm, lenv, c) | ||
| 604 | + c.line("var " & id & ": Value = call(getVar(\"clojure.core/get\"), [" & v & ", " & kv & "])") | ||
| 605 | + lenv.locals[symName(k)] = id | ||
| 606 | + else: | ||
| 607 | + err("Unsupported binding form: " & prStr(target)) | ||
| 608 | + | ||
| 476 | proc genLet(bindings: Value, body: seq[Value], dst: string, env: Env, c: Ctx) = | 609 | proc genLet(bindings: Value, body: seq[Value], dst: string, env: Env, c: Ctx) = |
| 477 | if bindings.isNil or bindings.kind != kVector: | 610 | if bindings.isNil or bindings.kind != kVector: |
| 478 | err("let requires a vector for its bindings") | 611 | err("let requires a vector for its bindings") |
| @@ -481,57 +614,35 @@ proc genLet(bindings: Value, body: seq[Value], dst: string, env: Env, c: Ctx) = | |||
| 481 | let lenv = newEnv(env) | 614 | let lenv = newEnv(env) |
| 482 | var i = 0 | 615 | var i = 0 |
| 483 | while i < bindings.items.len: | 616 | while i < bindings.items.len: |
| 484 | - let target = bindings.items[i] | 617 | + let v = genExpr(bindings.items[i + 1], lenv, c) |
| 485 | - let initForm = bindings.items[i + 1] | 618 | + bindPattern(bindings.items[i], v, lenv, c) |
| 486 | - let v = genExpr(initForm, lenv, c) | ||
| 487 | - if target.kind == kSymbol: | ||
| 488 | - let id = c.gensym("l" & mangle(target.s)) | ||
| 489 | - c.line("var " & id & ": Value = " & v) | ||
| 490 | - lenv.locals[target.s] = id | ||
| 491 | - elif target.kind == kVector: | ||
| 492 | - # sequential destructuring: [a b & rest] | ||
| 493 | - var idx = 0 | ||
| 494 | - var j = 0 | ||
| 495 | - while j < target.items.len: | ||
| 496 | - let p = target.items[j] | ||
| 497 | - if isSym(p, "&"): | ||
| 498 | - let restSym = symName(target.items[j + 1]) | ||
| 499 | - let id = c.gensym("l" & mangle(restSym)) | ||
| 500 | - c.line("var " & id & ": Value = seqDrop(" & v & ", " & $idx & ")") | ||
| 501 | - lenv.locals[restSym] = id | ||
| 502 | - break | ||
| 503 | - let id = c.gensym("l" & mangle(symName(p))) | ||
| 504 | - c.line("var " & id & ": Value = call(getVar(\"clojure.core/nth\"), [" & v & ", mkInt(" & | ||
| 505 | - $idx & "), NilV])") | ||
| 506 | - lenv.locals[symName(p)] = id | ||
| 507 | - inc idx; inc j | ||
| 508 | - elif target.kind == kMap: | ||
| 509 | - # associative destructuring: {a :a, :keys [b c]} | ||
| 510 | - for (k, valForm) in target.pairs: | ||
| 511 | - if k.kind == kKeyword and k.s == "keys": | ||
| 512 | - for ks in valForm.items: | ||
| 513 | - let nm = symName(ks) | ||
| 514 | - let id = c.gensym("l" & mangle(nm)) | ||
| 515 | - c.line("var " & id & ": Value = call(getVar(\"clojure.core/get\"), [" & v & | ||
| 516 | - ", mkKeyword(" & nimStr(nm) & ")])") | ||
| 517 | - lenv.locals[nm] = id | ||
| 518 | - else: | ||
| 519 | - let nm = symName(k) | ||
| 520 | - let id = c.gensym("l" & mangle(nm)) | ||
| 521 | - let kv = genExpr(valForm, lenv, c) | ||
| 522 | - c.line("var " & id & ": Value = call(getVar(\"clojure.core/get\"), [" & v & ", " & kv & "])") | ||
| 523 | - lenv.locals[nm] = id | ||
| 524 | - else: | ||
| 525 | - err("Unsupported binding form: " & prStr(target)) | ||
| 526 | i += 2 | 619 | i += 2 |
| 527 | genBody(body, dst, lenv, c) | 620 | genBody(body, dst, lenv, c) |
| 528 | 621 | ||
| 529 | proc genLoop(bindings: Value, body: seq[Value], dst: string, env: Env, c: Ctx) = | 622 | proc genLoop(bindings: Value, body: seq[Value], dst: string, env: Env, c: Ctx) = |
| 530 | if bindings.isNil or bindings.kind != kVector or bindings.items.len mod 2 != 0: | 623 | if bindings.isNil or bindings.kind != kVector or bindings.items.len mod 2 != 0: |
| 531 | err("loop requires an even-sized binding vector") | 624 | err("loop requires an even-sized binding vector") |
| 625 | + # A destructuring loop variable becomes a plain one that recur can assign, | ||
| 626 | + # taken apart again by a let at the top of the body. | ||
| 627 | + var bindings = bindings | ||
| 628 | + var body = body | ||
| 629 | + var patterns: seq[Value] = @[] | ||
| 630 | + var i = 0 | ||
| 631 | + while i < bindings.items.len: | ||
| 632 | + if bindings.items[i].kind != kSymbol: | ||
| 633 | + let nm = mkSymbol("loop__" & $i) | ||
| 634 | + patterns.add bindings.items[i] | ||
| 635 | + patterns.add nm | ||
| 636 | + var xs = bindings.items | ||
| 637 | + xs[i] = nm | ||
| 638 | + bindings = mkVector(xs) | ||
| 639 | + i += 2 | ||
| 640 | + if patterns.len > 0: | ||
| 641 | + body = @[mkList(@[mkSymbol("let"), mkVector(patterns)] & body)] | ||
| 642 | + | ||
| 532 | var names: seq[string] = @[] | 643 | var names: seq[string] = @[] |
| 533 | var inits: seq[Value] = @[] | 644 | var inits: seq[Value] = @[] |
| 534 | - var i = 0 | 645 | + i = 0 |
| 535 | while i < bindings.items.len: | 646 | while i < bindings.items.len: |
| 536 | names.add symName(bindings.items[i]) | 647 | names.add symName(bindings.items[i]) |
| 537 | inits.add bindings.items[i + 1] | 648 | inits.add bindings.items[i + 1] |
| @@ -545,8 +656,9 @@ proc genLoop(bindings: Value, body: seq[Value], dst: string, env: Env, c: Ctx) = | |||
| 545 | collectRecurs(body, recurs) | 656 | collectRecurs(body, recurs) |
| 546 | for r in recurs: | 657 | for r in recurs: |
| 547 | if r.len != names.len: recurs = @[]; break # arity error, reported later | 658 | if r.len != names.len: recurs = @[]; break # arity error, reported later |
| 659 | + let shadowed = shadowedNames(body) | ||
| 548 | var isInt: seq[bool] = @[] | 660 | var isInt: seq[bool] = @[] |
| 549 | - for n in names: isInt.add true | 661 | + for n in names: isInt.add n notin shadowed |
| 550 | var probeIdents: seq[string] = @[] | 662 | var probeIdents: seq[string] = @[] |
| 551 | for n in names: probeIdents.add "probe" | 663 | for n in names: probeIdents.add "probe" |
| 552 | while true: | 664 | while true: |
| @@ -1160,6 +1272,7 @@ proc genInto(f: Value, dst: string, env: Env, c: Ctx) = | |||
| 1160 | # ------------------------------------------------------------- entry point | 1272 | # ------------------------------------------------------------- entry point |
| 1161 | const preamble = """ | 1273 | const preamble = """ |
| 1162 | ## Generated by clonim. Do not edit. | 1274 | ## Generated by clonim. Do not edit. |
| 1275 | +import std/os | ||
| 1163 | import app_runtime | 1276 | import app_runtime |
| 1164 | 1277 | ||
| 1165 | proc cljMain() = | 1278 | proc cljMain() = |
| @@ -1191,6 +1304,20 @@ proc compileForms*(forms: seq[Value]): string = | |||
| 1191 | c.line("var " & t & ": Value = NilV") | 1304 | c.line("var " & t & ": Value = NilV") |
| 1192 | genInto(f, t, env, c) | 1305 | genInto(f, t, env, c) |
| 1193 | c.line("discard " & t) | 1306 | c.line("discard " & t) |
| 1307 | + # A program that defines -main gets it called with the command-line | ||
| 1308 | + # arguments once the top-level forms have run, as on the JVM. | ||
| 1309 | + var mainVar = "" | ||
| 1310 | + for name, _ in c.defCounts: | ||
| 1311 | + if name == "-main" or name.endsWith("/-main"): mainVar = name | ||
| 1312 | + c.line("var clonimArgv: seq[Value] = @[]") | ||
| 1313 | + c.line("for a in commandLineParams(): clonimArgv.add mkStr(a)") | ||
| 1314 | + c.line("setVar(\"clojure.core/*command-line-args*\", " & | ||
| 1315 | + "(if clonimArgv.len == 0: NilV else: mkList(clonimArgv)))") | ||
| 1316 | + if mainVar.len > 0: | ||
| 1317 | + c.line("if hasVar(" & nimStr(mainVar) & "):") | ||
| 1318 | + c.push | ||
| 1319 | + c.line("discard call(getVar(" & nimStr(mainVar) & "), clonimArgv)") | ||
| 1320 | + c.pop | ||
| 1194 | var src = preamble & " initClonimRuntime()\n registerCore()\n registerNamespaceCore()\n" & c.prelude.join("\n") & "\n" & | 1321 | var src = preamble & " initClonimRuntime()\n registerCore()\n registerNamespaceCore()\n" & c.prelude.join("\n") & "\n" & |
| 1195 | c.body.join("\n") & "\n\n" | 1322 | c.body.join("\n") & "\n\n" |
| 1196 | src &= """ | 1323 | src &= """ |
modified
src/core.nim +34 -25 | @@ -401,6 +401,12 @@ proc fusedCount*(base: Value, ops: openArray[FusedOp]): Value = | ||
| 401 | 401 | inc n |
| 402 | 402 | mkInt(n) |
| 403 | 403 | |
| 404 | +proc sOf(v: Value): string = | |
| 405 | + ## A string argument, checked: reaching into a nil or a number here would | |
| 406 | + ## read through a nil object pointer. | |
| 407 | + if v.isNil or v.kind != kStr: err("Expected a string, got: " & prStr(v)) | |
| 408 | + v.s | |
| 409 | + | |
| 404 | 410 | proc parseIntRadix(s: string, radix: int): int64 = |
| 405 | 411 | ## Java's Integer/Long.parseInt: an optional sign, then digits in `radix`. |
| 406 | 412 | var i = 0 |
| @@ -639,6 +645,7 @@ proc registerCore*() = | ||
| 639 | 645 | def "*out*", mkKeyword("stdout") |
| 640 | 646 | def "*err*", mkKeyword("stderr") |
| 641 | 647 | def "*in*", mkKeyword("stdin") |
| 648 | + def "*command-line-args*", NilV | |
| 642 | 649 | def "println", proc (a: openArray[Value]): Value = |
| 643 | 650 | var parts: seq[string] = @[] |
| 644 | 651 | for x in a: parts.add str(x) |
| @@ -674,25 +681,25 @@ proc registerCore*() = | ||
| 674 | 681 | def "keyword", proc (a: openArray[Value]): Value = mkKeyword(str(a[0])) |
| 675 | 682 | def "symbol", proc (a: openArray[Value]): Value = mkSymbol(str(a[0])) |
| 676 | 683 | def "subs", proc (a: openArray[Value]): Value = |
| 677 | - let s = a[0].s | |
| 684 | + let s = sOf(a[0]) | |
| 678 | 685 | let st = int(intOf(a[1])) |
| 679 | 686 | let en = (if a.len > 2: int(intOf(a[2])) else: s.len) |
| 680 | 687 | mkStr(s[st ..< en]) |
| 681 | - def "clojure.string/upper-case", proc (a: openArray[Value]): Value = mkStr(a[0].s.toUpperAscii) | |
| 682 | - def "clojure.string/lower-case", proc (a: openArray[Value]): Value = mkStr(a[0].s.toLowerAscii) | |
| 683 | - def "clojure.string/trim", proc (a: openArray[Value]): Value = mkStr(a[0].s.strip) | |
| 688 | + def "clojure.string/upper-case", proc (a: openArray[Value]): Value = mkStr(sOf(a[0]).toUpperAscii) | |
| 689 | + def "clojure.string/lower-case", proc (a: openArray[Value]): Value = mkStr(sOf(a[0]).toLowerAscii) | |
| 690 | + def "clojure.string/trim", proc (a: openArray[Value]): Value = mkStr(sOf(a[0]).strip) | |
| 684 | 691 | def "re-find", proc (a: openArray[Value]): Value = |
| 685 | - let pattern = re(a[0].s) | |
| 686 | - let start = find(a[1].s, pattern) | |
| 692 | + let pattern = re(sOf(a[0])) | |
| 693 | + let start = find(sOf(a[1]), pattern) | |
| 687 | 694 | if start < 0: NilV |
| 688 | 695 | else: |
| 689 | - let n = matchLen(a[1].s, pattern, start) | |
| 690 | - mkStr(a[1].s[start ..< start + n]) | |
| 696 | + let n = matchLen(sOf(a[1]), pattern, start) | |
| 697 | + mkStr(sOf(a[1])[start ..< start + n]) | |
| 691 | 698 | def "clojure.string/replace", proc (a: openArray[Value]): Value = |
| 692 | - mkStr(a[0].s.replace(re(a[1].s), a[2].s)) | |
| 699 | + mkStr(sOf(a[0]).replace(re(sOf(a[1])), sOf(a[2]))) | |
| 693 | 700 | def "clojure.string/split", proc (a: openArray[Value]): Value = |
| 694 | 701 | var r: seq[Value] = @[] |
| 695 | - for piece in a[0].s.split(a[1].s): r.add mkStr(piece) | |
| 702 | + for piece in sOf(a[0]).split(sOf(a[1])): r.add mkStr(piece) | |
| 696 | 703 | mkVector(r) |
| 697 | 704 | def "clojure.string/join", proc (a: openArray[Value]): Value = |
| 698 | 705 | let sep = (if a.len > 1: str(a[0]) else: "") |
| @@ -702,9 +709,11 @@ proc registerCore*() = | ||
| 702 | 709 | mkStr(parts.join(sep)) |
| 703 | 710 | def "read-line", proc (a: openArray[Value]): Value = |
| 704 | 711 | try: mkStr(stdin.readLine()) except CatchableError: NilV |
| 705 | - def "slurp", proc (a: openArray[Value]): Value = mkStr(readFile(a[0].s)) | |
| 712 | + def "slurp", proc (a: openArray[Value]): Value = | |
| 713 | + (if a[0].kind == kKeyword and a[0].s == "stdin": mkStr(stdin.readAll) | |
| 714 | + else: mkStr(readFile(sOf(a[0])))) | |
| 706 | 715 | def "spit", proc (a: openArray[Value]): Value = |
| 707 | - writeFile(a[0].s, str(a[1])); NilV | |
| 716 | + writeFile(sOf(a[0]), str(a[1])); NilV | |
| 708 | 717 | def "int-array", proc (a: openArray[Value]): Value = |
| 709 | 718 | if a.len == 1 and a[0].kind == kInt: mkList(newSeqWith(int(a[0].i), mkInt(0))) |
| 710 | 719 | elif a.len == 1: mkList(toSeq(a[0])) else: mkList(a) |
| @@ -726,7 +735,7 @@ proc registerCore*() = | ||
| 726 | 735 | NilV |
| 727 | 736 | def ".getBytes", proc (a: openArray[Value]): Value = |
| 728 | 737 | var xs: seq[Value] = @[] |
| 729 | - for ch in a[0].s: xs.add mkInt(ord(ch)) | |
| 738 | + for ch in sOf(a[0]): xs.add mkInt(ord(ch)) | |
| 730 | 739 | mkList(xs) |
| 731 | 740 | def "StringBuilder.", proc (a: openArray[Value]): Value = |
| 732 | 741 | ## A mutable string: the one place a string value is written in place. |
| @@ -736,11 +745,11 @@ proc registerCore*() = | ||
| 736 | 745 | a[0].obj.s.add(str(a[1])) |
| 737 | 746 | a[0] |
| 738 | 747 | def ".toString", proc (a: openArray[Value]): Value = mkStr(str(a[0])) |
| 739 | - def ".length", proc (a: openArray[Value]): Value = mkInt(int64(a[0].s.len)) | |
| 748 | + def ".length", proc (a: openArray[Value]): Value = mkInt(int64(sOf(a[0]).len)) | |
| 740 | 749 | def "*output-stream*", proc (a: openArray[Value]): Value = |
| 741 | 750 | ## A write handle, driven by keyword messages so that it stays an |
| 742 | 751 | ## ordinary value: (o :write bytes) and (o :close). |
| 743 | - let path = a[0].s | |
| 752 | + let path = sOf(a[0]) | |
| 744 | 753 | var f: File |
| 745 | 754 | if not f.open(path, fmWrite): err("Cannot open for writing: " & path) |
| 746 | 755 | var closed = false |
| @@ -763,7 +772,7 @@ proc registerCore*() = | ||
| 763 | 772 | (if a[0].kind == kFn: call(a[0], [mkKeyword("close")]) else: NilV) |
| 764 | 773 | def "*delete-file*", proc (a: openArray[Value]): Value = |
| 765 | 774 | try: |
| 766 | - removeFile(a[0].s) | |
| 775 | + removeFile(sOf(a[0])) | |
| 767 | 776 | TrueV |
| 768 | 777 | except OSError: |
| 769 | 778 | if a.len > 1 and truthy(a[1]): FalseV |
| @@ -771,8 +780,8 @@ proc registerCore*() = | ||
| 771 | 780 | def "*http-fetch*", proc (a: openArray[Value]): Value = |
| 772 | 781 | ## Native replacement for the narrow Jolt HTTP API used by Freeqsay. |
| 773 | 782 | try: |
| 774 | - let response = newHttpClient().get(a[0].s) | |
| 775 | - writeFile(a[1].s, response.body) | |
| 783 | + let response = newHttpClient().get(sOf(a[0])) | |
| 784 | + writeFile(sOf(a[1]), response.body) | |
| 776 | 785 | mkMap(@[(mkKeyword("outcome"), mkKeyword("ok")), |
| 777 | 786 | (mkKeyword("status"), mkInt(response.code.int)), |
| 778 | 787 | (mkKeyword("error"), NilV)]) |
| @@ -1101,29 +1110,29 @@ proc registerCore*() = | ||
| 1101 | 1110 | def "class", proc (a: openArray[Value]): Value = mkKeyword($a[0].kind) |
| 1102 | 1111 | def "instance?", proc (a: openArray[Value]): Value = |
| 1103 | 1112 | mkBool(a[0].kind == kKeyword and a[0].s == $a[1].kind) |
| 1104 | - def "format", proc (a: openArray[Value]): Value = mkStr(javaFormat(a[0].s, a[1 .. ^1])) | |
| 1113 | + def "format", proc (a: openArray[Value]): Value = mkStr(javaFormat(sOf(a[0]), a[1 .. ^1])) | |
| 1105 | 1114 | def "System/exit", proc (a: openArray[Value]): Value = |
| 1106 | 1115 | quit(if a.len > 0: int(intOf(a[0])) else: 0) |
| 1107 | 1116 | def "System/currentTimeMillis", proc (a: openArray[Value]): Value = |
| 1108 | 1117 | mkInt(int64(epochTime() * 1000)) |
| 1109 | 1118 | def "System/getProperty", proc (a: openArray[Value]): Value = |
| 1110 | 1119 | ## Only the properties a hosted program can reasonably expect here. |
| 1111 | - case a[0].s | |
| 1120 | + case sOf(a[0]) | |
| 1112 | 1121 | of "java.io.tmpdir": mkStr(getTempDir().strip(leading = false, chars = {'/'})) |
| 1113 | 1122 | of "user.dir": mkStr(getCurrentDir()) |
| 1114 | 1123 | of "user.name": mkStr(getEnv("USER")) |
| 1115 | 1124 | of "line.separator": mkStr("\n") |
| 1116 | 1125 | else: (if a.len > 1: a[1] else: NilV) |
| 1117 | 1126 | def "clojure.string/blank?", proc (a: openArray[Value]): Value = |
| 1118 | - mkBool(a[0].kind == kNil or a[0].s.strip.len == 0) | |
| 1127 | + mkBool(a[0].kind == kNil or sOf(a[0]).strip.len == 0) | |
| 1119 | 1128 | def "clojure.string/starts-with?", proc (a: openArray[Value]): Value = |
| 1120 | - mkBool(a[0].s.startsWith(a[1].s)) | |
| 1129 | + mkBool(sOf(a[0]).startsWith(sOf(a[1]))) | |
| 1121 | 1130 | def "clojure.string/ends-with?", proc (a: openArray[Value]): Value = |
| 1122 | - mkBool(a[0].s.endsWith(a[1].s)) | |
| 1131 | + mkBool(sOf(a[0]).endsWith(sOf(a[1]))) | |
| 1123 | 1132 | def "clojure.string/includes?", proc (a: openArray[Value]): Value = |
| 1124 | - mkBool(a[1].s in a[0].s) | |
| 1133 | + mkBool(sOf(a[1]) in sOf(a[0])) | |
| 1125 | 1134 | def "clojure.string/index-of", proc (a: openArray[Value]): Value = |
| 1126 | - let i = a[0].s.find(a[1].s) | |
| 1135 | + let i = sOf(a[0]).find(sOf(a[1])) | |
| 1127 | 1136 | (if i < 0: NilV else: mkInt(int64(i))) |
| 1128 | 1137 | def "throw", proc (a: openArray[Value]): Value = err(str(a[0])) |
| 1129 | 1138 | def "ex-info", proc (a: openArray[Value]): Value = mkStr(str(a[0])) |
| @@ -401,6 +401,12 @@ proc fusedCount*(base: Value, ops: openArray[FusedOp]): Value = | |||
| 401 | inc n | 401 | inc n |
| 402 | mkInt(n) | 402 | mkInt(n) |
| 403 | 403 | ||
| 404 | +proc sOf(v: Value): string = | ||
| 405 | + ## A string argument, checked: reaching into a nil or a number here would | ||
| 406 | + ## read through a nil object pointer. | ||
| 407 | + if v.isNil or v.kind != kStr: err("Expected a string, got: " & prStr(v)) | ||
| 408 | + v.s | ||
| 409 | + | ||
| 404 | proc parseIntRadix(s: string, radix: int): int64 = | 410 | proc parseIntRadix(s: string, radix: int): int64 = |
| 405 | ## Java's Integer/Long.parseInt: an optional sign, then digits in `radix`. | 411 | ## Java's Integer/Long.parseInt: an optional sign, then digits in `radix`. |
| 406 | var i = 0 | 412 | var i = 0 |
| @@ -639,6 +645,7 @@ proc registerCore*() = | |||
| 639 | def "*out*", mkKeyword("stdout") | 645 | def "*out*", mkKeyword("stdout") |
| 640 | def "*err*", mkKeyword("stderr") | 646 | def "*err*", mkKeyword("stderr") |
| 641 | def "*in*", mkKeyword("stdin") | 647 | def "*in*", mkKeyword("stdin") |
| 648 | + def "*command-line-args*", NilV | ||
| 642 | def "println", proc (a: openArray[Value]): Value = | 649 | def "println", proc (a: openArray[Value]): Value = |
| 643 | var parts: seq[string] = @[] | 650 | var parts: seq[string] = @[] |
| 644 | for x in a: parts.add str(x) | 651 | for x in a: parts.add str(x) |
| @@ -674,25 +681,25 @@ proc registerCore*() = | |||
| 674 | def "keyword", proc (a: openArray[Value]): Value = mkKeyword(str(a[0])) | 681 | def "keyword", proc (a: openArray[Value]): Value = mkKeyword(str(a[0])) |
| 675 | def "symbol", proc (a: openArray[Value]): Value = mkSymbol(str(a[0])) | 682 | def "symbol", proc (a: openArray[Value]): Value = mkSymbol(str(a[0])) |
| 676 | def "subs", proc (a: openArray[Value]): Value = | 683 | def "subs", proc (a: openArray[Value]): Value = |
| 677 | - let s = a[0].s | 684 | + let s = sOf(a[0]) |
| 678 | let st = int(intOf(a[1])) | 685 | let st = int(intOf(a[1])) |
| 679 | let en = (if a.len > 2: int(intOf(a[2])) else: s.len) | 686 | let en = (if a.len > 2: int(intOf(a[2])) else: s.len) |
| 680 | mkStr(s[st ..< en]) | 687 | mkStr(s[st ..< en]) |
| 681 | - def "clojure.string/upper-case", proc (a: openArray[Value]): Value = mkStr(a[0].s.toUpperAscii) | 688 | + def "clojure.string/upper-case", proc (a: openArray[Value]): Value = mkStr(sOf(a[0]).toUpperAscii) |
| 682 | - def "clojure.string/lower-case", proc (a: openArray[Value]): Value = mkStr(a[0].s.toLowerAscii) | 689 | + def "clojure.string/lower-case", proc (a: openArray[Value]): Value = mkStr(sOf(a[0]).toLowerAscii) |
| 683 | - def "clojure.string/trim", proc (a: openArray[Value]): Value = mkStr(a[0].s.strip) | 690 | + def "clojure.string/trim", proc (a: openArray[Value]): Value = mkStr(sOf(a[0]).strip) |
| 684 | def "re-find", proc (a: openArray[Value]): Value = | 691 | def "re-find", proc (a: openArray[Value]): Value = |
| 685 | - let pattern = re(a[0].s) | 692 | + let pattern = re(sOf(a[0])) |
| 686 | - let start = find(a[1].s, pattern) | 693 | + let start = find(sOf(a[1]), pattern) |
| 687 | if start < 0: NilV | 694 | if start < 0: NilV |
| 688 | else: | 695 | else: |
| 689 | - let n = matchLen(a[1].s, pattern, start) | 696 | + let n = matchLen(sOf(a[1]), pattern, start) |
| 690 | - mkStr(a[1].s[start ..< start + n]) | 697 | + mkStr(sOf(a[1])[start ..< start + n]) |
| 691 | def "clojure.string/replace", proc (a: openArray[Value]): Value = | 698 | def "clojure.string/replace", proc (a: openArray[Value]): Value = |
| 692 | - mkStr(a[0].s.replace(re(a[1].s), a[2].s)) | 699 | + mkStr(sOf(a[0]).replace(re(sOf(a[1])), sOf(a[2]))) |
| 693 | def "clojure.string/split", proc (a: openArray[Value]): Value = | 700 | def "clojure.string/split", proc (a: openArray[Value]): Value = |
| 694 | var r: seq[Value] = @[] | 701 | var r: seq[Value] = @[] |
| 695 | - for piece in a[0].s.split(a[1].s): r.add mkStr(piece) | 702 | + for piece in sOf(a[0]).split(sOf(a[1])): r.add mkStr(piece) |
| 696 | mkVector(r) | 703 | mkVector(r) |
| 697 | def "clojure.string/join", proc (a: openArray[Value]): Value = | 704 | def "clojure.string/join", proc (a: openArray[Value]): Value = |
| 698 | let sep = (if a.len > 1: str(a[0]) else: "") | 705 | let sep = (if a.len > 1: str(a[0]) else: "") |
| @@ -702,9 +709,11 @@ proc registerCore*() = | |||
| 702 | mkStr(parts.join(sep)) | 709 | mkStr(parts.join(sep)) |
| 703 | def "read-line", proc (a: openArray[Value]): Value = | 710 | def "read-line", proc (a: openArray[Value]): Value = |
| 704 | try: mkStr(stdin.readLine()) except CatchableError: NilV | 711 | try: mkStr(stdin.readLine()) except CatchableError: NilV |
| 705 | - def "slurp", proc (a: openArray[Value]): Value = mkStr(readFile(a[0].s)) | 712 | + def "slurp", proc (a: openArray[Value]): Value = |
| 713 | + (if a[0].kind == kKeyword and a[0].s == "stdin": mkStr(stdin.readAll) | ||
| 714 | + else: mkStr(readFile(sOf(a[0])))) | ||
| 706 | def "spit", proc (a: openArray[Value]): Value = | 715 | def "spit", proc (a: openArray[Value]): Value = |
| 707 | - writeFile(a[0].s, str(a[1])); NilV | 716 | + writeFile(sOf(a[0]), str(a[1])); NilV |
| 708 | def "int-array", proc (a: openArray[Value]): Value = | 717 | def "int-array", proc (a: openArray[Value]): Value = |
| 709 | if a.len == 1 and a[0].kind == kInt: mkList(newSeqWith(int(a[0].i), mkInt(0))) | 718 | if a.len == 1 and a[0].kind == kInt: mkList(newSeqWith(int(a[0].i), mkInt(0))) |
| 710 | elif a.len == 1: mkList(toSeq(a[0])) else: mkList(a) | 719 | elif a.len == 1: mkList(toSeq(a[0])) else: mkList(a) |
| @@ -726,7 +735,7 @@ proc registerCore*() = | |||
| 726 | NilV | 735 | NilV |
| 727 | def ".getBytes", proc (a: openArray[Value]): Value = | 736 | def ".getBytes", proc (a: openArray[Value]): Value = |
| 728 | var xs: seq[Value] = @[] | 737 | var xs: seq[Value] = @[] |
| 729 | - for ch in a[0].s: xs.add mkInt(ord(ch)) | 738 | + for ch in sOf(a[0]): xs.add mkInt(ord(ch)) |
| 730 | mkList(xs) | 739 | mkList(xs) |
| 731 | def "StringBuilder.", proc (a: openArray[Value]): Value = | 740 | def "StringBuilder.", proc (a: openArray[Value]): Value = |
| 732 | ## A mutable string: the one place a string value is written in place. | 741 | ## A mutable string: the one place a string value is written in place. |
| @@ -736,11 +745,11 @@ proc registerCore*() = | |||
| 736 | a[0].obj.s.add(str(a[1])) | 745 | a[0].obj.s.add(str(a[1])) |
| 737 | a[0] | 746 | a[0] |
| 738 | def ".toString", proc (a: openArray[Value]): Value = mkStr(str(a[0])) | 747 | def ".toString", proc (a: openArray[Value]): Value = mkStr(str(a[0])) |
| 739 | - def ".length", proc (a: openArray[Value]): Value = mkInt(int64(a[0].s.len)) | 748 | + def ".length", proc (a: openArray[Value]): Value = mkInt(int64(sOf(a[0]).len)) |
| 740 | def "*output-stream*", proc (a: openArray[Value]): Value = | 749 | def "*output-stream*", proc (a: openArray[Value]): Value = |
| 741 | ## A write handle, driven by keyword messages so that it stays an | 750 | ## A write handle, driven by keyword messages so that it stays an |
| 742 | ## ordinary value: (o :write bytes) and (o :close). | 751 | ## ordinary value: (o :write bytes) and (o :close). |
| 743 | - let path = a[0].s | 752 | + let path = sOf(a[0]) |
| 744 | var f: File | 753 | var f: File |
| 745 | if not f.open(path, fmWrite): err("Cannot open for writing: " & path) | 754 | if not f.open(path, fmWrite): err("Cannot open for writing: " & path) |
| 746 | var closed = false | 755 | var closed = false |
| @@ -763,7 +772,7 @@ proc registerCore*() = | |||
| 763 | (if a[0].kind == kFn: call(a[0], [mkKeyword("close")]) else: NilV) | 772 | (if a[0].kind == kFn: call(a[0], [mkKeyword("close")]) else: NilV) |
| 764 | def "*delete-file*", proc (a: openArray[Value]): Value = | 773 | def "*delete-file*", proc (a: openArray[Value]): Value = |
| 765 | try: | 774 | try: |
| 766 | - removeFile(a[0].s) | 775 | + removeFile(sOf(a[0])) |
| 767 | TrueV | 776 | TrueV |
| 768 | except OSError: | 777 | except OSError: |
| 769 | if a.len > 1 and truthy(a[1]): FalseV | 778 | if a.len > 1 and truthy(a[1]): FalseV |
| @@ -771,8 +780,8 @@ proc registerCore*() = | |||
| 771 | def "*http-fetch*", proc (a: openArray[Value]): Value = | 780 | def "*http-fetch*", proc (a: openArray[Value]): Value = |
| 772 | ## Native replacement for the narrow Jolt HTTP API used by Freeqsay. | 781 | ## Native replacement for the narrow Jolt HTTP API used by Freeqsay. |
| 773 | try: | 782 | try: |
| 774 | - let response = newHttpClient().get(a[0].s) | 783 | + let response = newHttpClient().get(sOf(a[0])) |
| 775 | - writeFile(a[1].s, response.body) | 784 | + writeFile(sOf(a[1]), response.body) |
| 776 | mkMap(@[(mkKeyword("outcome"), mkKeyword("ok")), | 785 | mkMap(@[(mkKeyword("outcome"), mkKeyword("ok")), |
| 777 | (mkKeyword("status"), mkInt(response.code.int)), | 786 | (mkKeyword("status"), mkInt(response.code.int)), |
| 778 | (mkKeyword("error"), NilV)]) | 787 | (mkKeyword("error"), NilV)]) |
| @@ -1101,29 +1110,29 @@ proc registerCore*() = | |||
| 1101 | def "class", proc (a: openArray[Value]): Value = mkKeyword($a[0].kind) | 1110 | def "class", proc (a: openArray[Value]): Value = mkKeyword($a[0].kind) |
| 1102 | def "instance?", proc (a: openArray[Value]): Value = | 1111 | def "instance?", proc (a: openArray[Value]): Value = |
| 1103 | mkBool(a[0].kind == kKeyword and a[0].s == $a[1].kind) | 1112 | mkBool(a[0].kind == kKeyword and a[0].s == $a[1].kind) |
| 1104 | - def "format", proc (a: openArray[Value]): Value = mkStr(javaFormat(a[0].s, a[1 .. ^1])) | 1113 | + def "format", proc (a: openArray[Value]): Value = mkStr(javaFormat(sOf(a[0]), a[1 .. ^1])) |
| 1105 | def "System/exit", proc (a: openArray[Value]): Value = | 1114 | def "System/exit", proc (a: openArray[Value]): Value = |
| 1106 | quit(if a.len > 0: int(intOf(a[0])) else: 0) | 1115 | quit(if a.len > 0: int(intOf(a[0])) else: 0) |
| 1107 | def "System/currentTimeMillis", proc (a: openArray[Value]): Value = | 1116 | def "System/currentTimeMillis", proc (a: openArray[Value]): Value = |
| 1108 | mkInt(int64(epochTime() * 1000)) | 1117 | mkInt(int64(epochTime() * 1000)) |
| 1109 | def "System/getProperty", proc (a: openArray[Value]): Value = | 1118 | def "System/getProperty", proc (a: openArray[Value]): Value = |
| 1110 | ## Only the properties a hosted program can reasonably expect here. | 1119 | ## Only the properties a hosted program can reasonably expect here. |
| 1111 | - case a[0].s | 1120 | + case sOf(a[0]) |
| 1112 | of "java.io.tmpdir": mkStr(getTempDir().strip(leading = false, chars = {'/'})) | 1121 | of "java.io.tmpdir": mkStr(getTempDir().strip(leading = false, chars = {'/'})) |
| 1113 | of "user.dir": mkStr(getCurrentDir()) | 1122 | of "user.dir": mkStr(getCurrentDir()) |
| 1114 | of "user.name": mkStr(getEnv("USER")) | 1123 | of "user.name": mkStr(getEnv("USER")) |
| 1115 | of "line.separator": mkStr("\n") | 1124 | of "line.separator": mkStr("\n") |
| 1116 | else: (if a.len > 1: a[1] else: NilV) | 1125 | else: (if a.len > 1: a[1] else: NilV) |
| 1117 | def "clojure.string/blank?", proc (a: openArray[Value]): Value = | 1126 | def "clojure.string/blank?", proc (a: openArray[Value]): Value = |
| 1118 | - mkBool(a[0].kind == kNil or a[0].s.strip.len == 0) | 1127 | + mkBool(a[0].kind == kNil or sOf(a[0]).strip.len == 0) |
| 1119 | def "clojure.string/starts-with?", proc (a: openArray[Value]): Value = | 1128 | def "clojure.string/starts-with?", proc (a: openArray[Value]): Value = |
| 1120 | - mkBool(a[0].s.startsWith(a[1].s)) | 1129 | + mkBool(sOf(a[0]).startsWith(sOf(a[1]))) |
| 1121 | def "clojure.string/ends-with?", proc (a: openArray[Value]): Value = | 1130 | def "clojure.string/ends-with?", proc (a: openArray[Value]): Value = |
| 1122 | - mkBool(a[0].s.endsWith(a[1].s)) | 1131 | + mkBool(sOf(a[0]).endsWith(sOf(a[1]))) |
| 1123 | def "clojure.string/includes?", proc (a: openArray[Value]): Value = | 1132 | def "clojure.string/includes?", proc (a: openArray[Value]): Value = |
| 1124 | - mkBool(a[1].s in a[0].s) | 1133 | + mkBool(sOf(a[1]) in sOf(a[0])) |
| 1125 | def "clojure.string/index-of", proc (a: openArray[Value]): Value = | 1134 | def "clojure.string/index-of", proc (a: openArray[Value]): Value = |
| 1126 | - let i = a[0].s.find(a[1].s) | 1135 | + let i = sOf(a[0]).find(sOf(a[1])) |
| 1127 | (if i < 0: NilV else: mkInt(int64(i))) | 1136 | (if i < 0: NilV else: mkInt(int64(i))) |
| 1128 | def "throw", proc (a: openArray[Value]): Value = err(str(a[0])) | 1137 | def "throw", proc (a: openArray[Value]): Value = err(str(a[0])) |
| 1129 | def "ex-info", proc (a: openArray[Value]): Value = mkStr(str(a[0])) | 1138 | def "ex-info", proc (a: openArray[Value]): Value = mkStr(str(a[0])) |
modified
src/reader.nim +32 -3 | @@ -37,8 +37,18 @@ proc skipWs(r: var Reader) = | ||
| 37 | 37 | |
| 38 | 38 | proc readForm(r: var Reader): Value |
| 39 | 39 | |
| 40 | +proc anonArgIndex(name: string): int = | |
| 41 | + ## % and %1 are the first argument, %2 the second, and so on. -1 for | |
| 42 | + ## anything that is not an anonymous-argument symbol. | |
| 43 | + if name.len == 0 or name[0] != '%': return -1 | |
| 44 | + if name.len == 1: return 1 | |
| 45 | + try: parseInt(name[1 .. ^1]) | |
| 46 | + except ValueError: -1 | |
| 47 | + | |
| 40 | 48 | proc replaceAnonArg(v: Value, arg: string): Value = |
| 41 | - if v.kind == kSymbol and v.s == "%": return mkSymbol(arg) | |
| 49 | + if v.kind == kSymbol: | |
| 50 | + let n = anonArgIndex(v.s) | |
| 51 | + if n > 0: return mkSymbol(arg & "_" & $n) | |
| 42 | 52 | case v.kind |
| 43 | 53 | of kList: mkList(v.items.mapIt(replaceAnonArg(it, arg))) |
| 44 | 54 | of kVector: mkVector(v.items.mapIt(replaceAnonArg(it, arg))) |
| @@ -49,6 +59,20 @@ proc replaceAnonArg(v: Value, arg: string): Value = | ||
| 49 | 59 | mkMap(pairs) |
| 50 | 60 | else: v |
| 51 | 61 | |
| 62 | +proc countAnonArgs(v: Value, arity: var int) = | |
| 63 | + if v.isNil: return | |
| 64 | + if v.kind == kSymbol: | |
| 65 | + let n = anonArgIndex(v.s) | |
| 66 | + if n > arity: arity = n | |
| 67 | + return | |
| 68 | + case v.kind | |
| 69 | + of kList, kVector, kSet: | |
| 70 | + for x in v.items: countAnonArgs(x, arity) | |
| 71 | + of kMap: | |
| 72 | + for (k, val) in v.pairs: | |
| 73 | + countAnonArgs(k, arity); countAnonArgs(val, arity) | |
| 74 | + else: discard | |
| 75 | + | |
| 52 | 76 | proc readDelimited(r: var Reader, closing: char): seq[Value] = |
| 53 | 77 | result = @[] |
| 54 | 78 | while true: |
| @@ -196,8 +220,13 @@ proc readForm(r: var Reader): Value = | ||
| 196 | 220 | inc r.anonId |
| 197 | 221 | let arg = "anon_arg_" & $r.anonId |
| 198 | 222 | let body = r.readDelimited(')') |
| 199 | - return mkList(@[mkSymbol("fn"), mkVector(@[mkSymbol(arg)])] & | |
| 200 | - body.mapIt(replaceAnonArg(it, arg))) | |
| 223 | + # The contents are one call, not a body: #(f x) is (fn [a] (f x)). | |
| 224 | + let form = replaceAnonArg(mkList(body), arg) | |
| 225 | + var arity = 0 | |
| 226 | + countAnonArgs(mkList(body), arity) | |
| 227 | + var params: seq[Value] = @[] | |
| 228 | + for i in 1 .. arity: params.add mkSymbol(arg & "_" & $i) | |
| 229 | + return mkList(@[mkSymbol("fn"), mkVector(params), form]) | |
| 201 | 230 | r.readerErr("Unsupported dispatch: #" & r.peek2 |
| 202 | 231 | ) |
| 203 | 232 | else: |
| @@ -37,8 +37,18 @@ proc skipWs(r: var Reader) = | |||
| 37 | 37 | ||
| 38 | proc readForm(r: var Reader): Value | 38 | proc readForm(r: var Reader): Value |
| 39 | 39 | ||
| 40 | +proc anonArgIndex(name: string): int = | ||
| 41 | + ## % and %1 are the first argument, %2 the second, and so on. -1 for | ||
| 42 | + ## anything that is not an anonymous-argument symbol. | ||
| 43 | + if name.len == 0 or name[0] != '%': return -1 | ||
| 44 | + if name.len == 1: return 1 | ||
| 45 | + try: parseInt(name[1 .. ^1]) | ||
| 46 | + except ValueError: -1 | ||
| 47 | + | ||
| 40 | proc replaceAnonArg(v: Value, arg: string): Value = | 48 | proc replaceAnonArg(v: Value, arg: string): Value = |
| 41 | - if v.kind == kSymbol and v.s == "%": return mkSymbol(arg) | 49 | + if v.kind == kSymbol: |
| 50 | + let n = anonArgIndex(v.s) | ||
| 51 | + if n > 0: return mkSymbol(arg & "_" & $n) | ||
| 42 | case v.kind | 52 | case v.kind |
| 43 | of kList: mkList(v.items.mapIt(replaceAnonArg(it, arg))) | 53 | of kList: mkList(v.items.mapIt(replaceAnonArg(it, arg))) |
| 44 | of kVector: mkVector(v.items.mapIt(replaceAnonArg(it, arg))) | 54 | of kVector: mkVector(v.items.mapIt(replaceAnonArg(it, arg))) |
| @@ -49,6 +59,20 @@ proc replaceAnonArg(v: Value, arg: string): Value = | |||
| 49 | mkMap(pairs) | 59 | mkMap(pairs) |
| 50 | else: v | 60 | else: v |
| 51 | 61 | ||
| 62 | +proc countAnonArgs(v: Value, arity: var int) = | ||
| 63 | + if v.isNil: return | ||
| 64 | + if v.kind == kSymbol: | ||
| 65 | + let n = anonArgIndex(v.s) | ||
| 66 | + if n > arity: arity = n | ||
| 67 | + return | ||
| 68 | + case v.kind | ||
| 69 | + of kList, kVector, kSet: | ||
| 70 | + for x in v.items: countAnonArgs(x, arity) | ||
| 71 | + of kMap: | ||
| 72 | + for (k, val) in v.pairs: | ||
| 73 | + countAnonArgs(k, arity); countAnonArgs(val, arity) | ||
| 74 | + else: discard | ||
| 75 | + | ||
| 52 | proc readDelimited(r: var Reader, closing: char): seq[Value] = | 76 | proc readDelimited(r: var Reader, closing: char): seq[Value] = |
| 53 | result = @[] | 77 | result = @[] |
| 54 | while true: | 78 | while true: |
| @@ -196,8 +220,13 @@ proc readForm(r: var Reader): Value = | |||
| 196 | inc r.anonId | 220 | inc r.anonId |
| 197 | let arg = "anon_arg_" & $r.anonId | 221 | let arg = "anon_arg_" & $r.anonId |
| 198 | let body = r.readDelimited(')') | 222 | let body = r.readDelimited(')') |
| 199 | - return mkList(@[mkSymbol("fn"), mkVector(@[mkSymbol(arg)])] & | 223 | + # The contents are one call, not a body: #(f x) is (fn [a] (f x)). |
| 200 | - body.mapIt(replaceAnonArg(it, arg))) | 224 | + let form = replaceAnonArg(mkList(body), arg) |
| 225 | + var arity = 0 | ||
| 226 | + countAnonArgs(mkList(body), arity) | ||
| 227 | + var params: seq[Value] = @[] | ||
| 228 | + for i in 1 .. arity: params.add mkSymbol(arg & "_" & $i) | ||
| 229 | + return mkList(@[mkSymbol("fn"), mkVector(params), form]) | ||
| 201 | r.readerErr("Unsupported dispatch: #" & r.peek2 | 230 | r.readerErr("Unsupported dispatch: #" & r.peek2 |
| 202 | ) | 231 | ) |
| 203 | else: | 232 | else: |
modified
src/runtime.nim +6 -0 | @@ -440,6 +440,12 @@ proc seqDrop*(v: Value, n: int): Value = | ||
| 440 | 440 | result = seqRest(result) |
| 441 | 441 | dec k |
| 442 | 442 | |
| 443 | +proc seqDropOrNil*(v: Value, n: int): Value = | |
| 444 | + ## What `& rest` binds: the remainder, or nil once it is exhausted, as on | |
| 445 | + ## the JVM, where an empty rest is nil rather than an empty seq. | |
| 446 | + result = seqDrop(v, n) | |
| 447 | + if seqIsEmpty(result): result = NilV | |
| 448 | + | |
| 443 | 449 | proc hashValue*(v: Value): uint32 = |
| 444 | 450 | if v.isNil: return 0 |
| 445 | 451 | case v.kind |
| @@ -440,6 +440,12 @@ proc seqDrop*(v: Value, n: int): Value = | |||
| 440 | result = seqRest(result) | 440 | result = seqRest(result) |
| 441 | dec k | 441 | dec k |
| 442 | 442 | ||
| 443 | +proc seqDropOrNil*(v: Value, n: int): Value = | ||
| 444 | + ## What `& rest` binds: the remainder, or nil once it is exhausted, as on | ||
| 445 | + ## the JVM, where an empty rest is nil rather than an empty seq. | ||
| 446 | + result = seqDrop(v, n) | ||
| 447 | + if seqIsEmpty(result): result = NilV | ||
| 448 | + | ||
| 443 | proc hashValue*(v: Value): uint32 = | 449 | proc hashValue*(v: Value): uint32 = |
| 444 | if v.isNil: return 0 | 450 | if v.isNil: return 0 |
| 445 | case v.kind | 451 | case v.kind |
modified
src/runtime_lib.nim +2 -0 | @@ -15,10 +15,12 @@ proc clonim_set_var(name: string, v: Value): Value = setVar(name, v) | ||
| 15 | 15 | proc clonim_cell_get(c: VarCell): Value = cellGet(c) |
| 16 | 16 | proc clonim_cell_is(c: VarCell, v: Value): bool = cellIs(c, v) |
| 17 | 17 | proc clonim_get_var(name: string): Value = getVar(name) |
| 18 | +proc clonim_has_var(name: string): bool = hasVar(name) | |
| 18 | 19 | proc clonim_call(f: Value, args: openArray[Value]): Value = call(f, args) |
| 19 | 20 | proc clonim_arg_at(args: openArray[Value], i: int): Value = argAt(args, i) |
| 20 | 21 | proc clonim_rest_args(args: openArray[Value], i: int): Value = restArgs(args, i) |
| 21 | 22 | proc clonim_seq_drop(v: Value, n: int): Value = seqDrop(v, n) |
| 23 | +proc clonim_seq_drop_or_nil(v: Value, n: int): Value = seqDropOrNil(v, n) | |
| 22 | 24 | proc clonim_to_seq(v: Value): seq[Value] = toSeq(v) |
| 23 | 25 | proc clonim_truthy(v: Value): bool = truthy(v) |
| 24 | 26 | |
| @@ -15,10 +15,12 @@ proc clonim_set_var(name: string, v: Value): Value = setVar(name, v) | |||
| 15 | proc clonim_cell_get(c: VarCell): Value = cellGet(c) | 15 | proc clonim_cell_get(c: VarCell): Value = cellGet(c) |
| 16 | proc clonim_cell_is(c: VarCell, v: Value): bool = cellIs(c, v) | 16 | proc clonim_cell_is(c: VarCell, v: Value): bool = cellIs(c, v) |
| 17 | proc clonim_get_var(name: string): Value = getVar(name) | 17 | proc clonim_get_var(name: string): Value = getVar(name) |
| 18 | +proc clonim_has_var(name: string): bool = hasVar(name) | ||
| 18 | proc clonim_call(f: Value, args: openArray[Value]): Value = call(f, args) | 19 | proc clonim_call(f: Value, args: openArray[Value]): Value = call(f, args) |
| 19 | proc clonim_arg_at(args: openArray[Value], i: int): Value = argAt(args, i) | 20 | proc clonim_arg_at(args: openArray[Value], i: int): Value = argAt(args, i) |
| 20 | proc clonim_rest_args(args: openArray[Value], i: int): Value = restArgs(args, i) | 21 | proc clonim_rest_args(args: openArray[Value], i: int): Value = restArgs(args, i) |
| 21 | proc clonim_seq_drop(v: Value, n: int): Value = seqDrop(v, n) | 22 | proc clonim_seq_drop(v: Value, n: int): Value = seqDrop(v, n) |
| 23 | +proc clonim_seq_drop_or_nil(v: Value, n: int): Value = seqDropOrNil(v, n) | ||
| 22 | proc clonim_to_seq(v: Value): seq[Value] = toSeq(v) | 24 | proc clonim_to_seq(v: Value): seq[Value] = toSeq(v) |
| 23 | proc clonim_truthy(v: Value): bool = truthy(v) | 25 | proc clonim_truthy(v: Value): bool = truthy(v) |
| 24 | 26 | ||
added
tests/binding_forms.expected +15 -0 | new file mode 100644 | ||
| @@ -0,0 +1,15 @@ | ||
| 1 | +[1 2 3] | |
| 2 | +(:a1 :b2) | |
| 3 | +7 12 | |
| 4 | +[1 2] | |
| 5 | +1 2 (3 4) [1 2 3 4] | |
| 6 | +nil true | |
| 7 | +1 9 {:x 1} | |
| 8 | +1 2 3 | |
| 9 | +nil (1) | |
| 10 | +[1 2 3] | |
| 11 | +(e3 b0) | |
| 12 | +(2 4 6) | |
| 13 | +(11 22) | |
| 14 | +[7 7] | |
| 15 | +3 | |
| new file mode 100644 | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | +[1 2 3] | ||
| 2 | +(:a1 :b2) | ||
| 3 | +7 12 | ||
| 4 | +[1 2] | ||
| 5 | +1 2 (3 4) [1 2 3 4] | ||
| 6 | +nil true | ||
| 7 | +1 9 {:x 1} | ||
| 8 | +1 2 3 | ||
| 9 | +nil (1) | ||
| 10 | +[1 2 3] | ||
| 11 | +(e3 b0) | ||
| 12 | +(2 4 6) | ||
| 13 | +(11 22) | ||
| 14 | +[7 7] | ||
| 15 | +3 | ||
added
tests/cli_args.expected +2 -0 | new file mode 100644 | ||
| @@ -0,0 +1,2 @@ | ||
| 1 | +args: 0 [] | |
| 2 | +same: true | |
| new file mode 100644 | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | +args: 0 [] | ||
| 2 | +same: true | ||