conjure-js 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist-cli/conjure-js.mjs +9336 -5028
  2. package/dist-vite-plugin/index.mjs +10455 -0
  3. package/package.json +9 -2
  4. package/src/bin/cli.ts +2 -2
  5. package/src/bin/nrepl-symbol.ts +150 -0
  6. package/src/bin/nrepl.ts +301 -157
  7. package/src/bin/version.ts +1 -1
  8. package/src/clojure/core.clj +764 -29
  9. package/src/clojure/core.clj.d.ts +76 -4
  10. package/src/clojure/demo/math.clj +5 -1
  11. package/src/clojure/generated/builtin-namespace-registry.ts +4 -0
  12. package/src/clojure/generated/clojure-core-source.ts +765 -29
  13. package/src/clojure/generated/clojure-set-source.ts +136 -0
  14. package/src/clojure/generated/clojure-walk-source.ts +72 -0
  15. package/src/clojure/set.clj +132 -0
  16. package/src/clojure/set.clj.d.ts +20 -0
  17. package/src/clojure/string.clj.d.ts +14 -0
  18. package/src/clojure/walk.clj +68 -0
  19. package/src/clojure/walk.clj.d.ts +7 -0
  20. package/src/core/assertions.ts +114 -6
  21. package/src/core/bootstrap.ts +337 -0
  22. package/src/core/conversions.ts +48 -31
  23. package/src/core/core-module.ts +303 -0
  24. package/src/core/env.ts +42 -7
  25. package/src/core/errors.ts +8 -0
  26. package/src/core/evaluator/apply.ts +40 -25
  27. package/src/core/evaluator/arity.ts +8 -8
  28. package/src/core/evaluator/async-evaluator.ts +565 -0
  29. package/src/core/evaluator/collections.ts +30 -4
  30. package/src/core/evaluator/destructure.ts +180 -69
  31. package/src/core/evaluator/dispatch.ts +24 -14
  32. package/src/core/evaluator/evaluate.ts +22 -20
  33. package/src/core/evaluator/expand.ts +45 -15
  34. package/src/core/evaluator/form-parsers.ts +178 -0
  35. package/src/core/evaluator/index.ts +7 -9
  36. package/src/core/evaluator/js-interop.ts +189 -0
  37. package/src/core/evaluator/quasiquote.ts +14 -8
  38. package/src/core/evaluator/recur-check.ts +6 -6
  39. package/src/core/evaluator/special-forms.ts +380 -173
  40. package/src/core/factories.ts +182 -3
  41. package/src/core/index.ts +55 -5
  42. package/src/core/module.ts +136 -0
  43. package/src/core/ns-forms.ts +107 -0
  44. package/src/core/positions.ts +9 -2
  45. package/src/core/printer.ts +371 -11
  46. package/src/core/reader.ts +127 -29
  47. package/src/core/registry.ts +209 -0
  48. package/src/core/runtime.ts +376 -0
  49. package/src/core/session.ts +263 -478
  50. package/src/core/stdlib/arithmetic.ts +516 -215
  51. package/src/core/stdlib/async-fns.ts +132 -0
  52. package/src/core/stdlib/atoms.ts +286 -63
  53. package/src/core/stdlib/errors.ts +54 -50
  54. package/src/core/stdlib/hof.ts +74 -173
  55. package/src/core/stdlib/js-namespace.ts +344 -0
  56. package/src/core/stdlib/lazy.ts +34 -0
  57. package/src/core/stdlib/maps-sets.ts +322 -0
  58. package/src/core/stdlib/meta.ts +109 -28
  59. package/src/core/stdlib/predicates.ts +322 -196
  60. package/src/core/stdlib/regex.ts +126 -98
  61. package/src/core/stdlib/seq.ts +564 -0
  62. package/src/core/stdlib/strings.ts +164 -135
  63. package/src/core/stdlib/transducers.ts +95 -100
  64. package/src/core/stdlib/utils.ts +283 -147
  65. package/src/core/stdlib/vars.ts +27 -27
  66. package/src/core/stdlib/vectors.ts +122 -0
  67. package/src/core/tokenizer.ts +13 -3
  68. package/src/core/transformations.ts +117 -9
  69. package/src/core/types.ts +118 -6
  70. package/src/host/node-host-module.ts +74 -0
  71. package/src/nrepl/relay.ts +432 -0
  72. package/src/vite-plugin-clj/codegen.ts +87 -95
  73. package/src/vite-plugin-clj/index.ts +242 -18
  74. package/src/vite-plugin-clj/namespace-utils.ts +39 -0
  75. package/src/vite-plugin-clj/static-analysis.ts +211 -0
  76. package/src/clojure/demo.clj +0 -63
  77. package/src/clojure/demo.clj.d.ts +0 -0
  78. package/src/core/core-env.ts +0 -60
  79. package/src/core/stdlib/collections.ts +0 -784
  80. package/src/host/node.ts +0 -55
