conjure-js 0.0.12 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist-cli/conjure-js.mjs +9360 -5298
  2. package/dist-vite-plugin/index.mjs +9463 -5185
  3. package/package.json +3 -1
  4. package/src/bin/cli.ts +2 -2
  5. package/src/bin/nrepl-symbol.ts +150 -0
  6. package/src/bin/nrepl.ts +289 -167
  7. package/src/bin/version.ts +1 -1
  8. package/src/clojure/core.clj +757 -29
  9. package/src/clojure/core.clj.d.ts +75 -131
  10. package/src/clojure/generated/builtin-namespace-registry.ts +4 -0
  11. package/src/clojure/generated/clojure-core-source.ts +758 -29
  12. package/src/clojure/generated/clojure-set-source.ts +136 -0
  13. package/src/clojure/generated/clojure-walk-source.ts +72 -0
  14. package/src/clojure/set.clj +132 -0
  15. package/src/clojure/set.clj.d.ts +20 -0
  16. package/src/clojure/string.clj.d.ts +14 -0
  17. package/src/clojure/walk.clj +68 -0
  18. package/src/clojure/walk.clj.d.ts +7 -0
  19. package/src/core/assertions.ts +114 -6
  20. package/src/core/bootstrap.ts +337 -0
  21. package/src/core/conversions.ts +48 -31
  22. package/src/core/core-module.ts +303 -0
  23. package/src/core/env.ts +20 -6
  24. package/src/core/evaluator/apply.ts +40 -25
  25. package/src/core/evaluator/arity.ts +8 -8
  26. package/src/core/evaluator/async-evaluator.ts +565 -0
  27. package/src/core/evaluator/collections.ts +28 -5
  28. package/src/core/evaluator/destructure.ts +180 -69
  29. package/src/core/evaluator/dispatch.ts +12 -14
  30. package/src/core/evaluator/evaluate.ts +22 -20
  31. package/src/core/evaluator/expand.ts +45 -15
  32. package/src/core/evaluator/form-parsers.ts +178 -0
  33. package/src/core/evaluator/index.ts +7 -9
  34. package/src/core/evaluator/js-interop.ts +189 -0
  35. package/src/core/evaluator/quasiquote.ts +14 -8
  36. package/src/core/evaluator/recur-check.ts +6 -6
  37. package/src/core/evaluator/special-forms.ts +234 -191
  38. package/src/core/factories.ts +182 -3
  39. package/src/core/index.ts +54 -4
  40. package/src/core/module.ts +136 -0
  41. package/src/core/ns-forms.ts +107 -0
  42. package/src/core/printer.ts +371 -11
  43. package/src/core/reader.ts +84 -33
  44. package/src/core/registry.ts +209 -0
  45. package/src/core/runtime.ts +376 -0
  46. package/src/core/session.ts +253 -487
  47. package/src/core/stdlib/arithmetic.ts +528 -194
  48. package/src/core/stdlib/async-fns.ts +132 -0
  49. package/src/core/stdlib/atoms.ts +291 -56
  50. package/src/core/stdlib/errors.ts +54 -50
  51. package/src/core/stdlib/hof.ts +82 -166
  52. package/src/core/stdlib/js-namespace.ts +344 -0
  53. package/src/core/stdlib/lazy.ts +34 -0
  54. package/src/core/stdlib/maps-sets.ts +322 -0
  55. package/src/core/stdlib/meta.ts +61 -30
  56. package/src/core/stdlib/predicates.ts +325 -187
  57. package/src/core/stdlib/regex.ts +126 -98
  58. package/src/core/stdlib/seq.ts +564 -0
  59. package/src/core/stdlib/strings.ts +164 -135
  60. package/src/core/stdlib/transducers.ts +95 -100
  61. package/src/core/stdlib/utils.ts +292 -130
  62. package/src/core/stdlib/vars.ts +27 -27
  63. package/src/core/stdlib/vectors.ts +122 -0
  64. package/src/core/tokenizer.ts +2 -2
  65. package/src/core/transformations.ts +117 -9
  66. package/src/core/types.ts +98 -2
  67. package/src/host/node-host-module.ts +74 -0
  68. package/src/{vite-plugin-clj/nrepl-relay.ts → nrepl/relay.ts} +72 -11
  69. package/src/vite-plugin-clj/codegen.ts +87 -95
  70. package/src/vite-plugin-clj/index.ts +178 -23
  71. package/src/vite-plugin-clj/namespace-utils.ts +39 -0
  72. package/src/vite-plugin-clj/static-analysis.ts +211 -0
  73. package/src/clojure/demo.clj +0 -72
  74. package/src/clojure/demo.clj.d.ts +0 -0
  75. package/src/core/core-env.ts +0 -61
  76. package/src/core/stdlib/collections.ts +0 -739
  77. package/src/host/node.ts +0 -55
