conjure-js 0.0.12 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist-cli/conjure-js.mjs +9360 -5298
  2. package/dist-vite-plugin/index.mjs +9463 -5185
  3. package/package.json +3 -1
  4. package/src/bin/cli.ts +2 -2
  5. package/src/bin/nrepl-symbol.ts +150 -0
  6. package/src/bin/nrepl.ts +289 -167
  7. package/src/bin/version.ts +1 -1
  8. package/src/clojure/core.clj +757 -29
  9. package/src/clojure/core.clj.d.ts +75 -131
  10. package/src/clojure/generated/builtin-namespace-registry.ts +4 -0
  11. package/src/clojure/generated/clojure-core-source.ts +758 -29
  12. package/src/clojure/generated/clojure-set-source.ts +136 -0
  13. package/src/clojure/generated/clojure-walk-source.ts +72 -0
  14. package/src/clojure/set.clj +132 -0
  15. package/src/clojure/set.clj.d.ts +20 -0
  16. package/src/clojure/string.clj.d.ts +14 -0
  17. package/src/clojure/walk.clj +68 -0
  18. package/src/clojure/walk.clj.d.ts +7 -0
  19. package/src/core/assertions.ts +114 -6
  20. package/src/core/bootstrap.ts +337 -0
  21. package/src/core/conversions.ts +48 -31
  22. package/src/core/core-module.ts +303 -0
  23. package/src/core/env.ts +20 -6
  24. package/src/core/evaluator/apply.ts +40 -25
  25. package/src/core/evaluator/arity.ts +8 -8
  26. package/src/core/evaluator/async-evaluator.ts +565 -0
  27. package/src/core/evaluator/collections.ts +28 -5
  28. package/src/core/evaluator/destructure.ts +180 -69
  29. package/src/core/evaluator/dispatch.ts +12 -14
  30. package/src/core/evaluator/evaluate.ts +22 -20
  31. package/src/core/evaluator/expand.ts +45 -15
  32. package/src/core/evaluator/form-parsers.ts +178 -0
  33. package/src/core/evaluator/index.ts +7 -9
  34. package/src/core/evaluator/js-interop.ts +189 -0
  35. package/src/core/evaluator/quasiquote.ts +14 -8
  36. package/src/core/evaluator/recur-check.ts +6 -6
  37. package/src/core/evaluator/special-forms.ts +234 -191
  38. package/src/core/factories.ts +182 -3
  39. package/src/core/index.ts +54 -4
  40. package/src/core/module.ts +136 -0
  41. package/src/core/ns-forms.ts +107 -0
  42. package/src/core/printer.ts +371 -11
  43. package/src/core/reader.ts +84 -33
  44. package/src/core/registry.ts +209 -0
  45. package/src/core/runtime.ts +376 -0
  46. package/src/core/session.ts +253 -487
  47. package/src/core/stdlib/arithmetic.ts +528 -194
  48. package/src/core/stdlib/async-fns.ts +132 -0
  49. package/src/core/stdlib/atoms.ts +291 -56
  50. package/src/core/stdlib/errors.ts +54 -50
  51. package/src/core/stdlib/hof.ts +82 -166
  52. package/src/core/stdlib/js-namespace.ts +344 -0
  53. package/src/core/stdlib/lazy.ts +34 -0
  54. package/src/core/stdlib/maps-sets.ts +322 -0
  55. package/src/core/stdlib/meta.ts +61 -30
  56. package/src/core/stdlib/predicates.ts +325 -187
  57. package/src/core/stdlib/regex.ts +126 -98
  58. package/src/core/stdlib/seq.ts +564 -0
  59. package/src/core/stdlib/strings.ts +164 -135
  60. package/src/core/stdlib/transducers.ts +95 -100
  61. package/src/core/stdlib/utils.ts +292 -130
  62. package/src/core/stdlib/vars.ts +27 -27
  63. package/src/core/stdlib/vectors.ts +122 -0
  64. package/src/core/tokenizer.ts +2 -2
  65. package/src/core/transformations.ts +117 -9
  66. package/src/core/types.ts +98 -2
  67. package/src/host/node-host-module.ts +74 -0
  68. package/src/{vite-plugin-clj/nrepl-relay.ts → nrepl/relay.ts} +72 -11
  69. package/src/vite-plugin-clj/codegen.ts +87 -95
  70. package/src/vite-plugin-clj/index.ts +178 -23
  71. package/src/vite-plugin-clj/namespace-utils.ts +39 -0
  72. package/src/vite-plugin-clj/static-analysis.ts +211 -0
  73. package/src/clojure/demo.clj +0 -72
  74. package/src/clojure/demo.clj.d.ts +0 -0
  75. package/src/core/core-env.ts +0 -61
  76. package/src/core/stdlib/collections.ts +0 -739
  77. package/src/host/node.ts +0 -55
