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.
- package/dist-cli/conjure-js.mjs +9336 -5028
- package/dist-vite-plugin/index.mjs +10455 -0
- package/package.json +9 -2
- package/src/bin/cli.ts +2 -2
- package/src/bin/nrepl-symbol.ts +150 -0
- package/src/bin/nrepl.ts +301 -157
- package/src/bin/version.ts +1 -1
- package/src/clojure/core.clj +764 -29
- package/src/clojure/core.clj.d.ts +76 -4
- package/src/clojure/demo/math.clj +5 -1
- package/src/clojure/generated/builtin-namespace-registry.ts +4 -0
- package/src/clojure/generated/clojure-core-source.ts +765 -29
- package/src/clojure/generated/clojure-set-source.ts +136 -0
- package/src/clojure/generated/clojure-walk-source.ts +72 -0
- package/src/clojure/set.clj +132 -0
- package/src/clojure/set.clj.d.ts +20 -0
- package/src/clojure/string.clj.d.ts +14 -0
- package/src/clojure/walk.clj +68 -0
- package/src/clojure/walk.clj.d.ts +7 -0
- package/src/core/assertions.ts +114 -6
- package/src/core/bootstrap.ts +337 -0
- package/src/core/conversions.ts +48 -31
- package/src/core/core-module.ts +303 -0
- package/src/core/env.ts +42 -7
- package/src/core/errors.ts +8 -0
- package/src/core/evaluator/apply.ts +40 -25
- package/src/core/evaluator/arity.ts +8 -8
- package/src/core/evaluator/async-evaluator.ts +565 -0
- package/src/core/evaluator/collections.ts +30 -4
- package/src/core/evaluator/destructure.ts +180 -69
- package/src/core/evaluator/dispatch.ts +24 -14
- package/src/core/evaluator/evaluate.ts +22 -20
- package/src/core/evaluator/expand.ts +45 -15
- package/src/core/evaluator/form-parsers.ts +178 -0
- package/src/core/evaluator/index.ts +7 -9
- package/src/core/evaluator/js-interop.ts +189 -0
- package/src/core/evaluator/quasiquote.ts +14 -8
- package/src/core/evaluator/recur-check.ts +6 -6
- package/src/core/evaluator/special-forms.ts +380 -173
- package/src/core/factories.ts +182 -3
- package/src/core/index.ts +55 -5
- package/src/core/module.ts +136 -0
- package/src/core/ns-forms.ts +107 -0
- package/src/core/positions.ts +9 -2
- package/src/core/printer.ts +371 -11
- package/src/core/reader.ts +127 -29
- package/src/core/registry.ts +209 -0
- package/src/core/runtime.ts +376 -0
- package/src/core/session.ts +263 -478
- package/src/core/stdlib/arithmetic.ts +516 -215
- package/src/core/stdlib/async-fns.ts +132 -0
- package/src/core/stdlib/atoms.ts +286 -63
- package/src/core/stdlib/errors.ts +54 -50
- package/src/core/stdlib/hof.ts +74 -173
- package/src/core/stdlib/js-namespace.ts +344 -0
- package/src/core/stdlib/lazy.ts +34 -0
- package/src/core/stdlib/maps-sets.ts +322 -0
- package/src/core/stdlib/meta.ts +109 -28
- package/src/core/stdlib/predicates.ts +322 -196
- package/src/core/stdlib/regex.ts +126 -98
- package/src/core/stdlib/seq.ts +564 -0
- package/src/core/stdlib/strings.ts +164 -135
- package/src/core/stdlib/transducers.ts +95 -100
- package/src/core/stdlib/utils.ts +283 -147
- package/src/core/stdlib/vars.ts +27 -27
- package/src/core/stdlib/vectors.ts +122 -0
- package/src/core/tokenizer.ts +13 -3
- package/src/core/transformations.ts +117 -9
- package/src/core/types.ts +118 -6
- package/src/host/node-host-module.ts +74 -0
- package/src/nrepl/relay.ts +432 -0
- package/src/vite-plugin-clj/codegen.ts +87 -95
- package/src/vite-plugin-clj/index.ts +242 -18
- package/src/vite-plugin-clj/namespace-utils.ts +39 -0
- package/src/vite-plugin-clj/static-analysis.ts +211 -0
- package/src/clojure/demo.clj +0 -63
- package/src/clojure/demo.clj.d.ts +0 -0
- package/src/core/core-env.ts +0 -60
- package/src/core/stdlib/collections.ts +0 -784
- package/src/host/node.ts +0 -55
package/src/core/printer.ts
CHANGED
|
@@ -1,7 +1,116 @@
|
|
|
1
1
|
import { EvaluationError } from './errors'
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
+
}
|
package/src/core/reader.ts
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
import { ReaderError } from './errors'
|
|
2
|
-
import {
|
|
3
|
-
|
|
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: [
|
|
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: [
|
|
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: [
|
|
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
|
-
|
|
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
|
|
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: [
|
|
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: [
|
|
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 =
|
|
348
|
+
val = v.boolean(token.value === 'true')
|
|
255
349
|
break
|
|
256
350
|
case 'nil':
|
|
257
|
-
val =
|
|
351
|
+
val = v.nil()
|
|
258
352
|
break
|
|
259
353
|
default:
|
|
260
|
-
val =
|
|
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
|
|
375
|
-
if (/^%[2-9]$/.test(name)) return
|
|
376
|
-
if (name === '%&') return
|
|
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(
|
|
546
|
+
paramSymbols.push(v.symbol(`p${i}`))
|
|
453
547
|
}
|
|
454
548
|
if (hasRest) {
|
|
455
|
-
paramSymbols.push(
|
|
456
|
-
paramSymbols.push(
|
|
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 =
|
|
462
|
-
|
|
463
|
-
|
|
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 =
|
|
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:
|