conjure-js 0.0.11 → 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 (80) hide show
  1. package/dist-cli/conjure-js.mjs +9336 -5028
  2. package/dist-vite-plugin/index.mjs +10455 -0
  3. package/package.json +9 -2
  4. package/src/bin/cli.ts +2 -2
  5. package/src/bin/nrepl-symbol.ts +150 -0
  6. package/src/bin/nrepl.ts +301 -157
  7. package/src/bin/version.ts +1 -1
  8. package/src/clojure/core.clj +764 -29
  9. package/src/clojure/core.clj.d.ts +76 -4
  10. package/src/clojure/demo/math.clj +5 -1
  11. package/src/clojure/generated/builtin-namespace-registry.ts +4 -0
  12. package/src/clojure/generated/clojure-core-source.ts +765 -29
  13. package/src/clojure/generated/clojure-set-source.ts +136 -0
  14. package/src/clojure/generated/clojure-walk-source.ts +72 -0
  15. package/src/clojure/set.clj +132 -0
  16. package/src/clojure/set.clj.d.ts +20 -0
  17. package/src/clojure/string.clj.d.ts +14 -0
  18. package/src/clojure/walk.clj +68 -0
  19. package/src/clojure/walk.clj.d.ts +7 -0
  20. package/src/core/assertions.ts +114 -6
  21. package/src/core/bootstrap.ts +337 -0
  22. package/src/core/conversions.ts +48 -31
  23. package/src/core/core-module.ts +303 -0
  24. package/src/core/env.ts +42 -7
  25. package/src/core/errors.ts +8 -0
  26. package/src/core/evaluator/apply.ts +40 -25
  27. package/src/core/evaluator/arity.ts +8 -8
  28. package/src/core/evaluator/async-evaluator.ts +565 -0
  29. package/src/core/evaluator/collections.ts +30 -4
  30. package/src/core/evaluator/destructure.ts +180 -69
  31. package/src/core/evaluator/dispatch.ts +24 -14
  32. package/src/core/evaluator/evaluate.ts +22 -20
  33. package/src/core/evaluator/expand.ts +45 -15
  34. package/src/core/evaluator/form-parsers.ts +178 -0
  35. package/src/core/evaluator/index.ts +7 -9
  36. package/src/core/evaluator/js-interop.ts +189 -0
  37. package/src/core/evaluator/quasiquote.ts +14 -8
  38. package/src/core/evaluator/recur-check.ts +6 -6
  39. package/src/core/evaluator/special-forms.ts +380 -173
  40. package/src/core/factories.ts +182 -3
  41. package/src/core/index.ts +55 -5
  42. package/src/core/module.ts +136 -0
  43. package/src/core/ns-forms.ts +107 -0
  44. package/src/core/positions.ts +9 -2
  45. package/src/core/printer.ts +371 -11
  46. package/src/core/reader.ts +127 -29
  47. package/src/core/registry.ts +209 -0
  48. package/src/core/runtime.ts +376 -0
  49. package/src/core/session.ts +263 -478
  50. package/src/core/stdlib/arithmetic.ts +516 -215
  51. package/src/core/stdlib/async-fns.ts +132 -0
  52. package/src/core/stdlib/atoms.ts +286 -63
  53. package/src/core/stdlib/errors.ts +54 -50
  54. package/src/core/stdlib/hof.ts +74 -173
  55. package/src/core/stdlib/js-namespace.ts +344 -0
  56. package/src/core/stdlib/lazy.ts +34 -0
  57. package/src/core/stdlib/maps-sets.ts +322 -0
  58. package/src/core/stdlib/meta.ts +109 -28
  59. package/src/core/stdlib/predicates.ts +322 -196
  60. package/src/core/stdlib/regex.ts +126 -98
  61. package/src/core/stdlib/seq.ts +564 -0
  62. package/src/core/stdlib/strings.ts +164 -135
  63. package/src/core/stdlib/transducers.ts +95 -100
  64. package/src/core/stdlib/utils.ts +283 -147
  65. package/src/core/stdlib/vars.ts +27 -27
  66. package/src/core/stdlib/vectors.ts +122 -0
  67. package/src/core/tokenizer.ts +13 -3
  68. package/src/core/transformations.ts +117 -9
  69. package/src/core/types.ts +118 -6
  70. package/src/host/node-host-module.ts +74 -0
  71. package/src/nrepl/relay.ts +432 -0
  72. package/src/vite-plugin-clj/codegen.ts +87 -95
  73. package/src/vite-plugin-clj/index.ts +242 -18
  74. package/src/vite-plugin-clj/namespace-utils.ts +39 -0
  75. package/src/vite-plugin-clj/static-analysis.ts +211 -0
  76. package/src/clojure/demo.clj +0 -63
  77. package/src/clojure/demo.clj.d.ts +0 -0
  78. package/src/core/core-env.ts +0 -60
  79. package/src/core/stdlib/collections.ts +0 -784
  80. 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,17 +1,11 @@
