conjure-js 0.0.1

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/conjure +0 -0
  2. package/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
  3. package/dist/assets/editor.worker-CdQrwHl8.js +26 -0
  4. package/dist/assets/main-A7ZMId9A.css +1 -0
  5. package/dist/assets/main-CmI-7epE.js +3137 -0
  6. package/dist/index.html +195 -0
  7. package/dist/vite.svg +1 -0
  8. package/package.json +68 -0
  9. package/src/bin/__fixtures__/smoke/app/lib.clj +4 -0
  10. package/src/bin/__fixtures__/smoke/app/main.clj +4 -0
  11. package/src/bin/__fixtures__/smoke/repl-smoke.ts +12 -0
  12. package/src/bin/bencode.ts +205 -0
  13. package/src/bin/cli.ts +250 -0
  14. package/src/bin/nrepl-utils.ts +59 -0
  15. package/src/bin/nrepl.ts +393 -0
  16. package/src/bin/version.ts +4 -0
  17. package/src/clojure/core.clj +620 -0
  18. package/src/clojure/core.clj.d.ts +189 -0
  19. package/src/clojure/demo/math.clj +16 -0
  20. package/src/clojure/demo/math.clj.d.ts +4 -0
  21. package/src/clojure/demo.clj +42 -0
  22. package/src/clojure/demo.clj.d.ts +0 -0
  23. package/src/clojure/generated/builtin-namespace-registry.ts +14 -0
  24. package/src/clojure/generated/clojure-core-source.ts +623 -0
  25. package/src/clojure/generated/clojure-string-source.ts +196 -0
  26. package/src/clojure/string.clj +192 -0
  27. package/src/clojure/string.clj.d.ts +25 -0
  28. package/src/core/assertions.ts +134 -0
  29. package/src/core/conversions.ts +108 -0
  30. package/src/core/core-env.ts +58 -0
  31. package/src/core/env.ts +78 -0
  32. package/src/core/errors.ts +39 -0
  33. package/src/core/evaluator/apply.ts +114 -0
  34. package/src/core/evaluator/arity.ts +174 -0
  35. package/src/core/evaluator/collections.ts +25 -0
  36. package/src/core/evaluator/destructure.ts +247 -0
  37. package/src/core/evaluator/dispatch.ts +73 -0
  38. package/src/core/evaluator/evaluate.ts +100 -0
  39. package/src/core/evaluator/expand.ts +79 -0
  40. package/src/core/evaluator/index.ts +72 -0
  41. package/src/core/evaluator/quasiquote.ts +87 -0
  42. package/src/core/evaluator/recur-check.ts +109 -0
  43. package/src/core/evaluator/special-forms.ts +517 -0
  44. package/src/core/factories.ts +155 -0
  45. package/src/core/gensym.ts +9 -0
  46. package/src/core/index.ts +76 -0
  47. package/src/core/positions.ts +38 -0
  48. package/src/core/printer.ts +86 -0
  49. package/src/core/reader.ts +559 -0
  50. package/src/core/scanners.ts +93 -0
  51. package/src/core/session.ts +610 -0
  52. package/src/core/stdlib/arithmetic.ts +361 -0
  53. package/src/core/stdlib/atoms.ts +88 -0
  54. package/src/core/stdlib/collections.ts +784 -0
  55. package/src/core/stdlib/errors.ts +81 -0
  56. package/src/core/stdlib/hof.ts +307 -0
  57. package/src/core/stdlib/meta.ts +48 -0
  58. package/src/core/stdlib/predicates.ts +240 -0
  59. package/src/core/stdlib/regex.ts +238 -0
  60. package/src/core/stdlib/strings.ts +311 -0
  61. package/src/core/stdlib/transducers.ts +256 -0
  62. package/src/core/stdlib/utils.ts +287 -0
  63. package/src/core/tokenizer.ts +437 -0
  64. package/src/core/transformations.ts +75 -0
  65. package/src/core/types.ts +258 -0
  66. package/src/main.ts +1 -0
  67. package/src/monaco-esm.d.ts +7 -0
  68. package/src/playground/clojure-tokens.ts +67 -0
  69. package/src/playground/editor.worker.ts +5 -0
  70. package/src/playground/find-form.ts +138 -0
  71. package/src/playground/playground.ts +342 -0
  72. package/src/playground/samples/00-welcome.clj +385 -0
  73. package/src/playground/samples/01-collections.clj +191 -0
  74. package/src/playground/samples/02-higher-order-functions.clj +215 -0
  75. package/src/playground/samples/03-destructuring.clj +194 -0
  76. package/src/playground/samples/04-strings-and-regex.clj +202 -0
  77. package/src/playground/samples/05-error-handling.clj +212 -0
  78. package/src/repl/repl.ts +116 -0
  79. package/tsconfig.build.json +10 -0
  80. package/tsconfig.json +31 -0