@@ -1,508 +1,185 @@
1
- import { isKeyword, isList, isSymbol, isVector } from './assertions'
2
- import { loadCoreFunctions } from './core-env'
3
- import { define, internVar, lookup, lookupVar, makeEnv, makeNamespace, tryLookup } from './env'
4
- import { valueToString } from './transformations'
5
- import { createEvaluationContext, RecurSignal } from './evaluator'
1
+ import { builtInNamespaceSources } from '../clojure/generated/builtin-namespace-registry'
6
2
  import { CljThrownSignal, EvaluationError, ReaderError } from './errors'
7
- import { cljNativeFunction, cljNil } from './factories'
3
+ import { createEvaluationContext, RecurSignal } from './evaluator'
4
+ import { internVar, makeEnv } from './env'
5
+ import { v } from './factories'
6
+ import { jsToClj } from './evaluator/js-interop'
7
+ import type { RuntimeModule } from './module'
8
+ import { cljToJs as _cljToJs } from './conversions'
8
9
  import { formatErrorContext } from './positions'
9
10
  import { printString } from './printer'
10
11
  import { readForms } from './reader'
12
+ import type { Runtime, RuntimeSnapshot } from './runtime'
13
+ import { createRuntime, restoreRuntime } from './runtime'
14
+ import { extractAliasMapFromTokens, extractNsNameFromTokens } from './ns-forms'
11
15
  import { tokenize } from './tokenizer'
12
- import type { CljNamespace, CljValue, Env, Token, TokenSymbol } from './types'
13
- import { builtInNamespaceSources } from '../clojure/generated/builtin-namespace-registry'
16
+ import type { CljNamespace, CljValue, Env } from './types'
14
17
 
15
- type NamespaceRegistry = Map<string, Env>
18
+ // ---------------------------------------------------------------------------
19
+ // Public types
20
+ // ---------------------------------------------------------------------------
16
21
 
17
- type SessionOptions = {
22
+ export type SessionOptions = {
23
+ /** Primary output channel — wired to ctx.io.stdout (println, print, pr, prn, pprint, newline). */
18
24
  output?: (text: string) => void
25
+ /** Secondary error channel — wired to ctx.io.stderr. */
26
+ stderr?: (text: string) => void
19
27
  entries?: string[]
20
28
  sourceRoots?: string[]
21
29
  readFile?: (filePath: string) => string
30
+ modules?: RuntimeModule[]
31
+ /**
32
+ * Ambient JS globals injected into the `js` namespace as CljJsValue vars.
33
+ * Each key becomes accessible as `js/<key>` in Clojure code without any require.
34
+ * Example: `{ Math, console, fetch }` → `js/Math`, `js/console`, `js/fetch`.
35
+ */
36
+ hostBindings?: Record<string, unknown>
37
+ /**
38
+ * Called when (:require ["specifier" :as Alias]) is encountered.
39
+ * Must return (or resolve to) the module object, which is boxed as CljJsValue
40
+ * and bound to Alias in the current namespace.
41
+ * Only usable via evaluateAsync() — string requires are inherently async.
42
+ * Examples:
43
+ * Node/Bun: importModule: (s) => import(s)
44
+ * Vite: importModule: (s) => import(s) // Vite resolves statically at build time
45
+ * Tests: importModule: (s) => fakeModules[s]
46
+ */
47
+ importModule?: (specifier: string) => unknown | Promise<unknown>
22
48
  }
23
49
 
24
50
  export type Session = {
25
- registry: NamespaceRegistry
51
+ /** The underlying runtime — exposed for snapshot access and advanced embedding. */
52
+ readonly runtime: Runtime
53
+ /** Passthrough to runtime.registry. Used by nREPL and tooling for namespace lookup. */
54
+ readonly registry: Runtime['registry']
26
55
  readonly currentNs: string
27
56
  setNs: (namespace: string) => void
28
57
  getNs: (namespace: string) => CljNamespace | null
29
58
  loadFile: (source: string, nsName?: string, filePath?: string) => string
30
- evaluate: (source: string, opts?: { lineOffset?: number; colOffset?: number; file?: string }) => CljValue
59
+ /** Async variant of loadFile handles string requires ((:require ["pkg" :as X])). */
60
+ loadFileAsync: (source: string, nsName?: string, filePath?: string) => Promise<string>
61
+ evaluate: (
62
+ source: string,
63
+ opts?: { lineOffset?: number; colOffset?: number; file?: string }
64
+ ) => CljValue
65
+ evaluateAsync: (
66
+ source: string,
67
+ opts?: { lineOffset?: number; colOffset?: number; file?: string }
68
+ ) => Promise<CljValue>
31
69
  evaluateForms: (forms: CljValue[]) => CljValue
70
+ /**
71
+ * Call a CljFunction or CljNativeFunction using this session's evaluation context.
72
+ * Unlike the bare `applyFunction` export from `core/index`, this resolves namespaces
73
+ * through the session's runtime registry — required for any CLJ code that references
74
+ * qualified symbols like `js/Math` or `:require`-d aliases.
75
+ */
76
+ applyFunction: (fn: CljValue, args: CljValue[]) => CljValue
77
+ /**
78
+ * Convert a CljValue to a plain JS value using this session's evaluation context.
79
+ * CLJ functions are wrapped as JS callbacks that invoke via session.applyFunction,
80
+ * ensuring namespace resolution works for js/Math and other runtime namespaces.
81
+ */
82
+ cljToJs: (value: CljValue) => unknown
32
83
  addSourceRoot: (path: string) => void
33
84
  getCompletions: (prefix: string, nsName?: string) => string[]
34
85
  }
35
86
 
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
- currentEnv.ns!.readerAliases.set(alias.name, nsName)
181
- i++
182
- } else {
183
- throw new EvaluationError(
184
- `:as-alias specs only support :as-alias, got ${kw.name}`,
185
- { spec }
186
- )
187
- }
188
- }
189
- return
190
- }
191
-
192
- let targetEnv = registry.get(nsName)
193
- if (!targetEnv && resolveNamespace) {
194
- resolveNamespace(nsName)
195
- targetEnv = registry.get(nsName)
196
- }
197
- if (!targetEnv) {
198
- throw new EvaluationError(
199
- `Namespace ${nsName} not found. Only already-loaded namespaces can be required.`,
200
- { nsName }
201
- )
202
- }
203
-
204
- let i = 1
205
- while (i < elements.length) {
206
- const kw = elements[i]
207
- if (!isKeyword(kw)) {
208
- throw new EvaluationError(
209
- `Expected keyword in require spec, got ${kw.kind}`,
210
- { spec, position: i }
211
- )
212
- }
213
-
214
- if (kw.name === ':as') {
215
- i++
216
- const alias = elements[i]
217
- if (!alias || !isSymbol(alias)) {
218
- throw new EvaluationError(':as expects a symbol alias', {
219
- spec,
220
- position: i,
221
- })
222
- }
223
- currentEnv.ns!.aliases.set(alias.name, targetEnv.ns!)
224
- i++
225
- } else if (kw.name === ':refer') {
226
- i++
227
- const symsVec = elements[i]
228
- if (!symsVec || !isVector(symsVec)) {
229
- throw new EvaluationError(':refer expects a vector of symbols', {
230
- spec,
231
- position: i,
232
- })
233
- }
234
- for (const sym of symsVec.value) {
235
- if (!isSymbol(sym)) {
236
- throw new EvaluationError(':refer vector must contain only symbols', {
237
- spec,
238
- sym,
239
- })
240
- }
241
- const v = lookupVar(sym.name, targetEnv)
242
- if (v !== undefined) {
243
- currentEnv.ns!.vars.set(sym.name, v)
244
- } else {
245
- let value: CljValue
246
- try {
247
- value = lookup(sym.name, targetEnv)
248
- } catch {
249
- throw new EvaluationError(
250
- `Symbol ${sym.name} not found in namespace ${nsName}`,
251
- { nsName, symbol: sym.name }
252
- )
253
- }
254
- define(sym.name, value, currentEnv)
255
- }
256
- }
257
- i++
258
- } else {
259
- throw new EvaluationError(
260
- `Unknown require option ${kw.name}. Supported: :as, :refer`,
261
- { spec, keyword: kw.name }
262
- )
263
- }
264
- }
265
- }
266
-
267
- // ---------------------------------------------------------------------------
268
- // Clone helpers — used by snapshotSession / createSessionFromSnapshot
269
- // ---------------------------------------------------------------------------
270
-
271
- function cloneBindings(bindings: Map<string, CljValue>): Map<string, CljValue> {
272
- const out = new Map<string, CljValue>()
273
- for (const [k, v] of bindings) {
274
- out.set(k, v.kind === 'var' ? { ...v } : v)
275
- }
276
- return out
277
- }
278
-
279
- function cloneEnv(env: Env, memo: Map<Env, Env>): Env {
280
- if (memo.has(env)) return memo.get(env)!
281
- const cloned: Env = {
282
- bindings: cloneBindings(env.bindings),
283
- outer: null,
284
- }
285
- if (env.ns) {
286
- cloned.ns = {
287
- name: env.ns.name,
288
- vars: new Map([...env.ns.vars].map(([k, v]) => [k, { ...v }])),
289
- aliases: new Map(), // wired in cloneRegistry pass 2
290
- readerAliases: new Map(env.ns.readerAliases),
291
- }
292
- }
293
- memo.set(env, cloned)
294
- if (env.outer) cloned.outer = cloneEnv(env.outer, memo)
295
- return cloned
296
- }
297
-
298
- function cloneRegistry(registry: NamespaceRegistry): NamespaceRegistry {
299
- const memo = new Map<Env, Env>()
300
- const next = new Map<string, Env>()
301
- // Pass 1: clone all envs (ns.aliases left empty)
302
- for (const [name, env] of registry) next.set(name, cloneEnv(env, memo))
303
- // Pass 2: wire ns.aliases to the cloned CljNamespace objects
304
- for (const [name, env] of registry) {
305
- const clonedEnv = next.get(name)!
306
- if (env.ns && clonedEnv.ns) {
307
- for (const [alias, origNs] of env.ns.aliases) {
308
- const targetCloned = next.get(origNs.name)
309
- if (targetCloned?.ns) clonedEnv.ns.aliases.set(alias, targetCloned.ns)
310
- }
311
- }
312
- }
313
- return next
314
- }
315
-
316
- // ---------------------------------------------------------------------------
317
- // SessionState — the minimal data a session operates on
318
- // ---------------------------------------------------------------------------
319
-
320
- type SessionState = {
321
- registry: NamespaceRegistry
87
+ export type SessionSnapshot = {
88
+ runtimeSnapshot: RuntimeSnapshot
322
89
  currentNs: string
323
90
  }
324
91
 
325
92
  // ---------------------------------------------------------------------------
326
- // buildSessionApisingle source of truth for all session behavior.
327
- // Accepts a pre-populated registry (fresh or cloned) and wires up all
328
- // closures + the public API. Always re-wires resolveNs and require since
329
- // both must close over the specific registry instance they receive.
93
+ // buildSessionFacadethin evaluation facade over a Runtime.
94
+ // All namespace mechanics are delegated to the runtime.
330
95
  // ---------------------------------------------------------------------------
331
96
 
332
- function buildSessionApi(
333
- state: SessionState,
97
+ function buildSessionFacade(
98
+ runtime: Runtime,
99
+ initialNs: string,
334
100
  options?: SessionOptions
335
101
  ): Session {
336
- const registry = state.registry
337
- let currentNs = state.currentNs
338
-
339
- const coreEnv = registry.get('clojure.core')!
340
- coreEnv.resolveNs = (name: string) => registry.get(name) ?? null
341
-
342
- // Always re-wire print/println so snapshot-derived sessions get the right
343
- // emit target. Falls back to console.log when no output callback is provided.
344
- const emitFn = options?.output ?? ((text: string) => console.log(text))
345
- internVar(
346
- 'println',
347
- cljNativeFunction('println', (...args: CljValue[]) => {
348
- emitFn(args.map(valueToString).join(' '))
349
- return cljNil()
350
- }),
351
- coreEnv
352
- )
353
- internVar(
354
- 'print',
355
- cljNativeFunction('print', (...args: CljValue[]) => {
356
- emitFn(args.map(valueToString).join(' '))
357
- return cljNil()
358
- }),
359
- coreEnv
360
- )
361
-
362
- // Mutable source roots — seeded from options, growable via addSourceRoot.
363
- const sourceRoots = new Set<string>(options?.sourceRoots ?? [])
364
-
365
- function addSourceRoot(path: string) {
366
- sourceRoots.add(path)
367
- }
102
+ let currentNs = initialNs
368
103
 
369
104
  // One shared evaluation context for the lifetime of this session.
370
105
  const ctx = createEvaluationContext()
371
-
372
- function resolveNamespace(nsName: string): boolean {
373
- const builtInLoader = builtInNamespaceSources[nsName]
374
- if (builtInLoader) {
375
- loadFile(builtInLoader(), nsName)
376
- return true
377
- }
378
- if (!options?.readFile || sourceRoots.size === 0) {
379
- return false
380
- }
381
- for (const root of sourceRoots) {
382
- const filePath = `${root.replace(/\/$/, '')}/${nsName.replace(/\./g, '/')}.clj`
383
- try {
384
- const source = options.readFile(filePath)
385
- if (source) {
386
- loadFile(source)
387
- return true
388
- }
389
- } catch {
390
- continue
391
- }
392
- }
393
- return false
394
- }
395
-
396
- function ensureNs(name: string): Env {
397
- if (!registry.has(name)) {
398
- const nsEnv = makeEnv(coreEnv)
399
- nsEnv.ns = makeNamespace(name)
400
- registry.set(name, nsEnv)
401
- }
402
- return registry.get(name)!
106
+ ctx.resolveNs = (name: string) => runtime.getNs(name)
107
+ ctx.io = {
108
+ stdout: options?.output ?? ((text) => console.log(text)),
109
+ stderr: options?.stderr ?? ((text) => console.error(text)),
403
110
  }
404
-
405
- function setNs(name: string) {
406
- ensureNs(name)
111
+ ctx.importModule = options?.importModule
112
+ ctx.setCurrentNs = (name: string) => {
113
+ runtime.ensureNamespace(name)
407
114
  currentNs = name
115
+ runtime.syncNsVar(name)
408
116
  }
409
117
 
410
- function getNs(name: string): CljNamespace | null {
411
- return registry.get(name)?.ns ?? null
412
- }
118
+ const session: Session = {
119
+ get runtime() {
120
+ return runtime
121
+ },
413
122
 
414
- // Internal: returns the full Env (with lexical chain) for a namespace name.
415
- function getNsEnv(name: string): Env | null {
416
- return registry.get(name) ?? null
417
- }
123
+ get registry() {
124
+ return runtime.registry
125
+ },
418
126
 
419
- internVar(
420
- 'require',
421
- cljNativeFunction('require', (...args: CljValue[]) => {
422
- const currentEnv = registry.get(currentNs)!
423
- for (const arg of args) {
424
- processRequireSpec(arg, currentEnv, registry, resolveNamespace)
425
- }
426
- return cljNil()
427
- }),
428
- coreEnv
429
- )
430
-
431
- internVar(
432
- 'resolve',
433
- cljNativeFunction('resolve', (sym: CljValue) => {
434
- if (!isSymbol(sym)) return cljNil()
435
- const slashIdx = sym.name.indexOf('/')
436
- if (slashIdx > 0) {
437
- const nsName = sym.name.slice(0, slashIdx)
438
- const symName = sym.name.slice(slashIdx + 1)
439
- const nsEnv = registry.get(nsName) ?? null
440
- if (!nsEnv) return cljNil()
441
- return tryLookup(symName, nsEnv) ?? cljNil()
442
- }
443
- const currentEnv = registry.get(currentNs)!
444
- return tryLookup(sym.name, currentEnv) ?? cljNil()
445
- }),
446
- coreEnv
447
- )
448
-
449
- function processNsRequires(forms: CljValue[], env: Env) {
450
- const requireClauses = extractRequireClauses(forms)
451
- for (const specs of requireClauses) {
452
- for (const spec of specs) {
453
- processRequireSpec(spec, env, registry, resolveNamespace)
454
- }
455
- }
456
- }
127
+ get currentNs() {
128
+ return currentNs
129
+ },
457
130
 
458
- function loadFile(source: string, nsName?: string, filePath?: string): string {
459
- const tokens = tokenize(source)
460
- const targetNs = extractNsNameFromTokens(tokens) ?? nsName ?? 'user'
461
- const aliasMap = extractAliasMapFromTokens(tokens)
462
- const forms = readForms(tokens, targetNs, aliasMap)
463
- const env = ensureNs(targetNs)
464
- ctx.currentSource = source
465
- ctx.currentFile = filePath
466
- ctx.currentLineOffset = 0
467
- ctx.currentColOffset = 0
468
- processNsRequires(forms, env)
469
- try {
470
- for (const form of forms) {
471
- const expanded = ctx.expandAll(form, env)
472
- ctx.evaluate(expanded, env)
473
- }
474
- } finally {
475
- ctx.currentSource = undefined
476
- ctx.currentFile = undefined
477
- }
478
- return targetNs
479
- }
131
+ setNs(name: string) {
132
+ runtime.ensureNamespace(name)
133
+ currentNs = name
134
+ runtime.syncNsVar(name)
135
+ },
480
136
 
481
- const api: Session = {
482
- registry,
483
- get currentNs() {
137
+ getNs(name: string): CljNamespace | null {
138
+ return runtime.getNs(name)
139
+ },
140
+
141
+ loadFile(source: string, nsName?: string, filePath?: string): string {
142
+ return runtime.loadFile(source, nsName, filePath, ctx)
143
+ },
144
+
145
+ async loadFileAsync(source: string, nsName?: string, filePath?: string): Promise<string> {
146
+ // If there is no ns declaration in the source, pre-set the namespace from
147
+ // the hint so the forms evaluate in the right context.
148
+ if (nsName) {
149
+ const tokens = tokenize(source)
150
+ if (!extractNsNameFromTokens(tokens)) {
151
+ runtime.ensureNamespace(nsName)
152
+ currentNs = nsName
153
+ runtime.syncNsVar(nsName)
154
+ }
155
+ }
156
+ await session.evaluateAsync(source, { file: filePath })
484
157
  return currentNs
485
158
  },
486
- setNs,
487
- getNs,
488
- loadFile,
489
- addSourceRoot,
490
- evaluate(source: string, opts?: { lineOffset?: number; colOffset?: number; file?: string }) {
159
+
160
+ addSourceRoot(path: string): void {
161
+ runtime.addSourceRoot(path)
162
+ },
163
+
164
+ evaluate(
165
+ source: string,
166
+ opts?: { lineOffset?: number; colOffset?: number; file?: string }
167
+ ): CljValue {
491
168
  ctx.currentSource = source
492
169
  ctx.currentFile = opts?.file
493
170
  ctx.currentLineOffset = opts?.lineOffset ?? 0
494
- ctx.currentColOffset = opts?.colOffset ?? 0
171
+ ctx.currentColOffset = opts?.colOffset ?? 0
495
172
  try {
496
173
  const tokens = tokenize(source)
497
174
  // If source opens with an ns declaration, switch to that namespace
498
175
  // so requires resolve against the right env and currentNs is updated.
499
- // This mirrors what loadFile does, making eval and load-file consistent.
500
176
  const declaredNs = extractNsNameFromTokens(tokens)
501
177
  if (declaredNs) {
502
- ensureNs(declaredNs)
178
+ runtime.ensureNamespace(declaredNs)
503
179
  currentNs = declaredNs
180
+ runtime.syncNsVar(declaredNs)
504
181
  }
505
- const env = getNsEnv(currentNs)!
182
+ const env = runtime.getNamespaceEnv(currentNs)!
506
183
  // Seed alias map from tokens (new aliases declared in this source) and
507
184
  // from ns.aliases/:as (prior require calls) and ns.readerAliases/:as-alias.
508
185
  const aliasMap = extractAliasMapFromTokens(tokens)
@@ -513,8 +190,8 @@ function buildSessionApi(
513
190
  aliasMap.set(alias, nsName)
514
191
  })
515
192
  const forms = readForms(tokens, currentNs, aliasMap)
516
- processNsRequires(forms, env)
517
- let result: CljValue = cljNil()
193
+ runtime.processNsRequires(forms, env, ctx)
194
+ let result: CljValue = v.nil()
518
195
  for (const form of forms) {
519
196
  const expanded = ctx.expandAll(form, env)
520
197
  result = ctx.evaluate(expanded, env)
@@ -538,7 +215,78 @@ function buildSessionApi(
538
215
  ) {
539
216
  e.message += formatErrorContext(source, e.pos, {
540
217
  lineOffset: ctx.currentLineOffset,
541
- colOffset: ctx.currentColOffset,
218
+ colOffset: ctx.currentColOffset,
219
+ })
220
+ }
221
+ throw e
222
+ } finally {
223
+ ctx.currentSource = undefined
224
+ ctx.currentFile = undefined
225
+ }
226
+ },
227
+
228
+ async evaluateAsync(
229
+ source: string,
230
+ opts?: { lineOffset?: number; colOffset?: number; file?: string }
231
+ ): Promise<CljValue> {
232
+ ctx.currentSource = source
233
+ ctx.currentFile = opts?.file
234
+ ctx.currentLineOffset = opts?.lineOffset ?? 0
235
+ ctx.currentColOffset = opts?.colOffset ?? 0
236
+ try {
237
+ const tokens = tokenize(source)
238
+ const declaredNs = extractNsNameFromTokens(tokens)
239
+ if (declaredNs) {
240
+ runtime.ensureNamespace(declaredNs)
241
+ currentNs = declaredNs
242
+ runtime.syncNsVar(declaredNs)
243
+ }
244
+ const env = runtime.getNamespaceEnv(currentNs)!
245
+ const aliasMap = extractAliasMapFromTokens(tokens)
246
+ env.ns?.aliases.forEach((ns, alias) => {
247
+ aliasMap.set(alias, ns.name)
248
+ })
249
+ env.ns?.readerAliases.forEach((nsName, alias) => {
250
+ aliasMap.set(alias, nsName)
251
+ })
252
+ const forms = readForms(tokens, currentNs, aliasMap)
253
+ await runtime.processNsRequiresAsync(forms, env, ctx)
254
+ let result: CljValue = v.nil()
255
+ for (const form of forms) {
256
+ const expanded = ctx.expandAll(form, env)
257
+ result = ctx.evaluate(expanded, env)
258
+ }
259
+ if (result.kind !== 'pending') return result
260
+ try {
261
+ return await result.promise
262
+ } catch (e) {
263
+ if (e instanceof CljThrownSignal) {
264
+ throw new EvaluationError(
265
+ `Unhandled throw: ${printString(e.value)}`,
266
+ { thrownValue: e.value }
267
+ )
268
+ }
269
+ throw e
270
+ }
271
+ } catch (e) {
272
+ if (e instanceof CljThrownSignal) {
273
+ throw new EvaluationError(
274
+ `Unhandled throw: ${printString(e.value)}`,
275
+ { thrownValue: e.value }
276
+ )
277
+ }
278
+ if (e instanceof RecurSignal) {
279
+ throw new EvaluationError('recur called outside of loop or fn', {
280
+ args: e.args,
281
+ })
282
+ }
283
+ if (
284
+ (e instanceof EvaluationError || e instanceof ReaderError) &&
285
+ e.pos
286
+ ) {
287
+ e.message += formatErrorContext(source, e.pos, {
288
+ lineOffset: ctx.currentLineOffset,
289
+ colOffset: ctx.currentColOffset,
542
290
  })
543
291
  }
544
292
  throw e
@@ -547,10 +295,19 @@ function buildSessionApi(
547
295
  ctx.currentFile = undefined
548
296
  }
549
297
  },
550
- evaluateForms(forms: CljValue[]) {
298
+
299
+ applyFunction(fn: CljValue, args: CljValue[]): CljValue {
300
+ return ctx.applyCallable(fn, args, makeEnv())
301
+ },
302
+
303
+ cljToJs(value: CljValue): unknown {
304
+ return _cljToJs(value, { applyFunction: (fn, args) => ctx.applyCallable(fn, args, makeEnv()) })
305
+ },
306
+
307
+ evaluateForms(forms: CljValue[]): CljValue {
551
308
  try {
552
- const env = getNsEnv(currentNs)!
553
- let result: CljValue = cljNil()
309
+ const env = runtime.getNamespaceEnv(currentNs)!
310
+ let result: CljValue = v.nil()
554
311
  for (const form of forms) {
555
312
  const expanded = ctx.expandAll(form, env)
556
313
  result = ctx.evaluate(expanded, env)
@@ -571,8 +328,9 @@ function buildSessionApi(
571
328
  throw e
572
329
  }
573
330
  },
331
+
574
332
  getCompletions(prefix: string, nsName?: string): string[] {
575
- let env: Env | null = registry.get(nsName ?? currentNs) ?? null
333
+ let env: Env | null = runtime.registry.get(nsName ?? currentNs) ?? null
576
334
  const seen = new Set<string>()
577
335
  while (env) {
578
336
  for (const key of env.bindings.keys()) seen.add(key)
@@ -584,7 +342,8 @@ function buildSessionApi(
584
342
  return candidates.filter((k) => k.startsWith(prefix)).sort()
585
343
  },
586
344
  }
587
- return api
345
+
346
+ return session
588
347
  }
589
348
 
590
349
  // ---------------------------------------------------------------------------
@@ -592,25 +351,42 @@ function buildSessionApi(
592
351
  // ---------------------------------------------------------------------------
593
352
 
594
353
  export function createSession(options?: SessionOptions): Session {
595
- const registry: NamespaceRegistry = new Map()
596
-
597
- const coreEnv = makeEnv()
598
- coreEnv.ns = makeNamespace('clojure.core')
599
- loadCoreFunctions(coreEnv, options?.output)
600
- registry.set('clojure.core', coreEnv)
354
+ const modules = options?.modules ?? []
355
+ const runtime = createRuntime({
356
+ sourceRoots: options?.sourceRoots,
357
+ readFile: options?.readFile,
358
+ })
601
359
 
602
- const userEnv = makeEnv(coreEnv)
603
- userEnv.ns = makeNamespace('user')
604
- registry.set('user', userEnv)
605
-
606
- const session = buildSessionApi({ registry, currentNs: 'user' }, options)
360
+ const session = buildSessionFacade(runtime, 'user', options)
607
361
 
362
+ // Bootstrap: load clojure.core source (uses session's ctx via session.loadFile)
608
363
  const coreLoader = builtInNamespaceSources['clojure.core']
609
364
  if (!coreLoader) {
610
365
  throw new Error('Missing built-in clojure.core source in registry')
611
366
  }
612
367
  session.loadFile(coreLoader(), 'clojure.core')
613
368
 
369
+ if (modules.length > 0) {
370
+ session.runtime.installModules(modules)
371
+ }
372
+
373
+ // Intern host bindings into the js namespace as CljJsValue vars.
374
+ // Guard: built-in utility names (js/get, js/set!, js/call, etc.) must not be
375
+ // clobbered — they are already installed by makeJsModule() above.
376
+ if (options?.hostBindings) {
377
+ const jsEnv = runtime.getNamespaceEnv('js')
378
+ if (jsEnv) {
379
+ for (const [name, rawValue] of Object.entries(options.hostBindings)) {
380
+ if (jsEnv.ns?.vars.has(name)) {
381
+ throw new Error(
382
+ `createSession: hostBindings key '${name}' conflicts with built-in js/${name} — choose a different key`
383
+ )
384
+ }
385
+ internVar(name, jsToClj(rawValue), jsEnv)
386
+ }
387
+ }
388
+ }
389
+
614
390
  for (const source of options?.entries ?? []) {
615
391
  session.loadFile(source)
616
392
  }
@@ -619,24 +395,14 @@ export function createSession(options?: SessionOptions): Session {
619
395
  }
620
396
 
621
397
  /**
622
- * A snapshot of a session's registry + current namespace.
398
+ * A snapshot of a session's runtime state + current namespace.
623
399
  * Produced by snapshotSession; consumed by createSessionFromSnapshot.
624
400
  * CljValue objects are shared (they are immutable); only the Env containers
625
401
  * are deep-copied so each derived session gets independent namespace state.
626
402
  */
627
- export type SessionSnapshot = {
628
- registry: NamespaceRegistry
629
- currentNs: string
630
- }
631
-
632
- /**
633
- * Capture a deep clone of the session's env state.
634
- * Typically called once after createSession() and before any user code is
635
- * evaluated, to produce a pristine snapshot that can be cloned cheaply per test.
636
- */
637
403
  export function snapshotSession(session: Session): SessionSnapshot {
638
404
  return {
639
- registry: cloneRegistry(session.registry),
405
+ runtimeSnapshot: session.runtime.snapshot(),
640
406
  currentNs: session.currentNs,
641
407
  }
642
408
  }
@@ -645,17 +411,17 @@ export function snapshotSession(session: Session): SessionSnapshot {
645
411
  * Create a new session from a previously captured snapshot.
646
412
  * Skips the core.clj bootstrap — the cloned registry already contains the
647
413
  * fully-expanded core environment. Re-wires all registry-dependent closures
648
- * (resolveNs, require) to the new registry instance.
414
+ * (resolveNs, require, IO fns) to the new registry instance.
649
415
  */
650
416
  export function createSessionFromSnapshot(
651
417
  snapshot: SessionSnapshot,
652
418
  options?: SessionOptions
653
419
  ): Session {
654
- const registry = cloneRegistry(snapshot.registry)
655
- const session = buildSessionApi(
656
- { registry, currentNs: snapshot.currentNs },
657
- options
658
- )
420
+ const runtime = restoreRuntime(snapshot.runtimeSnapshot, {
421
+ sourceRoots: options?.sourceRoots,
422
+ readFile: options?.readFile,
423
+ })
424
+ const session = buildSessionFacade(runtime, snapshot.currentNs, options)
659
425
  for (const source of options?.entries ?? []) {
660
426
  session.loadFile(source)
661
427
  }