1
1
  import { ReaderError } from './errors'
2
- import {
3
- cljBoolean,
4
- cljList,
5
- cljNil,
6
- cljRegex,
7
- cljSymbol,
8
- cljVector,
9
- } from './factories'
2
+ import { v } from './factories'
3
+ import { is } from './assertions'
10
4
  import { makeTokenScanner, type TokenScanner } from './scanners'
11
5
  import { getTokenValue } from './tokenizer'
12
6
  import { valueKeywords, tokenKeywords, type Token } from './types'
13
7
  import type { CljValue, TokenKinds } from './types'
14
- import { setPos } from './positions'
8
+ import { getPos, setPos } from './positions'
15
9
 
16
10
  function readAtom(ctx: ReaderCtx): CljValue {
17
11
  const scanner = ctx.scanner
@@ -84,7 +78,7 @@ const readQuote = (ctx: ReaderCtx) => {
84
78
  if (!value) {
85
79
  throw new ReaderError(`Unexpected token: ${getTokenValue(token)}`, token)
86
80
  }
87
- return { kind: valueKeywords.list, value: [cljSymbol('quote'), value] }
81
+ return { kind: valueKeywords.list, value: [v.symbol('quote'), value] }
88
82
  }
89
83
 
90
84
  const readQuasiquote = (ctx: ReaderCtx) => {
@@ -101,7 +95,7 @@ const readQuasiquote = (ctx: ReaderCtx) => {
101
95
  if (!value) {
102
96
  throw new ReaderError(`Unexpected token: ${getTokenValue(token)}`, token)
103
97
  }
104
- return { kind: valueKeywords.list, value: [cljSymbol('quasiquote'), value] }
98
+ return { kind: valueKeywords.list, value: [v.symbol('quasiquote'), value] }
105
99
  }
106
100
 
107
101
  const readUnquote = (ctx: ReaderCtx) => {
@@ -118,7 +112,53 @@ const readUnquote = (ctx: ReaderCtx) => {
118
112
  if (!value) {
119
113
  throw new ReaderError(`Unexpected token: ${getTokenValue(token)}`, token)
120
114
  }
121
- return { kind: valueKeywords.list, value: [cljSymbol('unquote'), value] }
115
+ return { kind: valueKeywords.list, value: [v.symbol('unquote'), value] }
116
+ }
117
+
118
+ const readMeta = (ctx: ReaderCtx): CljValue => {
119
+ const scanner = ctx.scanner
120
+ const token = scanner.peek()
121
+ if (!token) {
122
+ throw new ReaderError(
123
+ 'Unexpected end of input while parsing metadata',
124
+ scanner.position()
125
+ )
126
+ }
127
+ scanner.advance() // consume Meta token
128
+
129
+ const metaForm = readForm(ctx)
130
+ const target = readForm(ctx)
131
+
132
+ // Convert metaForm to a CljMap
133
+ let metaEntries: [CljValue, CljValue][]
134
+ if (metaForm.kind === 'keyword') {
135
+ metaEntries = [[metaForm, v.boolean(true)]]
136
+ } else if (metaForm.kind === 'map') {
137
+ metaEntries = metaForm.entries
138
+ } else if (metaForm.kind === 'symbol') {
139
+ metaEntries = [[v.keyword(':tag'), metaForm]]
140
+ } else {
141
+ throw new ReaderError('Metadata must be a keyword, map, or symbol', token)
142
+ }
143
+
144
+ // Attach metadata to IMeta targets: symbols, lists, vectors, maps.
145
+ if (
146
+ target.kind === 'symbol' ||
147
+ target.kind === 'list' ||
148
+ target.kind === 'vector' ||
149
+ target.kind === 'map'
150
+ ) {
151
+ const existingEntries = target.meta ? target.meta.entries : []
152
+ const result = {
153
+ ...target,
154
+ meta: v.map([...existingEntries, ...metaEntries]),
155
+ }
156
+ // Spread drops non-enumerable properties like _pos — re-attach it.
157
+ const pos = getPos(target)
158
+ if (pos) setPos(result, pos)
159
+ return result
160
+ }
161
+ return target
122
162
  }
123
163
 
124
164
  const readVarQuote = (ctx: ReaderCtx) => {
@@ -126,13 +166,13 @@ const readVarQuote = (ctx: ReaderCtx) => {
126
166
  const token = scanner.peek()
127
167
  if (!token) {
128
168
  throw new ReaderError(
129
- "Unexpected end of input while parsing var quote",
169
+ 'Unexpected end of input while parsing var quote',
130
170
  scanner.position()
131
171
  )
132
172
  }
133
173
  scanner.advance() // consume VarQuote token
134
174
  const value = readForm(ctx)
135
- return cljList([cljSymbol('var'), value])
175
+ return v.list([v.symbol('var'), value])
136
176
  }
137
177
 
138
178
  const readDeref = (ctx: ReaderCtx) => {
@@ -149,7 +189,7 @@ const readDeref = (ctx: ReaderCtx) => {
149
189
  if (!value) {
150
190
  throw new ReaderError(`Unexpected token: ${getTokenValue(token)}`, token)
151
191
  }
152
- return { kind: valueKeywords.list, value: [cljSymbol('deref'), value] }
192
+ return { kind: valueKeywords.list, value: [v.symbol('deref'), value] }
153
193
  }
154
194
 
155
195
  const readUnquoteSplicing = (ctx: ReaderCtx) => {
@@ -168,7 +208,7 @@ const readUnquoteSplicing = (ctx: ReaderCtx) => {
168
208
  }
169
209
  return {
170
210
  kind: valueKeywords.list,
171
- value: [cljSymbol('unquote-splicing'), value],
211
+ value: [v.symbol('unquote-splicing'), value],
172
212
  }
173
213
  }
174
214
 
@@ -235,6 +275,60 @@ const readList = collectionReader('list', tokenKeywords.RParen)
235
275
 
236
276
  const readVector = collectionReader('vector', tokenKeywords.RBracket)
237
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
+
238
332
  const readSymbol = (scanner: TokenScanner) => {
239
333
  const token = scanner.peek()
240
334
  if (!token) {
@@ -251,13 +345,13 @@ const readSymbol = (scanner: TokenScanner) => {
251
345
  switch (token.value) {
252
346
  case 'true':
253
347
  case 'false':
254
- val = cljBoolean(token.value === 'true')
348
+ val = v.boolean(token.value === 'true')
255
349
  break
256
350
  case 'nil':
257
- val = cljNil()
351
+ val = v.nil()
258
352
  break
259
353
  default:
260
- val = cljSymbol(token.value)
354
+ val = v.symbol(token.value)
261
355
  }
262
356
  setPos(val, { start: token.start.offset, end: token.end.offset })
263
357
  return val
@@ -371,9 +465,9 @@ function substituteAnonFnParams(form: CljValue): CljValue {
371
465
  switch (form.kind) {
372
466
  case 'symbol': {
373
467
  const name = form.name
374
- if (name === '%' || name === '%1') return cljSymbol('p1')
375
- if (/^%[2-9]$/.test(name)) return cljSymbol(`p${name[1]}`)
376
- 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')
377
471
  return form
378
472
  }
379
473
  case 'list':
@@ -449,18 +543,18 @@ const readAnonFn = (ctx: ReaderCtx) => {
449
543
 
450
544
  const paramSymbols: CljValue[] = []
451
545
  for (let i = 1; i <= maxIndex; i++) {
452
- paramSymbols.push(cljSymbol(`p${i}`))
546
+ paramSymbols.push(v.symbol(`p${i}`))
453
547
  }
454
548
  if (hasRest) {
455
- paramSymbols.push(cljSymbol('&'))
456
- paramSymbols.push(cljSymbol('rest'))
549
+ paramSymbols.push(v.symbol('&'))
550
+ paramSymbols.push(v.symbol('rest'))
457
551
  }
458
552
 
459
553
  const substitutedBody = substituteAnonFnParams(bodyList)
460
554
 
461
- const result = cljList([
462
- cljSymbol('fn'),
463
- cljVector(paramSymbols),
555
+ const result = v.list([
556
+ v.symbol('fn'),
557
+ v.vector(paramSymbols),
464
558
  substitutedBody,
465
559
  ])
466
560
  if (closingEnd !== undefined) {
@@ -500,7 +594,7 @@ const readRegex = (ctx: ReaderCtx): CljValue => {
500
594
  }
501
595
  scanner.advance()
502
596
  const { pattern, flags } = extractInlineFlags(token.value)
503
- const val = cljRegex(pattern, flags)
597
+ const val = v.regex(pattern, flags)
504
598
  setPos(val, { start: token.start.offset, end: token.end.offset })
505
599
  return val
506
600
  }
@@ -533,10 +627,14 @@ function readForm(ctx: ReaderCtx): CljValue {
533
627
  return readUnquoteSplicing(ctx)
534
628
  case tokenKeywords.AnonFnStart:
535
629
  return readAnonFn(ctx)
630
+ case tokenKeywords.SetStart:
631
+ return readSet(ctx)
536
632
  case tokenKeywords.Deref:
537
633
  return readDeref(ctx)
538
634
  case tokenKeywords.VarQuote:
539
635
  return readVarQuote(ctx)
636
+ case tokenKeywords.Meta:
637
+ return readMeta(ctx)
540
638
  case tokenKeywords.Regex:
541
639
  return readRegex(ctx)
542
640
  default: