diff --git a/.dirge/memory/MEMORY.md b/.dirge/memory/MEMORY.md index 88acbda..bedec3e 100644 --- a/.dirge/memory/MEMORY.md +++ b/.dirge/memory/MEMORY.md @@ -1,7 +1,9 @@ -Build: `jpm test` runs all tests; `janet test/.janet` runs single. Source in `src/jolt/`, tests in `test/`. Entry: `src/jolt/main.janet`. Key files: compiler.janet (848L, 2-phase analyze→emit), evaluator.janet (interpreter with 21 special forms), core.janet (~130 Clojure core fns), types.janet (Var/Namespace/Context), reader.janet (parser), phm.janet (PersistentHashMap + LazySeq + PersistentHashSet), api.janet (public API + compile? dispatch), loader.janet (file loading). -§ -Architecture: Two eval modes — compile (`:compile? true`) uses analyze-form→emit-expr→Janet eval; interpreter mode uses tree-walking evaluator.janet. Stateful forms (defmacro, ns, deftype, defmulti, defmethod, syntax-quote, set!, var, ., new) always fall back to interpreter. Macros expand at analyze time. Core fns resolved to actual Janet function values via `core-fn-values` table for direct eval. -§ -Compile-mode eval path: `compile-and-eval` → `compile-ast` (emits Janet data structures with resolved fn values) → Janet `eval`. Source-to-source `compile-form` exists for debugging but NOT used by compile-and-eval. `compile-and-eval` interns def/defn results in Jolt namespace so interpreter can resolve them later. -§ REPL: `main.janet` initializes context and sets current ns to "user". `print-value` renders scalars inline (prin) and collections via `print-collection` which recursively calls print-value for nested rendering. Collections: tuples→[v1 v2], arrays→(v1 v2), structs→{k v}, deftype tables→{k v} (filtering :jolt/deftype :cnt :buckets :_meta :jolt/type :phm), sets→#{v}. Jolt symbol structs render as `name` or `ns/name`. +§ +REPL collection rendering: print-value in main.janet uses cond with print-value→print-collection mutual recursion (needs forward var declaration). Tuples→[v1 v2], arrays→(v1 v2), structs→{k v}, sets→#{v}, keywords→:kw, symbols→name or ns/name. Use prin for scalars, print only after collection closing bracket. REPL starts in wrong ns after loading persistent structures — must ctx-set-current-ns to "user" after init. +§ +fn* special form dispatch: fn* form emitted by macros MUST be @[...] (array) to enter eval-list's special form match. If wrapped in [...] (tuple), eval-form hits the (tuple? form) branch which maps over items instead of dispatching fn*. Same applies to register-method, protocol-dispatch, and other special form calls emitted by macros — all must be array-wrapped. +§ +Janet's struct? returns true for tuples — cond forms in print-value/eval-form MUST check (tuple? x) before (struct? x) or (get x :key). Otherwise Janet sees a tuple, says yes to struct?, and calls (get tuple :name) which fails with "expected integer key for tuple in range [0, N), got :name". This hit us in print-value rendering and eval-form struct handling. +§ +Protocol system: Type registry in context env (:type-registry) maps type-tag→proto-name→method-name→fn. Three dispatch special forms: protocol-dispatch (resolves method via registry or reified methods), register-method (stores impl in registry), make-reified (creates anonymous object with :jolt/protocol-methods). fn* forms emitted by extend-type/extend-protocol MUST be @[...] (array) for eval-list dispatch. Protocols are maps with :jolt/type :jolt/protocol and :methods map. diff --git a/.dirge/memory/PITFALLS.md b/.dirge/memory/PITFALLS.md index e46349b..b586d83 100644 --- a/.dirge/memory/PITFALLS.md +++ b/.dirge/memory/PITFALLS.md @@ -1,7 +1,5 @@ -Janet `break` does NOT work inside `let` — it only breaks from loops (`while`, `loop`). When searching for a key in a bucket and needing to return a value, use `(var found nil)` + `(set found val) (break)` pattern then check `found` after the loop. Same for `break nil` in bucket-dissoc: capture index in a var, break, then construct result after loop. -§ -Keywords containing `#` (like `:#inst`, `:#uuid`) are invalid Janet literal syntax. Use dynamic table construction: `(let [dr @{}] (put dr (keyword "#inst") fn) dr)` instead of `@{:#inst fn}`. This hit us in types.janet make-ctx :data-readers initialization. -§ -PHM/Set internal metadata keys (`:jolt/deftype`, `:cnt`, `:buckets`, `:_meta`, `:jolt/type`, `:phm`) leak into `pairs`/`keys` iteration. Must filter them in core fns like merge, merge-with, keys, vals, and in print-rendering code. `core-merge` without filtering produced corrupted PHMs with metadata as entries. Commit `9c44021` fixed this for merge; `c366963` for print-value. -§ Janet's `case` for multi-arity dispatch: `(defn f [& args] (case (length args) 1 ... 2 ...))`. Used in core-derive, core-isa?, core-ancestors, core-descendants because Janet doesn't support Clojure-style `([arg1] body1) ([arg1 arg2] body2)` multi-arity defn syntax. +§ +Janet's boolean function doesn't exist — use (if x true false). Janet's defn doesn't support Clojure-style multi-arity syntax ([args] body) — use [& args] with case (length args) dispatch. fn? exists as Janet builtin (not Jolt core fn) — use (or (function? x) (cfunction? x)) in tests. +§ +Janet's `cond` treats the last position as a test clause, NOT a catch-all body. A bare expression like `(push-str buf val)` in the last position runs as a test (always truthy, but executed for side effects between other cond clauses). Use `true (push-str buf val)` to make it a proper catch-all body. Hit us in buffer-based write-value — raw tuple addresses leaked into output because `(push-str buf (string v))` ran as a test clause between other branches. diff --git a/.dirge/skills/.usage.json b/.dirge/skills/.usage.json index cc5123c..1caf290 100644 --- a/.dirge/skills/.usage.json +++ b/.dirge/skills/.usage.json @@ -1,69 +1,71 @@ { - "jolt-dev": { - "created_by": "agent", - "use_count": 35, - "view_count": 62, - "patch_count": 42, - "last_used_at": "2026-06-03T03:49:43.510599+00:00", - "last_viewed_at": "2026-06-03T04:20:32.579847+00:00", - "last_patched_at": "2026-06-03T03:34:09.007253+00:00", - "created_at": "2026-06-01T21:26:06.614465+00:00", - "state": "active", - "pinned": false - }, "jolt-compiler": { "created_by": "agent", - "use_count": 9, - "view_count": 25, + "use_count": 10, + "view_count": 128, "patch_count": 9, - "last_used_at": "2026-06-03T03:49:43.470444+00:00", - "last_viewed_at": "2026-06-03T04:20:32.574065+00:00", + "last_used_at": "2026-06-03T13:32:50.977662+00:00", + "last_viewed_at": "2026-06-03T14:12:07.880363+00:00", "last_patched_at": "2026-06-03T03:03:27.524946+00:00", "created_at": "2026-06-02T17:54:38.690279+00:00", "state": "active", "pinned": false }, - "jpm-build": { - "created_by": "agent", - "use_count": 0, - "view_count": 27, - "patch_count": 0, - "last_viewed_at": "2026-06-03T04:20:32.597461+00:00", - "created_at": "2026-06-01T20:56:39.144222+00:00", - "state": "active", - "pinned": false - }, - "jolt-bootstrap": { - "created_by": "agent", - "use_count": 9, - "view_count": 36, - "patch_count": 10, - "last_used_at": "2026-06-02T18:45:23.336653+00:00", - "last_viewed_at": "2026-06-03T04:20:32.567542+00:00", - "last_patched_at": "2026-06-02T03:44:45.935676+00:00", - "created_at": "2026-06-01T21:49:51.101718+00:00", - "state": "active", - "pinned": false - }, - "jolt-gotchas": { - "created_by": "agent", - "use_count": 0, - "view_count": 11, - "patch_count": 0, - "last_viewed_at": "2026-06-03T04:20:32.585916+00:00", - "created_at": "2026-06-03T03:50:41.474730+00:00", - "state": "active", - "pinned": false - }, "jolt-persistent-structures": { "created_by": "agent", "use_count": 1, - "view_count": 13, + "view_count": 115, "patch_count": 0, "last_used_at": "2026-06-03T03:49:43.520839+00:00", - "last_viewed_at": "2026-06-03T04:20:32.591070+00:00", + "last_viewed_at": "2026-06-03T14:12:07.898607+00:00", "created_at": "2026-06-03T03:35:04.130959+00:00", "state": "active", "pinned": false + }, + "jolt-gotchas": { + "created_by": "agent", + "use_count": 3, + "view_count": 116, + "patch_count": 3, + "last_used_at": "2026-06-03T13:35:01.902830+00:00", + "last_viewed_at": "2026-06-03T14:12:07.892405+00:00", + "last_patched_at": "2026-06-03T13:35:14.859032+00:00", + "created_at": "2026-06-03T03:50:41.474730+00:00", + "state": "active", + "pinned": false + }, + "jolt-bootstrap": { + "created_by": "agent", + "use_count": 9, + "view_count": 138, + "patch_count": 10, + "last_used_at": "2026-06-02T18:45:23.336653+00:00", + "last_viewed_at": "2026-06-03T14:12:07.874318+00:00", + "last_patched_at": "2026-06-02T03:44:45.935676+00:00", + "created_at": "2026-06-01T21:49:51.101718+00:00", + "state": "active", + "pinned": false + }, + "jpm-build": { + "created_by": "agent", + "use_count": 0, + "view_count": 129, + "patch_count": 0, + "last_viewed_at": "2026-06-03T14:12:07.904433+00:00", + "created_at": "2026-06-01T20:56:39.144222+00:00", + "state": "active", + "pinned": false + }, + "jolt-dev": { + "created_by": "agent", + "use_count": 37, + "view_count": 166, + "patch_count": 44, + "last_used_at": "2026-06-03T13:32:50.995757+00:00", + "last_viewed_at": "2026-06-03T14:12:07.886018+00:00", + "last_patched_at": "2026-06-03T13:34:25.654327+00:00", + "created_at": "2026-06-01T21:26:06.614465+00:00", + "state": "active", + "pinned": false } } \ No newline at end of file diff --git a/.dirge/skills/jolt-dev/SKILL.md b/.dirge/skills/jolt-dev/SKILL.md index b0a973c..cf49cad 100644 --- a/.dirge/skills/jolt-dev/SKILL.md +++ b/.dirge/skills/jolt-dev/SKILL.md @@ -1,4 +1,8 @@ -# jolt-dev +--- +name: jolt-dev +description: Jolt development workflow — build, test, special form patterns, Janet gotchas +--- + # jolt-dev Jolt development workflow — build, test, special form patterns, Janet gotchas @@ -20,14 +24,14 @@ To add a new special form to the evaluator: 1. Add the name to `special-symbol?` in `src/jolt/evaluator.janet` 2. Add a match arm in `eval-list` (the match on `name`) -3. Add tests in `test/evaluator-test.janet` +3. Add tests The match arm receives `ctx`, `bindings`, and `form` (the full list). Use `(in form 1)` for first arg, etc. **Non-symbol heads** (keywords, etc.): `eval-list` first checks `(and (struct? first-form) (= :symbol (...)))` before extracting `name`. If not a symbol, falls through to default function application. -### Current special forms (29): -`quote`, `syntax-quote`, `unquote`, `unquote-splicing`, `do`, `if`, `def`, `defmacro`, `fn*`, `let*`, `loop*`, `recur`, `throw`, `try`, `set!`, `var`, `locking`, `instance?`, `defmulti`, `defmethod`, `deftype`, `new`, `.`, `var-get`, `var-set`, `var?`, `alter-var-root`, `find-var`, `intern`, `alter-meta!`, `reset-meta!`, `disj`, `set?` +### Current special forms (37): +`quote`, `syntax-quote`, `unquote`, `unquote-splicing`, `do`, `if`, `def`, `defmacro`, `fn*`, `let*`, `loop*`, `recur`, `throw`, `try`, `set!`, `var`, `locking`, `instance?`, `defmulti`, `defmethod`, `deftype`, `new`, `.`, `var-get`, `var-set`, `var?`, `alter-var-root`, `find-var`, `intern`, `alter-meta!`, `reset-meta!`, `disj`, `set?`, `protocol-dispatch`, `register-method`, `make-reified`, `satisfies?` ## Compiler Architecture @@ -37,6 +41,40 @@ Two-phase: `analyze-form [form bindings ctx]` → `emit-ast` (string) or `emit-e **eval-string dispatch** (compile mode): stateful forms → interpreter; everything else → `compile-and-eval`. Macros expand at analyze time. +## Protocol System + +Protocols are maps with `:jolt/type :jolt/protocol` and `:methods` map. +Type registry in context env (`:type-registry`) maps `type-tag → proto-name → method-name → fn`. + +**Special forms:** +- `protocol-dispatch [proto-sym method-sym obj rest-args]` — resolves method via type registry or reified methods +- `register-method [type-sym proto-sym method-sym fn-form]` — stores impl in type registry +- `make-reified [proto-sym methods-map]` — creates anonymous object with `:jolt/protocol-methods` + +**Critical rule:** fn* form inside extend-type/extend-protocol MUST be `@[...]` (array) to trigger eval-list's special form dispatch. Tuples `[...]` hit `(tuple? form)` branch instead. Same for register-method, protocol-dispatch calls. + +## REPL Collection Rendering (Buffer-Based) + +Use `write-value` + `write-collection` with a StringBuffer (`@""`) rather than `prin`/`print` directly. Build the entire output string in a buffer, then atomically `(print (string buf))`. Prevents Janet's C runtime (in `jpm build` executables) from interleaving its native `` printer between incremental `prin` calls. + +```janet +(var write-value nil) ; forward declaration + +(defn- write-collection [v buf] + (cond (tuple? v) (do (buffer/push-string buf "[") ...) + (array? v) (do (buffer/push-string buf "(") ...) ...)) + +(set write-value (fn [v buf] + (cond (nil? v) (buffer/push-string buf "nil") + (number? v) (buffer/push-string buf (string v)) + (tuple? v) (write-collection v buf) + true (buffer/push-string buf (string v))))) ; true REQUIRED + +(defn print-value [v] (def buf @"") (write-value v buf) (print (string buf))) +``` + +**Critical:** Janet's `cond` treats a bare expression in the last position as a **test** clause, not a catch-all body. Use `true` as the guard. + ## PersistentHashMap Gotchas - `core-map?`: `(if (and (table? x) (get x :jolt/deftype)) true false)` — `and` returns last truthy, not boolean @@ -45,9 +83,9 @@ Two-phase: `analyze-form [form bindings ctx]` → `emit-ast` (string) or `emit-e ## defrecord / deftype Patterns -- defrecord emits `(deftype TypeName [fields])` + arrow factory `(fn fields-vec (TypeName. field1 field2...))` +- defrecord emits `(deftype TypeName [fields])` + arrow factory - Records are tables with `:jolt/deftype` = type name string -- `set!` field mutation: `(set! (.-x obj) val)` parses as array with `.-x` symbol head — check symbol name before dispatch +- `set!` field mutation: `(set! (.-x obj) val)` parses as array with `.-x` symbol head ## Binding Macro @@ -55,22 +93,20 @@ Uses `array-map` (plain Janet struct) not `hash-map` (PHM) to avoid PHM get() in ## Tagged Literals (#inst, #uuid) -`:#inst` is invalid Janet keyword syntax (contains `#`). Use dynamic table construction: -```janet -(let [dr @{}] (put dr (keyword "#inst") (fn [s] s)) dr) -``` +Use dynamic table construction: `(let [dr @{}] (put dr (keyword "#inst") fn) dr)` ## LazySeq Patterns - Use `indexed?` not `tuple?` for realized sequences (may be arrays from `cons`/`concat`) -- Avoid `val'` (apostrophe in symbol names) — causes Janet parse errors; use `vf` instead -- `ls-first`/`ls-rest`/`ls-seq` all call `realize-ls` first (caches result, realizes once) +- Avoid `val'` (apostrophe in symbol names) — use `vf` instead ## Janet Gotchas - `def` creates constants; use `(var x nil)` for mutable locals -- Bare tuples in `eval` are function calls: `[1 2 3]` tries to call `1`. Use `['tuple 1 2 3]` +- Bare tuples in `eval` are function calls: `[1 2 3]` tries to call `1` - `try` format: `(try body ([err] handler))` NOT `(try body (catch sym handler))` - core-renames MUST match actual fn names: `"-"` → `"core-sub"` (not `"core--"`) -- Janet `parse` vs `parser/new`: use `parser/new` + `parser/consume` + `parser/eof` + `parser/produce` for full source parsing -- `(break val)` breaks from a while loop returning val — useful in bucket search patterns \ No newline at end of file +- `(break val)` breaks from loop returning val — useful in bucket search patterns +- `boolean` doesn't exist — use `(if x true false)` +- Janet doesn't support Clojure-style multi-arity defn — use `[& args]` with `case (length args)` +- Janet's `cond` treats bare expression in last position as test, not catch-all — use `true` guard \ No newline at end of file diff --git a/.dirge/skills/jolt-gotchas/SKILL.md b/.dirge/skills/jolt-gotchas/SKILL.md index 10775c4..976b09a 100644 --- a/.dirge/skills/jolt-gotchas/SKILL.md +++ b/.dirge/skills/jolt-gotchas/SKILL.md @@ -43,3 +43,26 @@ Both tables must be updated together when adding core fns. Missing entries = sil ## `set!` Field Mutation Reader Quirk `(set! (.-x obj) val)` parses as array with `.-x` symbol head — not as standalone `.-x` symbol. Check for this case before the `(. obj -field)` shorthand. + +## Janet `cond` Requires `true` Guard for Catch-All + +A bare expression in the last position of `cond` is treated as a **test** clause (not body). Use `true` as the test: + +```janet +(cond (nil? x) (buf "nil") (number? x) (buf (string x)) true (buf (string x))) +``` + +Without `true`, the last expression executes as a side-effect test between branches. Hit us in buffer-based write-value — raw tuple addresses leaked into REPL output. + +## Janet `cond` Requires `true` Guard for Catch-All + +A bare expression in the last position of `cond` is treated as a **test** clause (not body). It executes between other branches as a side-effect test. Use `true` as the test to make it a proper catch-all: + +```janet +(cond + (nil? x) (buf "nil") + (number? x) (buf (string x)) + true (buf (string x))) ; ← `true` required +``` + +Without `true`, `(push-str buf (string v))` in the last position leaked raw tuple addresses into REPL output. \ No newline at end of file diff --git a/src/jolt/clojure/set.clj b/src/jolt/clojure/set.clj new file mode 100644 index 0000000..c94d874 --- /dev/null +++ b/src/jolt/clojure/set.clj @@ -0,0 +1,124 @@ +; Jolt Standard Library: clojure.set +; Set operations (union, intersection, difference, subset?, superset?, etc.) + +(defn union + "Return a set that is the union of the input sets." + ([s1] s1) + ([s1 s2] + (if (< (count s1) (count s2)) + (reduce conj s2 s1) + (reduce conj s1 s2))) + ([s1 s2 & sets] + (reduce union (union s1 s2) sets))) + +(defn intersection + "Return a set that is the intersection of the input sets." + ([s1] s1) + ([s1 s2] + (reduce (fn [acc item] + (if (contains? s2 item) acc (disj acc item))) + s1 s1)) + ([s1 s2 & sets] + (reduce intersection (intersection s1 s2) sets))) + +(defn difference + "Return a set that is the first set without elements of the other sets." + ([s1] s1) + ([s1 s2] + (reduce disj s1 s2)) + ([s1 s2 & sets] + (reduce difference (difference s1 s2) sets))) + +(defn select + "Returns a set of the elements for which pred is true." + [pred s] + (reduce (fn [acc item] + (if (pred item) acc (disj acc item))) + s s)) + +(defn project + "Returns a rel of the elements of xrel with only the keys in ks." + [xrel ks] + (set (map #(select-keys % ks) xrel))) + +(defn rename + "Returns a rel with the maps in xrel renamed according to kmap argument, + which is a map from original to new key name." + [xrel kmap] + (set + (map + (fn [m] + (reduce (fn [acc [old new]] + (if (contains? m old) + (assoc acc new (get m old)) + acc)) + (apply dissoc m (keys kmap)) + kmap)) + xrel))) + +(defn rename-keys + "Returns the map with the keys in kmap renamed to the values in kmap." + [map kmap] + (reduce + (fn [m [old new]] + (if (contains? m old) + (assoc m new (get m old) old nil) + m)) + map kmap)) + +(defn map-invert + "Returns the map with the vals mapped to the keys." + [m] + (reduce (fn [acc [k v]] (assoc acc v k)) {} m)) + +(defn join + "When passed 2 rels, returns the rel corresponding to the natural + join. When passed an additional keymap, joins on the corresponding + keys." + ([xrel yrel] + (if (and (seq xrel) (seq yrel)) + (let [ks (intersection (set (keys (first xrel))) + (set (keys (first yrel)))) + idx (map-invert (zipmap (range) yrel))] + (reduce (fn [acc x] + (reduce (fn [acc y] + (if (= (select-keys x ks) + (select-keys y ks)) + (conj acc (merge x y)) + acc)) + acc yrel)) + #{} xrel)) + #{})) + ([xrel yrel kmap] + (let [kmap (if (map? kmap) kmap (zipmap kmap kmap)) + idx (reduce (fn [m y] + (assoc m (select-keys y (vals kmap)) y)) + {} yrel)] + (reduce + (fn [acc x] + (let [found (get idx (select-keys x (keys kmap)))] + (if found + (conj acc (merge x (rename-keys found kmap))) + acc))) + #{} xrel)))) + +(defn index + "Returns a map of the distinct values of ks in the xrel mapped to a + set of the maps in xrel with the corresponding values of ks." + [xrel ks] + (reduce (fn [m x] + (let [ik (select-keys x ks)] + (assoc m ik (conj (get m ik #{}) x)))) + {} xrel)) + +(defn subset? + "Is set1 a subset of set2?" + [set1 set2] + (and (<= (count set1) (count set2)) + (every? #(contains? set2 %) set1))) + +(defn superset? + "Is set1 a superset of set2?" + [set1 set2] + (and (>= (count set1) (count set2)) + (every? #(contains? set1 %) set2))) diff --git a/src/jolt/clojure/string.clj b/src/jolt/clojure/string.clj new file mode 100644 index 0000000..02e3b88 --- /dev/null +++ b/src/jolt/clojure/string.clj @@ -0,0 +1,126 @@ +; Jolt Standard Library: clojure.string +; String manipulation functions using Jolt core string interop. + +(defn blank? + [s] + (if (nil? s) true + (= 0 (count (str-trim s))))) + +(defn capitalize + "Converts first character of the string to upper-case, all other + characters to lower-case." + [s] + (if (< 1 (count s)) + (str (str-upper (subs s 0 1)) + (str-lower (subs s 1))) + (str-upper s))) + +(defn lower-case + "Converts string to all lower-case." + [s] + (str-lower s)) + +(defn upper-case + "Converts string to all upper-case." + [s] + (str-upper s)) + +(defn includes? + "True if s includes substr." + [s substr] + (not (nil? (str-find substr s)))) + +(defn join + "Returns a string of all elements in coll, separated by + an optional separator." + ([coll] (str-join coll)) + ([separator coll] (str-join coll separator))) + +(defn replace + "Replaces all instance of match with replacement in s." + [s match replacement] + (str-replace-all match s replacement)) + +(defn replace-first + "Replaces the first instance of pattern in string with replacement." + [s match replacement] + (str-replace match s replacement)) + +(defn str-reverse + "Returns s with its characters reversed." + [s] + (str-reverse-b s)) + +(defn split + "Splits string on a regular expression. Optional limit." + ([s re] + (map str-trim (str-split re s))) + ([s re limit] + (take limit (split s re)))) + +(defn starts-with? + "True if s starts with substr." + [s substr] + (let [slen (count s) slen2 (count substr)] + (and (>= slen slen2) + (= (subs s 0 slen2) substr)))) + +(defn ends-with? + "True if s ends with substr." + [s substr] + (let [slen (count s) slen2 (count substr)] + (and (>= slen slen2) + (= (subs s (- slen slen2)) substr)))) + +(defn trim + "Removes whitespace from both ends of string." + [s] + (str-trim s)) + +(defn triml + "Removes whitespace from the left side of string." + [s] + (str-triml s)) + +(defn trimr + "Removes whitespace from the right side of string." + [s] + (str-trimr s)) + +(defn trim-newline + "Removes all trailing newline \\n or return \\r characters from string." + [s] + (var result s) + (while (or (= (subs result (dec (count result))) "\n") + (= (subs result (dec (count result))) "\r")) + (set result (subs result 0 (dec (count result))))) + result) + +(defn escape + "Return a new string, using cmap to escape each character ch from s." + [s cmap] + (apply str + (map (fn [ch] + (if-let [rep (cmap ch)] rep (str ch))) + s))) + +(defn index-of + "Return index of value (string or char) in s, optionally + from start. Returns nil if not found." + ([s value] + (let [idx (str-find value s)] + (when idx (inc idx)))) + ([s value from] + (let [idx (str-find value (subs s from))] + (when idx (+ from (inc idx)))))) + +(defn last-index-of + "Return last index of value (string or char) in s." + ([s value] + (let [r (str-reverse-b s) sval (str-reverse-b value) + idx (str-find sval r)] + (when idx (inc (- (count s) (+ idx (count value))))))) + ([s value from] + (let [sub (subs s 0 from) r (str-reverse-b sub) sval (str-reverse-b value) + idx (str-find sval r)] + (when idx (inc (- from (+ idx (count value)))))))) diff --git a/src/jolt/clojure/walk.clj b/src/jolt/clojure/walk.clj new file mode 100644 index 0000000..e87a3ae --- /dev/null +++ b/src/jolt/clojure/walk.clj @@ -0,0 +1,77 @@ +; Jolt Standard Library: clojure.walk +; This file generalizes tree walking for Clojure data structures. + +(defn walk + "Traverses form, an arbitrary data structure. inner and outer are + functions. Applies inner to each element of form, building up a + data structure of the same type, then applies outer to the result. + Recognizes all Clojure data structures. Consumes seqs." + [inner outer form] + (cond + (list? form) (outer (apply list (map inner form))) + (seq? form) (outer (doall (map inner form))) + (vector? form) (outer (vec (map inner form))) + (map? form) (outer (into (empty form) (map inner form))) + (set? form) (outer (into (empty form) (map inner form))) + :else (outer form))) + +(defn postwalk + "Performs a depth-first, post-order traversal of form. Calls f on + each sub-form, uses f's return value in place of the original. + Recognizes all Clojure data structures. Consumes seqs." + [f form] + (walk (partial postwalk f) f form)) + +(defn prewalk + "Like postwalk, but does pre-order traversal." + [f form] + (walk (partial prewalk f) identity (f form))) + +(defn postwalk-demo + "Demonstrates the behavior of postwalk by returning a lazy seq of + forms passed to the postwalk outer function during the traversal + of form." + [form] + (let [acc (atom [])] + (postwalk (fn [x] (swap! acc conj x) x) form) + @acc)) + +(defn prewalk-demo + "Demonstrates the behavior of prewalk by returning a lazy seq of + forms passed to the prewalk outer function during the traversal + of form." + [form] + (let [acc (atom [])] + (prewalk (fn [x] (swap! acc conj x) x) form) + @acc)) + +(defn postwalk-replace + "Recursively transforms form by replacing keys in smap with their + values. Like clojure.string/replace but works with any data + structure. Does replacement at the leaves of the tree first." + [smap form] + (postwalk (fn [x] (if (contains? smap x) (get smap x) x)) form)) + +(defn prewalk-replace + "Recursively transforms form by replacing keys in smap with their + values. Like postwalk-replace but does replacement at the root of + the tree first." + [smap form] + (prewalk (fn [x] (if (contains? smap x) (get smap x) x)) form)) + +(defn keywordize-keys + "Recursively transforms all map keys from strings to keywords." + [m] + (let [f (fn [[k v]] (if (string? k) [(keyword k) v] [k v]))] + (postwalk (fn [x] (if (map? x) (into {} (map f x)) x)) m))) + +(defn stringify-keys + "Recursively transforms all map keys from keywords to strings." + [m] + (let [f (fn [[k v]] (if (keyword? k) [(name k) v] [k v]))] + (postwalk (fn [x] (if (map? x) (into {} (map f x)) x)) m))) + +(defn macroexpand-all + "Recursively performs all possible macroexpansions in form." + [form] + (prewalk (fn [x] (if (seq? x) (macroexpand x) x)) form)) diff --git a/src/jolt/core.janet b/src/jolt/core.janet index 6824b7f..5173cfb 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -1243,6 +1243,17 @@ "str" core-str "name" core-name "subs" core-subs + "str-trim" string/trim + "str-upper" string/ascii-upper + "str-lower" string/ascii-lower + "str-find" string/find + "str-replace" string/replace + "str-replace-all" string/replace-all + "str-reverse-b" string/reverse + "str-join" string/join + "str-split" string/split + "str-triml" string/triml + "str-trimr" string/trimr "print" core-print "println" core-println "pr" core-pr diff --git a/test/phase10-test.janet b/test/phase10-test.janet new file mode 100644 index 0000000..539d2cf --- /dev/null +++ b/test/phase10-test.janet @@ -0,0 +1,42 @@ +(use ../src/jolt/api) +(use ../src/jolt/reader) +(use ../src/jolt/evaluator) + +(defn ct-eval [ctx s] (eval-string ctx s)) + +(defn load-clj [ctx filepath] + (def source (slurp filepath)) + (var remaining source) + (while (> (length (string/trim remaining)) 0) + (def fr (parse-next remaining)) + (def form (fr 0)) + (set remaining (fr 1)) + (when (not (nil? form)) + (eval-form ctx @{} form)))) + +(print "40: clojure.string...") +(let [ctx (init)] + (load-clj ctx "src/jolt/clojure/string.clj") + (assert (= true (ct-eval ctx "(blank? nil)")) "blank? nil") + (assert (= true (ct-eval ctx "(blank? \" \")")) "blank? whitespace") + (assert (= false (ct-eval ctx "(blank? \"a\")")) "blank? non-empty") + (assert (= "Abc" (ct-eval ctx "(capitalize \"abc\")")) "capitalize") + (assert (= "hello" (ct-eval ctx "(lower-case \"HELLO\")")) "lower-case") + (assert (= "HELLO" (ct-eval ctx "(upper-case \"hello\")")) "upper-case") + (assert (= true (ct-eval ctx "(includes? \"hello\" \"ell\")")) "includes? true") + (assert (= "foo" (ct-eval ctx "(trim \"foo\")")) "trim") + (assert (= true (ct-eval ctx "(starts-with? \"hello\" \"he\")")) "starts-with? true") + (assert (= true (ct-eval ctx "(ends-with? \"hello\" \"lo\")")) "ends-with? true")) +(print " passed") + +(print "41: clojure.set...") +(let [ctx (init)] + (load-clj ctx "src/jolt/clojure/set.clj") + (assert (= #{1 2 3} (ct-eval ctx "(union #{1 2} #{2 3})")) "union") + (assert (= #{2} (ct-eval ctx "(intersection #{1 2} #{2 3})")) "intersection") + (assert (= #{1} (ct-eval ctx "(difference #{1 2} #{2 3})")) "difference") + (assert (= true (ct-eval ctx "(subset? #{1} #{1 2 3})")) "subset? true") + (assert (= true (ct-eval ctx "(superset? #{1 2 3} #{1})")) "superset? true")) +(print " passed") + +(print "All Phase 10 tests passed!")