@@ -1,495 +1,185 @@
1
- import { isKeyword, isList, isSymbol, isVector } from './assertions'
2
- import { loadCoreFunctions } from './core-env'
3
- import { define, 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
- loadFile: (source: string, nsName?: string) => string
30
- evaluate: (source: string) => CljValue
58
+ loadFile: (source: string, nsName?: string, filePath?: string) => string
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
- define(
346
- 'println',
347
- cljNativeFunction('println', (...args: CljValue[]) => {
348
- emitFn(args.map(valueToString).join(' '))
349
- return cljNil()
350
- }),
351
- coreEnv
352
- )
353
- define(
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
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)),
394
110
  }
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)!
403
- }
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
- define(
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
- define(
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): 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
- processNsRequires(forms, env)
465
- for (const form of forms) {
466
- const expanded = ctx.expandAll(form, env)
467
- ctx.evaluate(expanded, env)
468
- }
469
- return targetNs
470
- }
131
+ setNs(name: string) {
132
+ runtime.ensureNamespace(name)
133
+ currentNs = name
134
+ runtime.syncNsVar(name)
135
+ },
471
136
 
472
- const api: Session = {
473
- registry,
474
- 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 })
475
157
  return currentNs
476
158
  },
477
- setNs,
478
- getNs,
479
- loadFile,
480
- addSourceRoot,
481
- evaluate(source: 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 {
168
+ ctx.currentSource = source
169
+ ctx.currentFile = opts?.file
170
+ ctx.currentLineOffset = opts?.lineOffset ?? 0
171
+ ctx.currentColOffset = opts?.colOffset ?? 0
482
172
  try {
483
173
  const tokens = tokenize(source)
484
174
  // If source opens with an ns declaration, switch to that namespace
485
175
  // so requires resolve against the right env and currentNs is updated.
486
- // This mirrors what loadFile does, making eval and load-file consistent.
487
176
  const declaredNs = extractNsNameFromTokens(tokens)
488
177
  if (declaredNs) {
489
- ensureNs(declaredNs)
178
+ runtime.ensureNamespace(declaredNs)
490
179
  currentNs = declaredNs
180
+ runtime.syncNsVar(declaredNs)
491
181
  }
492
- const env = getNsEnv(currentNs)!
182
+ const env = runtime.getNamespaceEnv(currentNs)!
493
183
  // Seed alias map from tokens (new aliases declared in this source) and
494
184
  // from ns.aliases/:as (prior require calls) and ns.readerAliases/:as-alias.
495
185
  const aliasMap = extractAliasMapFromTokens(tokens)
@@ -500,8 +190,8 @@ function buildSessionApi(
500
190
  aliasMap.set(alias, nsName)
501
191
  })
502
192
  const forms = readForms(tokens, currentNs, aliasMap)
503
- processNsRequires(forms, env)
504
- let result: CljValue = cljNil()
193
+ runtime.processNsRequires(forms, env, ctx)
194
+ let result: CljValue = v.nil()
505
195
  for (const form of forms) {
506
196
  const expanded = ctx.expandAll(form, env)
507
197
  result = ctx.evaluate(expanded, env)
@@ -523,15 +213,101 @@ function buildSessionApi(
523
213
  (e instanceof EvaluationError || e instanceof ReaderError) &&
524
214
  e.pos
525
215
  ) {
526
- e.message += formatErrorContext(source, e.pos)
216
+ e.message += formatErrorContext(source, e.pos, {
217
+ lineOffset: ctx.currentLineOffset,
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,
290
+ })
527
291
  }
528
292
  throw e
293
+ } finally {
294
+ ctx.currentSource = undefined
295
+ ctx.currentFile = undefined
529
296
  }
530
297
  },
531
- 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 {
532
308
  try {
533
- const env = getNsEnv(currentNs)!
534
- let result: CljValue = cljNil()
309
+ const env = runtime.getNamespaceEnv(currentNs)!
310
+ let result: CljValue = v.nil()
535
311
  for (const form of forms) {
536
312
  const expanded = ctx.expandAll(form, env)
537
313
  result = ctx.evaluate(expanded, env)
@@ -552,8 +328,9 @@ function buildSessionApi(
552
328
  throw e
553
329
  }
554
330
  },
331
+
555
332
  getCompletions(prefix: string, nsName?: string): string[] {
556
- let env: Env | null = registry.get(nsName ?? currentNs) ?? null
333
+ let env: Env | null = runtime.registry.get(nsName ?? currentNs) ?? null
557
334
  const seen = new Set<string>()
558
335
  while (env) {
559
336
  for (const key of env.bindings.keys()) seen.add(key)
@@ -565,7 +342,8 @@ function buildSessionApi(
565
342
  return candidates.filter((k) => k.startsWith(prefix)).sort()
566
343
  },
567
344
  }
568
- return api
345
+
346
+ return session
569
347
  }
570
348
 
571
349
  // ---------------------------------------------------------------------------
@@ -573,25 +351,42 @@ function buildSessionApi(
573
351
  // ---------------------------------------------------------------------------
574
352
 
575
353
  export function createSession(options?: SessionOptions): Session {
576
- const registry: NamespaceRegistry = new Map()
577
-
578
- const coreEnv = makeEnv()
579
- coreEnv.ns = makeNamespace('clojure.core')
580
- loadCoreFunctions(coreEnv, options?.output)
581
- registry.set('clojure.core', coreEnv)
582
-
583
- const userEnv = makeEnv(coreEnv)
584
- userEnv.ns = makeNamespace('user')
585
- registry.set('user', userEnv)
354
+ const modules = options?.modules ?? []
355
+ const runtime = createRuntime({
356
+ sourceRoots: options?.sourceRoots,
357
+ readFile: options?.readFile,
358
+ })
586
359
 
587
- const session = buildSessionApi({ registry, currentNs: 'user' }, options)
360
+ const session = buildSessionFacade(runtime, 'user', options)
588
361
 
362
+ // Bootstrap: load clojure.core source (uses session's ctx via session.loadFile)
589
363
  const coreLoader = builtInNamespaceSources['clojure.core']
590
364
  if (!coreLoader) {
591
365
  throw new Error('Missing built-in clojure.core source in registry')
592
366
  }
593
367
  session.loadFile(coreLoader(), 'clojure.core')
594
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
+
595
390
  for (const source of options?.entries ?? []) {
596
391
  session.loadFile(source)
597
392
  }
@@ -600,24 +395,14 @@ export function createSession(options?: SessionOptions): Session {
600
395
  }
601
396
 
602
397
  /**
603
- * A snapshot of a session's registry + current namespace.
398
+ * A snapshot of a session's runtime state + current namespace.
604
399
  * Produced by snapshotSession; consumed by createSessionFromSnapshot.
605
400
  * CljValue objects are shared (they are immutable); only the Env containers
606
401
  * are deep-copied so each derived session gets independent namespace state.
607
402
  */
608
- export type SessionSnapshot = {
609
- registry: NamespaceRegistry
610
- currentNs: string
611
- }
612
-
613
- /**
614
- * Capture a deep clone of the session's env state.
615
- * Typically called once after createSession() and before any user code is
616
- * evaluated, to produce a pristine snapshot that can be cloned cheaply per test.
617
- */
618
403
  export function snapshotSession(session: Session): SessionSnapshot {
619
404
  return {
620
- registry: cloneRegistry(session.registry),
405
+ runtimeSnapshot: session.runtime.snapshot(),
621
406
  currentNs: session.currentNs,
622
407
  }
623
408
  }
@@ -626,17 +411,17 @@ export function snapshotSession(session: Session): SessionSnapshot {
626
411
  * Create a new session from a previously captured snapshot.
627
412
  * Skips the core.clj bootstrap — the cloned registry already contains the
628
413
  * fully-expanded core environment. Re-wires all registry-dependent closures
629
- * (resolveNs, require) to the new registry instance.
414
+ * (resolveNs, require, IO fns) to the new registry instance.
630
415
  */
631
416
  export function createSessionFromSnapshot(
632
417
  snapshot: SessionSnapshot,
633
418
  options?: SessionOptions
634
419
  ): Session {
635
- const registry = cloneRegistry(snapshot.registry)
636
- const session = buildSessionApi(
637
- { registry, currentNs: snapshot.currentNs },
638
- options
639
- )
420
+ const runtime = restoreRuntime(snapshot.runtimeSnapshot, {
421
+ sourceRoots: options?.sourceRoots,
422
+ readFile: options?.readFile,
423
+ })
424
+ const session = buildSessionFacade(runtime, snapshot.currentNs, options)
640
425
  for (const source of options?.entries ?? []) {
641
426
  session.loadFile(source)
642
427
  }