Phase 10: Standard Library — clojure.string, clojure.set, clojure.walk

- src/jolt/clojure/string.clj (123 lines, 20 functions):
  blank?, capitalize, lower-case, upper-case, includes?, join,
  replace, replace-first, str-reverse, split, starts-with?,
  ends-with?, trim, triml, trimr, trim-newline, escape,
  index-of, last-index-of
- src/jolt/clojure/set.clj (124 lines, 10 operations):
  union, intersection, difference, select, project, rename,
  rename-keys, map-invert, join, index, subset?, superset?
- src/jolt/clojure/walk.clj (77 lines, 9 functions):
  walk, postwalk, prewalk, postwalk-demo, prewalk-demo,
  postwalk-replace, prewalk-replace, keywordize-keys,
  stringify-keys, macroexpand-all
- src/jolt/core.janet: 11 Janet string interop bindings
  (str-trim, str-upper, str-lower, str-find, str-replace,
  str-replace-all, str-reverse-b, str-join, str-split,
  str-triml, str-trimr)
- test/phase10-test.janet: 2 test sections (40-41)
  15+ assertions covering string and set functions
- All .clj files use eval-form for multi-form loading
- 315 ok, 2 fail (pre-existing, unchanged)
This commit is contained in:
Yogthos 2026-06-03 10:12:51 -04:00
parent e63c2ce8d5
commit fdb0f4ab83
10 changed files with 518 additions and 77 deletions

View file

@ -1,7 +1,9 @@
Build: `jpm test` runs all tests; `janet test/<file>.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.

View file

@ -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.

View file

@ -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
}
}

View file

@ -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 `<tuple 0x...>` 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
- `(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

View file

@ -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.

124
src/jolt/clojure/set.clj Normal file
View file

@ -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)))

126
src/jolt/clojure/string.clj Normal file
View file

@ -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))))))))

77
src/jolt/clojure/walk.clj Normal file
View file

@ -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))

View file

@ -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

42
test/phase10-test.janet Normal file
View file

@ -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!")