@@ -1,7 +1,116 @@
1
1
  import { EvaluationError } from './errors'
2
- import { valueKeywords, type CljMultiMethod, type CljValue } from './types'
2
+ import type { CljCons, CljLazySeq } from './types'
3
+ import { valueKeywords, type CljMultiMethod, type CljValue, type EvaluationContext } from './types'
4
+ import { derefValue } from './env'
3
5
 
4
- export function printString(value: CljValue): string {
6
+ const LAZY_PRINT_CAP = 100
7
+
8
+ /** Realize a lazy-seq (local copy to avoid circular dep with transformations). */
9
+ function realizeLazy(ls: CljLazySeq): CljValue {
10
+ let current: CljValue = ls
11
+ while (current.kind === 'lazy-seq') {
12
+ const lazy = current as CljLazySeq
13
+ if (lazy.realized) { current = lazy.value!; continue }
14
+ if (lazy.thunk) {
15
+ lazy.value = lazy.thunk()
16
+ lazy.thunk = null
17
+ lazy.realized = true
18
+ current = lazy.value!
19
+ } else {
20
+ return { kind: 'nil', value: null }
21
+ }
22
+ }
23
+ return current
24
+ }
25
+
26
+ /** Walk a lazy/cons chain collecting up to `limit` elements for printing. */
27
+ function collectSeqElements(value: CljValue, limit: number, depth: number): { items: string[]; truncated: boolean } {
28
+ const items: string[] = []
29
+ let current = value
30
+ while (items.length < limit) {
31
+ if (current.kind === 'nil') break
32
+ if (current.kind === 'lazy-seq') {
33
+ current = realizeLazy(current as CljLazySeq)
34
+ continue
35
+ }
36
+ if (current.kind === 'cons') {
37
+ const c = current as CljCons
38
+ items.push(printString(c.head, depth + 1))
39
+ current = c.tail
40
+ continue
41
+ }
42
+ if (current.kind === 'list') {
43
+ for (const v of current.value) {
44
+ if (items.length >= limit) break
45
+ items.push(printString(v, depth + 1))
46
+ }
47
+ break
48
+ }
49
+ if (current.kind === 'vector') {
50
+ for (const v of current.value) {
51
+ if (items.length >= limit) break
52
+ items.push(printString(v, depth + 1))
53
+ }
54
+ break
55
+ }
56
+ // Unknown tail — just print it
57
+ items.push(printString(current, depth + 1))
58
+ break
59
+ }
60
+ return { items, truncated: items.length >= limit }
61
+ }
62
+
63
+ // --- Print context (*print-length* / *print-level*) ---
64
+ // Single-threaded JS: module-level state is safe.
65
+ export interface PrintContext {
66
+ printLength: number | null
67
+ printLevel: number | null
68
+ }
69
+
70
+ let _printCtx: PrintContext = { printLength: null, printLevel: null }
71
+
72
+ export function getPrintContext(): PrintContext { return _printCtx }
73
+
74
+ export function withPrintContext<T>(ctx: PrintContext, fn: () => T): T {
75
+ const prev = _printCtx
76
+ _printCtx = ctx
77
+ try {
78
+ return fn()
79
+ } finally {
80
+ _printCtx = prev
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Build a PrintContext by reading *print-length* and *print-level* from the
86
+ * runtime registry via ctx.resolveNs. Use this (instead of tryLookup) so that
87
+ * dynamic bindings from inside Clojure function bodies are visible even after
88
+ * a snapshot restore (where closure envs are stale).
89
+ */
90
+ export function buildPrintContext(ctx: EvaluationContext): PrintContext {
91
+ const lenVar = ctx.resolveNs('clojure.core')?.vars.get('*print-length*')
92
+ const lvlVar = ctx.resolveNs('clojure.core')?.vars.get('*print-level*')
93
+ const len = lenVar ? derefValue(lenVar) : undefined
94
+ const level = lvlVar ? derefValue(lvlVar) : undefined
95
+ return {
96
+ printLength: len?.kind === 'number' ? len.value : null,
97
+ printLevel: level?.kind === 'number' ? level.value : null,
98
+ }
99
+ }
100
+
101
+ export function printString(value: CljValue, _depth = 0): string {
102
+ const { printLevel } = _printCtx
103
+ if (printLevel !== null && _depth >= printLevel) {
104
+ if (
105
+ value.kind === 'list' || value.kind === 'vector' ||
106
+ value.kind === 'map' || value.kind === 'set' ||
107
+ value.kind === 'cons' || value.kind === 'lazy-seq'
108
+ ) return '#'
109
+ }
110
+ return printStringImpl(value, _depth)
111
+ }
112
+
113
+ function printStringImpl(value: CljValue, depth: number): string {
5
114
  switch (value.kind) {
6
115
  case valueKeywords.number:
7
116
  return value.value.toString()
@@ -37,12 +146,24 @@ export function printString(value: CljValue): string {
37
146
  return `${value.name}`
38
147
  case valueKeywords.symbol:
39
148
  return `${value.name}`
40
- case valueKeywords.list:
41
- return `(${value.value.map(printString).join(' ')})`
42
- case valueKeywords.vector:
43
- return `[${value.value.map(printString).join(' ')}]`
44
- case valueKeywords.map:
45
- return `{${value.entries.map(([key, value]) => `${printString(key)} ${printString(value)}`).join(' ')}}`
149
+ case valueKeywords.list: {
150
+ const { printLength } = _printCtx
151
+ const items = printLength !== null ? value.value.slice(0, printLength) : value.value
152
+ const suffix = printLength !== null && value.value.length > printLength ? ' ...' : ''
153
+ return `(${items.map(v => printString(v, depth + 1)).join(' ')}${suffix})`
154
+ }
155
+ case valueKeywords.vector: {
156
+ const { printLength } = _printCtx
157
+ const items = printLength !== null ? value.value.slice(0, printLength) : value.value
158
+ const suffix = printLength !== null && value.value.length > printLength ? ' ...' : ''
159
+ return `[${items.map(v => printString(v, depth + 1)).join(' ')}${suffix}]`
160
+ }
161
+ case valueKeywords.map: {
162
+ const { printLength } = _printCtx
163
+ const entries = printLength !== null ? value.entries.slice(0, printLength) : value.entries
164
+ const suffix = printLength !== null && value.entries.length > printLength ? ' ...' : ''
165
+ return `{${entries.map(([key, v]) => `${printString(key, depth + 1)} ${printString(v, depth + 1)}`).join(' ')}${suffix}}`
166
+ }
46
167
  case valueKeywords.function: {
47
168
  if (value.arities.length === 1) {
48
169
  const a = value.arities[0]
@@ -64,11 +185,11 @@ export function printString(value: CljValue): string {
64
185
  case valueKeywords.multiMethod:
65
186
  return `(multi-method ${(value as CljMultiMethod).name})`
66
187
  case valueKeywords.atom:
67
- return `#<Atom ${printString(value.value)}>`
188
+ return `#<Atom ${printString(value.value, depth + 1)}>`
68
189
  case valueKeywords.reduced:
69
- return `#<Reduced ${printString(value.value)}>`
190
+ return `#<Reduced ${printString(value.value, depth + 1)}>`
70
191
  case valueKeywords.volatile:
71
- return `#<Volatile ${printString(value.value)}>`
192
+ return `#<Volatile ${printString(value.value, depth + 1)}>`
72
193
  case valueKeywords.regex: {
73
194
  const escaped = value.pattern.replace(/"/g, '\\"')
74
195
  const prefix = value.flags ? `(?${value.flags})` : ''
@@ -76,6 +197,49 @@ export function printString(value: CljValue): string {
76
197
  }
77
198
  case valueKeywords.var:
78
199
  return `#'${value.ns}/${value.name}`
200
+ case valueKeywords.set: {
201
+ const { printLength } = _printCtx
202
+ const items = printLength !== null ? value.values.slice(0, printLength) : value.values
203
+ const suffix = printLength !== null && value.values.length > printLength ? ' ...' : ''
204
+ return `#{${items.map(v => printString(v, depth + 1)).join(' ')}${suffix}}`
205
+ }
206
+ case valueKeywords.delay:
207
+ if (value.realized) return `#<Delay @${printString(value.value!, depth + 1)}>`
208
+ return '#<Delay pending>'
209
+ case valueKeywords.lazySeq:
210
+ case valueKeywords.cons: {
211
+ const { printLength } = _printCtx
212
+ const limit = printLength !== null ? printLength : LAZY_PRINT_CAP
213
+ const { items, truncated } = collectSeqElements(value, limit, depth)
214
+ const suffix = truncated ? ' ...' : ''
215
+ return `(${items.join(' ')}${suffix})`
216
+ }
217
+ case valueKeywords.namespace:
218
+ return `#namespace[${value.name}]`
219
+ // --- ASYNC (experimental) ---
220
+ case 'pending':
221
+ if (value.resolved && value.resolvedValue !== undefined)
222
+ return `#<Pending @${printString(value.resolvedValue, depth + 1)}>`
223
+ return '#<Pending>'
224
+ // --- END ASYNC ---
225
+ case valueKeywords.jsValue: {
226
+ const raw = value.value
227
+ let typeName: string
228
+ if (raw === null) {
229
+ typeName = 'null'
230
+ } else if (raw === undefined) {
231
+ typeName = 'undefined'
232
+ } else if (typeof raw === 'function') {
233
+ typeName = 'Function'
234
+ } else if (Array.isArray(raw)) {
235
+ typeName = 'Array'
236
+ } else if (raw instanceof Promise) {
237
+ typeName = 'Promise'
238
+ } else {
239
+ typeName = (raw as { constructor?: { name?: string } }).constructor?.name ?? 'Object'
240
+ }
241
+ return `#<js ${typeName}>`
242
+ }
79
243
  default:
80
244
  throw new EvaluationError(`unhandled value type: ${value.kind}`, {
81
245
  value,
@@ -86,3 +250,199 @@ export function printString(value: CljValue): string {
86
250
  export function joinLines(lines: string[]): string {
87
251
  return lines.join('\n')
88
252
  }
253
+
254
+ // --- Pretty printer ---
255
+
256
+ // Known "body" forms: value is the number of "header" args kept on the first line.
257
+ // Remaining args are indented 2 spaces from the opening paren.
258
+ const BODY_FORM_HEADER_COUNT: Record<string, number> = {
259
+ // 0-header: entire body is indented
260
+ do: 0, try: 0, and: 0, or: 0, cond: 0, '->': 0, '->>': 0, 'some->': 0, 'some->>': 0,
261
+ // 1-header: one leading arg kept on first line (condition / binding vec / value)
262
+ when: 1, 'when-not': 1, 'when-let': 1, 'when-some': 1, 'when-first': 1,
263
+ if: 1, 'if-not': 1, 'if-let': 1, 'if-some': 1, while: 1,
264
+ let: 1, loop: 1, binding: 1, 'with-open': 1, 'with-local-vars': 1, locking: 1,
265
+ fn: 1, 'fn*': 1,
266
+ def: 1, defonce: 1, ns: 1,
267
+ doseq: 1, dotimes: 1, for: 1,
268
+ case: 1, 'cond->': 1, 'cond->>': 1,
269
+ // 2-header: name + params/dispatch on first line
270
+ defn: 2, 'defn-': 2, defmacro: 2, defmethod: 2,
271
+ }
272
+
273
+ // Forms whose first header arg (binding vector) should be printed as pairs.
274
+ const BINDING_FORMS = new Set([
275
+ 'let', 'loop', 'binding', 'with-open', 'for', 'doseq', 'dotimes',
276
+ ])
277
+
278
+ // Forms whose body args are pairs (test expr, test expr, ...).
279
+ const PAIR_BODY_FORMS = new Set(['cond', 'condp', 'case', 'cond->', 'cond->>'])
280
+
281
+ function sp(n: number): string {
282
+ return n > 0 ? ' '.repeat(n) : ''
283
+ }
284
+
285
+ function lastLineLen(s: string): number {
286
+ const nl = s.lastIndexOf('\n')
287
+ return nl === -1 ? s.length : s.length - nl - 1
288
+ }
289
+
290
+ function pp(value: CljValue, col: number, maxWidth: number): string {
291
+ const flat = printString(value)
292
+ if (col + flat.length <= maxWidth) return flat
293
+
294
+ switch (value.kind) {
295
+ case valueKeywords.list:
296
+ return ppList(value.value, col, maxWidth)
297
+ case valueKeywords.vector:
298
+ return ppVec(value.value, col, maxWidth, false)
299
+ case valueKeywords.map:
300
+ return ppMap(value.entries, col, maxWidth)
301
+ case valueKeywords.set:
302
+ return ppSet(value.values, col, maxWidth)
303
+ case valueKeywords.lazySeq:
304
+ case valueKeywords.cons:
305
+ // Flat representation is already computed above; no deeper pretty-print needed
306
+ return flat
307
+ default:
308
+ return flat
309
+ }
310
+ }
311
+
312
+ function ppList(items: CljValue[], col: number, maxWidth: number): string {
313
+ if (items.length === 0) return '()'
314
+
315
+ const [head, ...args] = items
316
+ const headStr = printString(head)
317
+ const name = head.kind === valueKeywords.symbol ? head.name : null
318
+
319
+ // --- Known body form ---
320
+ if (name !== null && name in BODY_FORM_HEADER_COUNT) {
321
+ const hCount = BODY_FORM_HEADER_COUNT[name]
322
+ const headerArgs = args.slice(0, hCount)
323
+ const bodyArgs = args.slice(hCount)
324
+ const bodyIndent = col + 2
325
+
326
+ // Build header line: "(name headerArg0 headerArg1 ...)"
327
+ let result = '(' + headStr
328
+ let curCol = col + 1 + headStr.length
329
+
330
+ for (let i = 0; i < headerArgs.length; i++) {
331
+ const arg = headerArgs[i]
332
+ const argCol = curCol + 1
333
+ const isPairVec = BINDING_FORMS.has(name) && i === 0 && arg.kind === valueKeywords.vector
334
+ const argStr = isPairVec
335
+ ? ppVec((arg as Extract<CljValue, { kind: 'vector' }>).value, argCol, maxWidth, true)
336
+ : pp(arg, argCol, maxWidth)
337
+ result += ' ' + argStr
338
+ curCol = argStr.includes('\n') ? lastLineLen(argStr) : argCol + argStr.length - 1
339
+ }
340
+
341
+ if (bodyArgs.length === 0) return result + ')'
342
+
343
+ const bodyStr = PAIR_BODY_FORMS.has(name)
344
+ ? ppPairs(bodyArgs, bodyIndent, maxWidth)
345
+ : bodyArgs.map(a => sp(bodyIndent) + pp(a, bodyIndent, maxWidth)).join('\n')
346
+
347
+ return result + '\n' + bodyStr + ')'
348
+ }
349
+
350
+ // --- General case: flow or 2-space indent ---
351
+ if (args.length === 0) return '(' + headStr + ')'
352
+
353
+ const firstArgCol = col + 1 + headStr.length + 1
354
+
355
+ if (args.length === 1) {
356
+ return '(' + headStr + ' ' + pp(args[0], firstArgCol, maxWidth) + ')'
357
+ }
358
+
359
+ // For short head names: align subsequent args with the first arg.
360
+ // For long head names: fall back to 2-space indent for all args.
361
+ const argIndent = headStr.length <= 10 ? firstArgCol : col + 2
362
+ const argStrs = args.map(a => pp(a, argIndent, maxWidth))
363
+
364
+ if (argIndent === firstArgCol) {
365
+ return (
366
+ '(' + headStr + ' ' + argStrs[0] + '\n' +
367
+ argStrs.slice(1).map(s => sp(argIndent) + s).join('\n') + ')'
368
+ )
369
+ }
370
+ return '(' + headStr + '\n' + argStrs.map(s => sp(argIndent) + s).join('\n') + ')'
371
+ }
372
+
373
+ function ppVec(items: CljValue[], col: number, maxWidth: number, pairMode: boolean): string {
374
+ if (items.length === 0) return '[]'
375
+
376
+ const innerCol = col + 1
377
+
378
+ if (pairMode) {
379
+ const lines: string[] = []
380
+ for (let i = 0; i < items.length; i += 2) {
381
+ const prefix = i === 0 ? '' : sp(innerCol)
382
+ const keyFlat = printString(items[i])
383
+ if (i + 1 >= items.length) {
384
+ lines.push(prefix + keyFlat)
385
+ continue
386
+ }
387
+ const val = items[i + 1]
388
+ const pairFlat = keyFlat + ' ' + printString(val)
389
+ if (innerCol + pairFlat.length <= maxWidth) {
390
+ lines.push(prefix + pairFlat)
391
+ } else {
392
+ const valStr = pp(val, innerCol + keyFlat.length + 1, maxWidth)
393
+ lines.push(prefix + keyFlat + ' ' + valStr)
394
+ }
395
+ }
396
+ return '[' + lines.join('\n') + ']'
397
+ }
398
+
399
+ const strs = items.map((item, i) => {
400
+ const s = pp(item, innerCol, maxWidth)
401
+ return (i === 0 ? '' : sp(innerCol)) + s
402
+ })
403
+ return '[' + strs.join('\n') + ']'
404
+ }
405
+
406
+ function ppMap(entries: [CljValue, CljValue][], col: number, maxWidth: number): string {
407
+ if (entries.length === 0) return '{}'
408
+ const innerCol = col + 1
409
+ const pairs = entries.map(([k, v], i) => {
410
+ const kStr = printString(k)
411
+ const vStr = pp(v, innerCol + kStr.length + 1, maxWidth)
412
+ return (i === 0 ? '' : sp(innerCol)) + kStr + ' ' + vStr
413
+ })
414
+ return '{' + pairs.join('\n') + '}'
415
+ }
416
+
417
+ function ppSet(items: CljValue[], col: number, maxWidth: number): string {
418
+ if (items.length === 0) return '#{}'
419
+ const innerCol = col + 2 // '#{'
420
+ const strs = items.map((item, i) => {
421
+ const s = pp(item, innerCol, maxWidth)
422
+ return (i === 0 ? '' : sp(innerCol)) + s
423
+ })
424
+ return '#{' + strs.join('\n') + '}'
425
+ }
426
+
427
+ function ppPairs(items: CljValue[], indent: number, maxWidth: number): string {
428
+ const lines: string[] = []
429
+ for (let i = 0; i < items.length; i += 2) {
430
+ const testStr = pp(items[i], indent, maxWidth)
431
+ if (i + 1 >= items.length) {
432
+ lines.push(sp(indent) + testStr)
433
+ continue
434
+ }
435
+ const exprFlat = printString(items[i + 1])
436
+ const pairFlat = testStr + ' ' + exprFlat
437
+ if (indent + pairFlat.length <= maxWidth) {
438
+ lines.push(sp(indent) + pairFlat)
439
+ } else {
440
+ lines.push(sp(indent) + testStr + '\n' + sp(indent + 2) + pp(items[i + 1], indent + 2, maxWidth))
441
+ }
442
+ }
443
+ return lines.join('\n')
444
+ }
445
+
446
+ export function prettyPrintString(value: CljValue, maxWidth = 80): string {
447
+ return pp(value, 0, maxWidth)
448
+ }
@@ -1,14 +1,6 @@
1
1
  import { ReaderError } from './errors'
2
- import {
3
- cljBoolean,
4
- cljKeyword,
5
- cljList,
6
- cljMap,
7
- cljNil,
8
- cljRegex,
9
- cljSymbol,
10
- cljVector,
11
- } from './factories'
2
+ import { v } from './factories'
3
+ import { is } from './assertions'
12
4
  import { makeTokenScanner, type TokenScanner } from './scanners'
13
5
  import { getTokenValue } from './tokenizer'
14
6
  import { valueKeywords, tokenKeywords, type Token } from './types'
@@ -86,7 +78,7 @@ const readQuote = (ctx: ReaderCtx) => {
86
78
  if (!value) {
87
79
  throw new ReaderError(`Unexpected token: ${getTokenValue(token)}`, token)
88
80
  }
89
- return { kind: valueKeywords.list, value: [cljSymbol('quote'), value] }
81
+ return { kind: valueKeywords.list, value: [v.symbol('quote'), value] }
90
82
  }
91
83
 
92
84
  const readQuasiquote = (ctx: ReaderCtx) => {
@@ -103,7 +95,7 @@ const readQuasiquote = (ctx: ReaderCtx) => {
103
95
  if (!value) {
104
96
  throw new ReaderError(`Unexpected token: ${getTokenValue(token)}`, token)
105
97
  }
106
- return { kind: valueKeywords.list, value: [cljSymbol('quasiquote'), value] }
98
+ return { kind: valueKeywords.list, value: [v.symbol('quasiquote'), value] }
107
99
  }
108
100
 
109
101
  const readUnquote = (ctx: ReaderCtx) => {
@@ -120,7 +112,7 @@ const readUnquote = (ctx: ReaderCtx) => {
120
112
  if (!value) {
121
113
  throw new ReaderError(`Unexpected token: ${getTokenValue(token)}`, token)
122
114
  }
123
- return { kind: valueKeywords.list, value: [cljSymbol('unquote'), value] }
115
+ return { kind: valueKeywords.list, value: [v.symbol('unquote'), value] }
124
116
  }
125
117
 
126
118
  const readMeta = (ctx: ReaderCtx): CljValue => {
@@ -140,11 +132,11 @@ const readMeta = (ctx: ReaderCtx): CljValue => {
140
132
  // Convert metaForm to a CljMap
141
133
  let metaEntries: [CljValue, CljValue][]
142
134
  if (metaForm.kind === 'keyword') {
143
- metaEntries = [[metaForm, cljBoolean(true)]]
135
+ metaEntries = [[metaForm, v.boolean(true)]]
144
136
  } else if (metaForm.kind === 'map') {
145
137
  metaEntries = metaForm.entries
146
138
  } else if (metaForm.kind === 'symbol') {
147
- metaEntries = [[cljKeyword(':tag'), metaForm]]
139
+ metaEntries = [[v.keyword(':tag'), metaForm]]
148
140
  } else {
149
141
  throw new ReaderError('Metadata must be a keyword, map, or symbol', token)
150
142
  }
@@ -157,7 +149,10 @@ const readMeta = (ctx: ReaderCtx): CljValue => {
157
149
  target.kind === 'map'
158
150
  ) {
159
151
  const existingEntries = target.meta ? target.meta.entries : []
160
- const result = { ...target, meta: cljMap([...existingEntries, ...metaEntries]) }
152
+ const result = {
153
+ ...target,
154
+ meta: v.map([...existingEntries, ...metaEntries]),
155
+ }
161
156
  // Spread drops non-enumerable properties like _pos — re-attach it.
162
157
  const pos = getPos(target)
163
158
  if (pos) setPos(result, pos)
@@ -171,13 +166,13 @@ const readVarQuote = (ctx: ReaderCtx) => {
171
166
  const token = scanner.peek()
172
167
  if (!token) {
173
168
  throw new ReaderError(
174
- "Unexpected end of input while parsing var quote",
169
+ 'Unexpected end of input while parsing var quote',
175
170
  scanner.position()
176
171
  )
177
172
  }
178
173
  scanner.advance() // consume VarQuote token
179
174
  const value = readForm(ctx)
180
- return cljList([cljSymbol('var'), value])
175
+ return v.list([v.symbol('var'), value])
181
176
  }
182
177
 
183
178
  const readDeref = (ctx: ReaderCtx) => {
@@ -194,7 +189,7 @@ const readDeref = (ctx: ReaderCtx) => {
194
189
  if (!value) {
195
190
  throw new ReaderError(`Unexpected token: ${getTokenValue(token)}`, token)
196
191
  }
197
- return { kind: valueKeywords.list, value: [cljSymbol('deref'), value] }
192
+ return { kind: valueKeywords.list, value: [v.symbol('deref'), value] }
198
193
  }
199
194
 
200
195
  const readUnquoteSplicing = (ctx: ReaderCtx) => {
@@ -213,7 +208,7 @@ const readUnquoteSplicing = (ctx: ReaderCtx) => {
213
208
  }
214
209
  return {
215
210
  kind: valueKeywords.list,
216
- value: [cljSymbol('unquote-splicing'), value],
211
+ value: [v.symbol('unquote-splicing'), value],
217
212
  }
218
213
  }
219
214
 
@@ -280,6 +275,60 @@ const readList = collectionReader('list', tokenKeywords.RParen)
280
275
 
281
276
  const readVector = collectionReader('vector', tokenKeywords.RBracket)
282
277
 
278
+ const readSet = (ctx: ReaderCtx) => {
279
+ const scanner = ctx.scanner
280
+ const startToken = scanner.peek()
281
+ if (!startToken) {
282
+ throw new ReaderError(
283
+ 'Unexpected end of input while parsing set',
284
+ scanner.position()
285
+ )
286
+ }
287
+ scanner.advance() // consume the SetStart token
288
+
289
+ const values: CljValue[] = []
290
+ let pairMatched = false
291
+ let closingEnd: number | undefined
292
+ while (!scanner.isAtEnd()) {
293
+ const token = scanner.peek()
294
+ if (!token) break
295
+ if (isClosingToken(token) && token.kind !== tokenKeywords.RBrace) {
296
+ throw new ReaderError(
297
+ `Expected '}' to close set started at line ${startToken.start.line} column ${startToken.start.col}, but got '${getTokenValue(token)}' at line ${token.start.line} column ${token.start.col}`,
298
+ token,
299
+ { start: token.start.offset, end: token.end.offset }
300
+ )
301
+ }
302
+ if (token.kind === tokenKeywords.RBrace) {
303
+ closingEnd = token.end.offset
304
+ scanner.advance() // consume the closing brace
305
+ pairMatched = true
306
+ break
307
+ }
308
+ values.push(readForm(ctx))
309
+ }
310
+ if (!pairMatched) {
311
+ throw new ReaderError(
312
+ `Unmatched set started at line ${startToken.start.line} column ${startToken.start.col}`,
313
+ scanner.peek()
314
+ )
315
+ }
316
+
317
+ // Deduplicate using isEqual
318
+ const deduped: CljValue[] = []
319
+ for (const v of values) {
320
+ if (!deduped.some((existing) => is.equal(existing, v))) {
321
+ deduped.push(v)
322
+ }
323
+ }
324
+
325
+ const result = v.set(deduped)
326
+ if (closingEnd !== undefined) {
327
+ setPos(result, { start: startToken.start.offset, end: closingEnd })
328
+ }
329
+ return result
330
+ }
331
+
283
332
  const readSymbol = (scanner: TokenScanner) => {
284
333
  const token = scanner.peek()
285
334
  if (!token) {
@@ -296,13 +345,13 @@ const readSymbol = (scanner: TokenScanner) => {
296
345
  switch (token.value) {
297
346
  case 'true':
298
347
  case 'false':
299
- val = cljBoolean(token.value === 'true')
348
+ val = v.boolean(token.value === 'true')
300
349
  break
301
350
  case 'nil':
302
- val = cljNil()
351
+ val = v.nil()
303
352
  break
304
353
  default:
305
- val = cljSymbol(token.value)
354
+ val = v.symbol(token.value)
306
355
  }
307
356
  setPos(val, { start: token.start.offset, end: token.end.offset })
308
357
  return val
@@ -416,9 +465,9 @@ function substituteAnonFnParams(form: CljValue): CljValue {
416
465
  switch (form.kind) {
417
466
  case 'symbol': {
418
467
  const name = form.name
419
- if (name === '%' || name === '%1') return cljSymbol('p1')
420
- if (/^%[2-9]$/.test(name)) return cljSymbol(`p${name[1]}`)
421
- if (name === '%&') return cljSymbol('rest')
468
+ if (name === '%' || name === '%1') return v.symbol('p1')
469
+ if (/^%[2-9]$/.test(name)) return v.symbol(`p${name[1]}`)
470
+ if (name === '%&') return v.symbol('rest')
422
471
  return form
423
472
  }
424
473
  case 'list':
@@ -494,18 +543,18 @@ const readAnonFn = (ctx: ReaderCtx) => {
494
543
 
495
544
  const paramSymbols: CljValue[] = []
496
545
  for (let i = 1; i <= maxIndex; i++) {
497
- paramSymbols.push(cljSymbol(`p${i}`))
546
+ paramSymbols.push(v.symbol(`p${i}`))
498
547
  }
499
548
  if (hasRest) {
500
- paramSymbols.push(cljSymbol('&'))
501
- paramSymbols.push(cljSymbol('rest'))
549
+ paramSymbols.push(v.symbol('&'))
550
+ paramSymbols.push(v.symbol('rest'))
502
551
  }
503
552
 
504
553
  const substitutedBody = substituteAnonFnParams(bodyList)
505
554
 
506
- const result = cljList([
507
- cljSymbol('fn'),
508
- cljVector(paramSymbols),
555
+ const result = v.list([
556
+ v.symbol('fn'),
557
+ v.vector(paramSymbols),
509
558
  substitutedBody,
510
559
  ])
511
560
  if (closingEnd !== undefined) {
@@ -545,7 +594,7 @@ const readRegex = (ctx: ReaderCtx): CljValue => {
545
594
  }
546
595
  scanner.advance()
547
596
  const { pattern, flags } = extractInlineFlags(token.value)
548
- const val = cljRegex(pattern, flags)
597
+ const val = v.regex(pattern, flags)
549
598
  setPos(val, { start: token.start.offset, end: token.end.offset })
550
599
  return val
551
600
  }
@@ -578,6 +627,8 @@ function readForm(ctx: ReaderCtx): CljValue {
578
627
  return readUnquoteSplicing(ctx)
579
628
  case tokenKeywords.AnonFnStart:
580
629
  return readAnonFn(ctx)
630
+ case tokenKeywords.SetStart:
631
+ return readSet(ctx)
581
632
  case tokenKeywords.Deref:
582
633
  return readDeref(ctx)
583
634
  case tokenKeywords.VarQuote: