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.
- package/dist-cli/conjure-js.mjs +9360 -5298
- package/dist-vite-plugin/index.mjs +9463 -5185
- package/package.json +3 -1
- package/src/bin/cli.ts +2 -2
- package/src/bin/nrepl-symbol.ts +150 -0
- package/src/bin/nrepl.ts +289 -167
- package/src/bin/version.ts +1 -1
- package/src/clojure/core.clj +757 -29
- package/src/clojure/core.clj.d.ts +75 -131
- package/src/clojure/generated/builtin-namespace-registry.ts +4 -0
- package/src/clojure/generated/clojure-core-source.ts +758 -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 +20 -6
- 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 +28 -5
- package/src/core/evaluator/destructure.ts +180 -69
- package/src/core/evaluator/dispatch.ts +12 -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 +234 -191
- package/src/core/factories.ts +182 -3
- package/src/core/index.ts +54 -4
- package/src/core/module.ts +136 -0
- package/src/core/ns-forms.ts +107 -0
- package/src/core/printer.ts +371 -11
- package/src/core/reader.ts +84 -33
- package/src/core/registry.ts +209 -0
- package/src/core/runtime.ts +376 -0
- package/src/core/session.ts +253 -487
- package/src/core/stdlib/arithmetic.ts +528 -194
- package/src/core/stdlib/async-fns.ts +132 -0
- package/src/core/stdlib/atoms.ts +291 -56
- package/src/core/stdlib/errors.ts +54 -50
- package/src/core/stdlib/hof.ts +82 -166
- 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 +61 -30
- package/src/core/stdlib/predicates.ts +325 -187
- 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 +292 -130
- package/src/core/stdlib/vars.ts +27 -27
- package/src/core/stdlib/vectors.ts +122 -0
- package/src/core/tokenizer.ts +2 -2
- package/src/core/transformations.ts +117 -9
- package/src/core/types.ts +98 -2
- package/src/host/node-host-module.ts +74 -0
- package/src/{vite-plugin-clj/nrepl-relay.ts → nrepl/relay.ts} +72 -11
- package/src/vite-plugin-clj/codegen.ts +87 -95
- package/src/vite-plugin-clj/index.ts +178 -23
- 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 -72
- package/src/clojure/demo.clj.d.ts +0 -0
- package/src/core/core-env.ts +0 -61
- package/src/core/stdlib/collections.ts +0 -739
- 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,14 +1,6 @@
|
|
|
1
1
|
import { ReaderError } from './errors'
|
|
2
|
-
import {
|
|
3
|
-
|
|
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: [
|
|
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: [
|
|
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: [
|
|
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,
|
|
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 = [[
|
|
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 = {
|
|
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
|
-
|
|
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
|
|
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: [
|
|
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: [
|
|
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 =
|
|
348
|
+
val = v.boolean(token.value === 'true')
|
|
300
349
|
break
|
|
301
350
|
case 'nil':
|
|
302
|
-
val =
|
|
351
|
+
val = v.nil()
|
|
303
352
|
break
|
|
304
353
|
default:
|
|
305
|
-
val =
|
|
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
|
|
420
|
-
if (/^%[2-9]$/.test(name)) return
|
|
421
|
-
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')
|
|
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(
|
|
546
|
+
paramSymbols.push(v.symbol(`p${i}`))
|
|
498
547
|
}
|
|
499
548
|
if (hasRest) {
|
|
500
|
-
paramSymbols.push(
|
|
501
|
-
paramSymbols.push(
|
|
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 =
|
|
507
|
-
|
|
508
|
-
|
|
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 =
|
|
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:
|