@@ -0,0 +1,610 @@
1
+ import { isKeyword, isList, isSymbol, isVector } from './assertions'
2
+ import { loadCoreFunctions } from './core-env'
3
+ import { define, lookup, makeEnv, tryLookup } from './env'
4
+ import { valueToString } from './transformations'
5
+ import { createEvaluationContext, RecurSignal } from './evaluator'
6
+ import { CljThrownSignal, EvaluationError, ReaderError } from './errors'
7
+ import { cljNativeFunction, cljNil } from './factories'
8
+ import { formatErrorContext } from './positions'
9
+ import { printString } from './printer'
10
+ import { readForms } from './reader'
11
+ import { tokenize } from './tokenizer'
12
+ import type { CljValue, Env, Token, TokenSymbol } from './types'
13
+ import { builtInNamespaceSources } from '../clojure/generated/builtin-namespace-registry'
14
+
15
+ type NamespaceRegistry = Map<string, Env>
16
+
17
+ type SessionOptions = {
18
+ output?: (text: string) => void
19
+ entries?: string[]
20
+ sourceRoots?: string[]
21
+ readFile?: (filePath: string) => string
22
+ }
23
+
24
+ export type Session = {
25
+ registry: NamespaceRegistry
26
+ readonly currentNs: string
27
+ setNs: (namespace: string) => void
28
+ getNs: (namespace: string) => Env | null
29
+ loadFile: (source: string, nsName?: string) => string
30
+ evaluate: (source: string) => CljValue
31
+ evaluateForms: (forms: CljValue[]) => CljValue
32
+ addSourceRoot: (path: string) => void
33
+ getCompletions: (prefix: string, nsName?: string) => string[]
34
+ }
35
+
36
+ // Lightweight token scan to extract the namespace name before full parsing.
37
+ // Looks for the pattern: LParen Symbol("ns") Symbol(name) at the top of the token stream.
38
+ function extractNsNameFromTokens(tokens: Token[]): string | null {
39
+ const meaningful = tokens.filter((t) => t.kind !== 'Comment')
40
+ if (meaningful.length < 3) return null
41
+ if (meaningful[0].kind !== 'LParen') return null
42
+ if (meaningful[1].kind !== 'Symbol' || meaningful[1].value !== 'ns')
43
+ return null
44
+ if (meaningful[2].kind !== 'Symbol') return null
45
+ return meaningful[2].value
46
+ }
47
+
48
+ // Lightweight token scan to extract :as alias pairs from the ns form's :require clauses.
49
+ // Returns Map { 'alias' -> 'full.ns.name' } for all [some.ns :as alias] specs found.
50
+ // This runs before readForms so the reader can expand ::alias/foo at read time.
51
+ function extractAliasMapFromTokens(tokens: Token[]): Map<string, string> {
52
+ const aliases = new Map<string, string>()
53
+ const meaningful = tokens.filter(
54
+ (t) => t.kind !== 'Comment' && t.kind !== 'Whitespace'
55
+ )
56
+ // Must start with (ns ...)
57
+ if (meaningful.length < 3) return aliases
58
+ if (meaningful[0].kind !== 'LParen') return aliases
59
+ if (meaningful[1].kind !== 'Symbol' || meaningful[1].value !== 'ns')
60
+ return aliases
61
+
62
+ // Walk the top-level ns form tracking paren depth.
63
+ // For each [ vector ] we encounter, check for ns-sym :as alias-sym.
64
+ let i = 3 // skip ( ns <name>
65
+ let depth = 1
66
+ while (i < meaningful.length && depth > 0) {
67
+ const tok = meaningful[i]
68
+ if (tok.kind === 'LParen') {
69
+ depth++
70
+ i++
71
+ continue
72
+ }
73
+ if (tok.kind === 'RParen') {
74
+ depth--
75
+ i++
76
+ continue
77
+ }
78
+ if (tok.kind === 'LBracket') {
79
+ // Scan through this vector for: first Symbol (ns name) + :as + Symbol (alias)
80
+ let j = i + 1
81
+ let nsSym: string | null = null
82
+ while (j < meaningful.length && meaningful[j].kind !== 'RBracket') {
83
+ const t = meaningful[j]
84
+ if (t.kind === 'Symbol' && nsSym === null) {
85
+ nsSym = t.value
86
+ }
87
+ if (
88
+ t.kind === 'Keyword' &&
89
+ (t.value === ':as' || t.value === ':as-alias')
90
+ ) {
91
+ j++
92
+ if (
93
+ j < meaningful.length &&
94
+ meaningful[j].kind === 'Symbol' &&
95
+ nsSym
96
+ ) {
97
+ aliases.set((meaningful[j] as TokenSymbol).value, nsSym)
98
+ }
99
+ }
100
+ j++
101
+ }
102
+ }
103
+ i++
104
+ }
105
+ return aliases
106
+ }
107
+
108
+ function findNsForm(forms: CljValue[]) {
109
+ const nsForm = forms.find(
110
+ (f) => isList(f) && isSymbol(f.value[0]) && f.value[0].name === 'ns'
111
+ )
112
+ if (!nsForm || !isList(nsForm)) return null
113
+ return nsForm
114
+ }
115
+
116
+ function extractRequireClauses(forms: CljValue[]): CljValue[][] {
117
+ const nsForm = findNsForm(forms)
118
+ if (!nsForm) return []
119
+ const clauses: CljValue[][] = []
120
+ for (let i = 2; i < nsForm.value.length; i++) {
121
+ const clause = nsForm.value[i]
122
+ if (
123
+ isList(clause) &&
124
+ isKeyword(clause.value[0]) &&
125
+ clause.value[0].name === ':require'
126
+ ) {
127
+ clauses.push(clause.value.slice(1))
128
+ }
129
+ }
130
+ return clauses
131
+ }
132
+
133
+ function processRequireSpec(
134
+ spec: CljValue,
135
+ currentEnv: Env,
136
+ registry: NamespaceRegistry,
137
+ resolveNamespace?: (nsName: string) => boolean
138
+ ): void {
139
+ if (!isVector(spec)) {
140
+ throw new EvaluationError(
141
+ 'require spec must be a vector, e.g. [my.ns :as alias]',
142
+ { spec }
143
+ )
144
+ }
145
+
146
+ const elements = spec.value
147
+ if (elements.length === 0 || !isSymbol(elements[0])) {
148
+ throw new EvaluationError(
149
+ 'First element of require spec must be a namespace symbol',
150
+ { spec }
151
+ )
152
+ }
153
+
154
+ const nsName = elements[0].name
155
+
156
+ // :as-alias creates a reader alias without loading the namespace.
157
+ // The namespace need not exist — the alias is only used for ::alias/foo expansion.
158
+ const hasAsAlias = elements.some(
159
+ (el) => isKeyword(el) && el.name === ':as-alias'
160
+ )
161
+ if (hasAsAlias) {
162
+ let i = 1
163
+ while (i < elements.length) {
164
+ const kw = elements[i]
165
+ if (!isKeyword(kw)) {
166
+ throw new EvaluationError(
167
+ `Expected keyword in require spec, got ${kw.kind}`,
168
+ { spec, position: i }
169
+ )
170
+ }
171
+ if (kw.name === ':as-alias') {
172
+ i++
173
+ const alias = elements[i]
174
+ if (!alias || !isSymbol(alias)) {
175
+ throw new EvaluationError(':as-alias expects a symbol alias', {
176
+ spec,
177
+ position: i,
178
+ })
179
+ }
180
+ if (!currentEnv.readerAliases) {
181
+ currentEnv.readerAliases = new Map()
182
+ }
183
+ currentEnv.readerAliases.set(alias.name, nsName)
184
+ i++
185
+ } else {
186
+ throw new EvaluationError(
187
+ `:as-alias specs only support :as-alias, got ${kw.name}`,
188
+ { spec }
189
+ )
190
+ }
191
+ }
192
+ return
193
+ }
194
+
195
+ let targetEnv = registry.get(nsName)
196
+ if (!targetEnv && resolveNamespace) {
197
+ resolveNamespace(nsName)
198
+ targetEnv = registry.get(nsName)
199
+ }
200
+ if (!targetEnv) {
201
+ throw new EvaluationError(
202
+ `Namespace ${nsName} not found. Only already-loaded namespaces can be required.`,
203
+ { nsName }
204
+ )
205
+ }
206
+
207
+ let i = 1
208
+ while (i < elements.length) {
209
+ const kw = elements[i]
210
+ if (!isKeyword(kw)) {
211
+ throw new EvaluationError(
212
+ `Expected keyword in require spec, got ${kw.kind}`,
213
+ { spec, position: i }
214
+ )
215
+ }
216
+
217
+ if (kw.name === ':as') {
218
+ i++
219
+ const alias = elements[i]
220
+ if (!alias || !isSymbol(alias)) {
221
+ throw new EvaluationError(':as expects a symbol alias', {
222
+ spec,
223
+ position: i,
224
+ })
225
+ }
226
+ if (!currentEnv.aliases) {
227
+ currentEnv.aliases = new Map()
228
+ }
229
+ currentEnv.aliases.set(alias.name, targetEnv)
230
+ i++
231
+ } else if (kw.name === ':refer') {
232
+ i++
233
+ const symsVec = elements[i]
234
+ if (!symsVec || !isVector(symsVec)) {
235
+ throw new EvaluationError(':refer expects a vector of symbols', {
236
+ spec,
237
+ position: i,
238
+ })
239
+ }
240
+ for (const sym of symsVec.value) {
241
+ if (!isSymbol(sym)) {
242
+ throw new EvaluationError(':refer vector must contain only symbols', {
243
+ spec,
244
+ sym,
245
+ })
246
+ }
247
+ let value: CljValue
248
+ try {
249
+ value = lookup(sym.name, targetEnv)
250
+ } catch {
251
+ throw new EvaluationError(
252
+ `Symbol ${sym.name} not found in namespace ${nsName}`,
253
+ { nsName, symbol: sym.name }
254
+ )
255
+ }
256
+ define(sym.name, value, currentEnv)
257
+ }
258
+ i++
259
+ } else {
260
+ throw new EvaluationError(
261
+ `Unknown require option ${kw.name}. Supported: :as, :refer`,
262
+ { spec, keyword: kw.name }
263
+ )
264
+ }
265
+ }
266
+ }
267
+
268
+ // ---------------------------------------------------------------------------
269
+ // Clone helpers — used by snapshotSession / createSessionFromSnapshot
270
+ // ---------------------------------------------------------------------------
271
+
272
+ function cloneEnv(env: Env, memo: Map<Env, Env>): Env {
273
+ if (memo.has(env)) return memo.get(env)!
274
+ const cloned: Env = {
275
+ bindings: new Map(env.bindings),
276
+ outer: null,
277
+ namespace: env.namespace,
278
+ }
279
+ memo.set(env, cloned)
280
+ if (env.outer) cloned.outer = cloneEnv(env.outer, memo)
281
+ if (env.aliases)
282
+ cloned.aliases = new Map(
283
+ [...env.aliases].map(([k, v]) => [k, cloneEnv(v, memo)])
284
+ )
285
+ if (env.readerAliases) cloned.readerAliases = new Map(env.readerAliases)
286
+ return cloned
287
+ }
288
+
289
+ function cloneRegistry(registry: NamespaceRegistry): NamespaceRegistry {
290
+ const memo = new Map<Env, Env>()
291
+ const next = new Map<string, Env>()
292
+ for (const [name, env] of registry) next.set(name, cloneEnv(env, memo))
293
+ return next
294
+ }
295
+
296
+ // ---------------------------------------------------------------------------
297
+ // SessionState — the minimal data a session operates on
298
+ // ---------------------------------------------------------------------------
299
+
300
+ type SessionState = {
301
+ registry: NamespaceRegistry
302
+ currentNs: string
303
+ }
304
+
305
+ // ---------------------------------------------------------------------------
306
+ // buildSessionApi — single source of truth for all session behavior.
307
+ // Accepts a pre-populated registry (fresh or cloned) and wires up all
308
+ // closures + the public API. Always re-wires resolveNs and require since
309
+ // both must close over the specific registry instance they receive.
310
+ // ---------------------------------------------------------------------------
311
+
312
+ function buildSessionApi(
313
+ state: SessionState,
314
+ options?: SessionOptions
315
+ ): Session {
316
+ const registry = state.registry
317
+ let currentNs = state.currentNs
318
+
319
+ const coreEnv = registry.get('clojure.core')!
320
+ coreEnv.resolveNs = (name: string) => registry.get(name) ?? null
321
+
322
+ // Always re-wire print/println so snapshot-derived sessions get the right
323
+ // emit target. Falls back to console.log when no output callback is provided.
324
+ const emitFn = options?.output ?? ((text: string) => console.log(text))
325
+ define(
326
+ 'println',
327
+ cljNativeFunction('println', (...args: CljValue[]) => {
328
+ emitFn(args.map(valueToString).join(' '))
329
+ return cljNil()
330
+ }),
331
+ coreEnv
332
+ )
333
+ define(
334
+ 'print',
335
+ cljNativeFunction('print', (...args: CljValue[]) => {
336
+ emitFn(args.map(valueToString).join(' '))
337
+ return cljNil()
338
+ }),
339
+ coreEnv
340
+ )
341
+
342
+ // Mutable source roots — seeded from options, growable via addSourceRoot.
343
+ const sourceRoots = new Set<string>(options?.sourceRoots ?? [])
344
+
345
+ function addSourceRoot(path: string) {
346
+ sourceRoots.add(path)
347
+ }
348
+
349
+ // One shared evaluation context for the lifetime of this session.
350
+ const ctx = createEvaluationContext()
351
+
352
+ function resolveNamespace(nsName: string): boolean {
353
+ const builtInLoader = builtInNamespaceSources[nsName]
354
+ if (builtInLoader) {
355
+ loadFile(builtInLoader(), nsName)
356
+ return true
357
+ }
358
+ if (!options?.readFile || sourceRoots.size === 0) {
359
+ return false
360
+ }
361
+ for (const root of sourceRoots) {
362
+ const filePath = `${root.replace(/\/$/, '')}/${nsName.replace(/\./g, '/')}.clj`
363
+ try {
364
+ const source = options.readFile(filePath)
365
+ if (source) {
366
+ loadFile(source)
367
+ return true
368
+ }
369
+ } catch {
370
+ continue
371
+ }
372
+ }
373
+ return false
374
+ }
375
+
376
+ function ensureNs(name: string): Env {
377
+ if (!registry.has(name)) {
378
+ const nsEnv = makeEnv(coreEnv)
379
+ nsEnv.namespace = name
380
+ registry.set(name, nsEnv)
381
+ }
382
+ return registry.get(name)!
383
+ }
384
+
385
+ function setNs(name: string) {
386
+ ensureNs(name)
387
+ currentNs = name
388
+ }
389
+
390
+ function getNs(name: string): Env | null {
391
+ return registry.get(name) ?? null
392
+ }
393
+
394
+ define(
395
+ 'require',
396
+ cljNativeFunction('require', (...args: CljValue[]) => {
397
+ const currentEnv = registry.get(currentNs)!
398
+ for (const arg of args) {
399
+ processRequireSpec(arg, currentEnv, registry, resolveNamespace)
400
+ }
401
+ return cljNil()
402
+ }),
403
+ coreEnv
404
+ )
405
+
406
+ define(
407
+ 'resolve',
408
+ cljNativeFunction('resolve', (sym: CljValue) => {
409
+ if (!isSymbol(sym)) return cljNil()
410
+ const slashIdx = sym.name.indexOf('/')
411
+ if (slashIdx > 0) {
412
+ const nsName = sym.name.slice(0, slashIdx)
413
+ const symName = sym.name.slice(slashIdx + 1)
414
+ const nsEnv = registry.get(nsName) ?? null
415
+ if (!nsEnv) return cljNil()
416
+ return tryLookup(symName, nsEnv) ?? cljNil()
417
+ }
418
+ const currentEnv = registry.get(currentNs)!
419
+ return tryLookup(sym.name, currentEnv) ?? cljNil()
420
+ }),
421
+ coreEnv
422
+ )
423
+
424
+ function processNsRequires(forms: CljValue[], env: Env) {
425
+ const requireClauses = extractRequireClauses(forms)
426
+ for (const specs of requireClauses) {
427
+ for (const spec of specs) {
428
+ processRequireSpec(spec, env, registry, resolveNamespace)
429
+ }
430
+ }
431
+ }
432
+
433
+ function loadFile(source: string, nsName?: string): string {
434
+ const tokens = tokenize(source)
435
+ const targetNs = extractNsNameFromTokens(tokens) ?? nsName ?? 'user'
436
+ const aliasMap = extractAliasMapFromTokens(tokens)
437
+ const forms = readForms(tokens, targetNs, aliasMap)
438
+ const env = ensureNs(targetNs)
439
+ processNsRequires(forms, env)
440
+ for (const form of forms) {
441
+ const expanded = ctx.expandAll(form, env)
442
+ ctx.evaluate(expanded, env)
443
+ }
444
+ return targetNs
445
+ }
446
+
447
+ const api: Session = {
448
+ registry,
449
+ get currentNs() {
450
+ return currentNs
451
+ },
452
+ setNs,
453
+ getNs,
454
+ loadFile,
455
+ addSourceRoot,
456
+ evaluate(source: string) {
457
+ try {
458
+ const tokens = tokenize(source)
459
+ const env = getNs(currentNs)!
460
+ // Seed alias map from tokens (new aliases declared in this source) and
461
+ // from env.aliases/:as (prior require calls) and env.readerAliases/:as-alias.
462
+ const aliasMap = extractAliasMapFromTokens(tokens)
463
+ env.aliases?.forEach((nsEnv, alias) => {
464
+ if (nsEnv.namespace) aliasMap.set(alias, nsEnv.namespace)
465
+ })
466
+ env.readerAliases?.forEach((nsName, alias) => {
467
+ aliasMap.set(alias, nsName)
468
+ })
469
+ const forms = readForms(tokens, currentNs, aliasMap)
470
+ processNsRequires(forms, env)
471
+ let result: CljValue = cljNil()
472
+ for (const form of forms) {
473
+ const expanded = ctx.expandAll(form, env)
474
+ result = ctx.evaluate(expanded, env)
475
+ }
476
+ return result
477
+ } catch (e) {
478
+ if (e instanceof CljThrownSignal) {
479
+ throw new EvaluationError(
480
+ `Unhandled throw: ${printString(e.value)}`,
481
+ { thrownValue: e.value }
482
+ )
483
+ }
484
+ if (e instanceof RecurSignal) {
485
+ throw new EvaluationError('recur called outside of loop or fn', {
486
+ args: e.args,
487
+ })
488
+ }
489
+ if (
490
+ (e instanceof EvaluationError || e instanceof ReaderError) &&
491
+ e.pos
492
+ ) {
493
+ e.message += formatErrorContext(source, e.pos)
494
+ }
495
+ throw e
496
+ }
497
+ },
498
+ evaluateForms(forms: CljValue[]) {
499
+ try {
500
+ const env = getNs(currentNs)!
501
+ let result: CljValue = cljNil()
502
+ for (const form of forms) {
503
+ const expanded = ctx.expandAll(form, env)
504
+ result = ctx.evaluate(expanded, env)
505
+ }
506
+ return result
507
+ } catch (e) {
508
+ if (e instanceof CljThrownSignal) {
509
+ throw new EvaluationError(
510
+ `Unhandled throw: ${printString(e.value)}`,
511
+ { thrownValue: e.value }
512
+ )
513
+ }
514
+ if (e instanceof RecurSignal) {
515
+ throw new EvaluationError('recur called outside of loop or fn', {
516
+ args: e.args,
517
+ })
518
+ }
519
+ throw e
520
+ }
521
+ },
522
+ getCompletions(prefix: string, nsName?: string): string[] {
523
+ let env: Env | null = registry.get(nsName ?? currentNs) ?? null
524
+ const seen = new Set<string>()
525
+ while (env) {
526
+ for (const key of env.bindings.keys()) seen.add(key)
527
+ env = env.outer
528
+ }
529
+ const candidates = [...seen]
530
+ if (!prefix) return candidates.sort()
531
+ return candidates.filter((k) => k.startsWith(prefix)).sort()
532
+ },
533
+ }
534
+ return api
535
+ }
536
+
537
+ // ---------------------------------------------------------------------------
538
+ // Public API
539
+ // ---------------------------------------------------------------------------
540
+
541
+ export function createSession(options?: SessionOptions): Session {
542
+ const registry: NamespaceRegistry = new Map()
543
+
544
+ const coreEnv = makeEnv()
545
+ coreEnv.namespace = 'clojure.core'
546
+ loadCoreFunctions(coreEnv, options?.output)
547
+ registry.set('clojure.core', coreEnv)
548
+
549
+ const userEnv = makeEnv(coreEnv)
550
+ userEnv.namespace = 'user'
551
+ registry.set('user', userEnv)
552
+
553
+ const session = buildSessionApi({ registry, currentNs: 'user' }, options)
554
+
555
+ const coreLoader = builtInNamespaceSources['clojure.core']
556
+ if (!coreLoader) {
557
+ throw new Error('Missing built-in clojure.core source in registry')
558
+ }
559
+ session.loadFile(coreLoader(), 'clojure.core')
560
+
561
+ for (const source of options?.entries ?? []) {
562
+ session.loadFile(source)
563
+ }
564
+
565
+ return session
566
+ }
567
+
568
+ /**
569
+ * A snapshot of a session's registry + current namespace.
570
+ * Produced by snapshotSession; consumed by createSessionFromSnapshot.
571
+ * CljValue objects are shared (they are immutable); only the Env containers
572
+ * are deep-copied so each derived session gets independent namespace state.
573
+ */
574
+ export type SessionSnapshot = {
575
+ registry: NamespaceRegistry
576
+ currentNs: string
577
+ }
578
+
579
+ /**
580
+ * Capture a deep clone of the session's env state.
581
+ * Typically called once after createSession() and before any user code is
582
+ * evaluated, to produce a pristine snapshot that can be cloned cheaply per test.
583
+ */
584
+ export function snapshotSession(session: Session): SessionSnapshot {
585
+ return {
586
+ registry: cloneRegistry(session.registry),
587
+ currentNs: session.currentNs,
588
+ }
589
+ }
590
+
591
+ /**
592
+ * Create a new session from a previously captured snapshot.
593
+ * Skips the core.clj bootstrap — the cloned registry already contains the
594
+ * fully-expanded core environment. Re-wires all registry-dependent closures
595
+ * (resolveNs, require) to the new registry instance.
596
+ */
597
+ export function createSessionFromSnapshot(
598
+ snapshot: SessionSnapshot,
599
+ options?: SessionOptions
600
+ ): Session {
601
+ const registry = cloneRegistry(snapshot.registry)
602
+ const session = buildSessionApi(
603
+ { registry, currentNs: snapshot.currentNs },
604
+ options
605
+ )
606
+ for (const source of options?.entries ?? []) {
607
+ session.loadFile(source)
608
+ }
609
+ return session
610
+ }