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,32 +1,24 @@
1
+ import { is } from '../assertions'
1
2
  import {
2
- isAFunction,
3
- isEqual,
4
- isFalsy,
5
- isKeyword,
6
- isList,
7
- isMap,
8
- isMultiMethod,
9
- isSymbol,
10
- isTruthy,
11
- isVector,
12
- } from '../assertions'
13
- import { define, extend, getNamespaceEnv, getRootEnv, lookup, lookupVar } from '../env'
3
+ define,
4
+ extend,
5
+ getNamespaceEnv,
6
+ internVar,
7
+ lookup,
8
+ lookupVar,
9
+ makeEnv,
10
+ } from '../env'
14
11
  import { CljThrownSignal, EvaluationError } from '../errors'
15
- import {
16
- cljKeyword,
17
- cljMap,
18
- cljMultiArityFunction,
19
- cljMultiArityMacro,
20
- cljMultiMethod,
21
- cljNativeFunction,
22
- cljNil,
23
- cljString,
24
- cljVar,
25
- } from '../factories'
12
+ import { v } from '../factories'
13
+ // --- ASYNC (experimental) ---
14
+ import { createAsyncEvalCtx } from './async-evaluator'
15
+ // --- END ASYNC ---
16
+ import { getLineCol, getPos } from '../positions'
26
17
  import type {
27
18
  CljFunction,
28
19
  CljKeyword,
29
20
  CljList,
21
+ CljMap,
30
22
  CljMultiMethod,
31
23
  CljNativeFunction,
32
24
  CljValue,
@@ -35,8 +27,29 @@ import type {
35
27
  } from '../types'
36
28
  import { parseArities, RecurSignal } from './arity'
37
29
  import { destructureBindings } from './destructure'
30
+ import { evaluateDot, evaluateNew } from './js-interop'
38
31
  import { evaluateQuasiquote } from './quasiquote'
39
32
  import { assertRecurInTailPosition } from './recur-check'
33
+ import {
34
+ matchesDiscriminator,
35
+ parseTryStructure,
36
+ validateBindingVector,
37
+ } from './form-parsers'
38
+
39
+ function hasDynamicMeta(meta: CljMap | undefined): boolean {
40
+ if (!meta) return false
41
+ for (const [k, v] of meta.entries) {
42
+ if (
43
+ is.keyword(k) &&
44
+ k.name === ':dynamic' &&
45
+ is.boolean(v) &&
46
+ v.value === true
47
+ ) {
48
+ return true
49
+ }
50
+ }
51
+ return false
52
+ }
40
53
 
41
54
  export const specialFormKeywords = {
42
55
  quote: 'quote',
@@ -54,14 +67,26 @@ export const specialFormKeywords = {
54
67
  defmethod: 'defmethod',
55
68
  try: 'try',
56
69
  var: 'var',
70
+ binding: 'binding',
71
+ 'set!': 'set!',
72
+ letfn: 'letfn',
73
+ delay: 'delay',
74
+ 'lazy-seq': 'lazy-seq',
75
+ // --- ASYNC (experimental) ---
76
+ async: 'async',
77
+ // --- END ASYNC ---
78
+ // --- JS INTEROP ---
79
+ '.': '.',
80
+ 'js/new': 'js/new',
81
+ // --- END JS INTEROP ---
57
82
  } as const
58
83
 
59
84
  function keywordToDispatchFn(kw: CljKeyword): CljNativeFunction {
60
- return cljNativeFunction(`kw:${kw.name}`, (...args: CljValue[]) => {
85
+ return v.nativeFn(`kw:${kw.name}`, (...args: CljValue[]) => {
61
86
  const target = args[0]
62
- if (!isMap(target)) return cljNil()
63
- const entry = target.entries.find(([k]) => isEqual(k, kw))
64
- return entry ? entry[1] : cljNil()
87
+ if (!is.map(target)) return v.nil()
88
+ const entry = target.entries.find(([k]) => is.equal(k, kw))
89
+ return entry ? entry[1] : v.nil()
65
90
  })
66
91
  }
67
92
 
@@ -70,83 +95,12 @@ function evaluateTry(
70
95
  env: Env,
71
96
  ctx: EvaluationContext
72
97
  ): CljValue {
73
- const forms = list.value.slice(1)
74
- const bodyForms: CljValue[] = []
75
- const catchClauses: Array<{
76
- discriminator: CljValue
77
- binding: string
78
- body: CljValue[]
79
- }> = []
80
- let finallyForms: CljValue[] | null = null
81
-
82
- for (let i = 0; i < forms.length; i++) {
83
- const form = forms[i]
84
- if (isList(form) && form.value.length > 0 && isSymbol(form.value[0])) {
85
- const head = form.value[0].name
86
- if (head === 'catch') {
87
- if (form.value.length < 3) {
88
- throw new EvaluationError(
89
- 'catch requires a discriminator and a binding symbol',
90
- { form, env }
91
- )
92
- }
93
- const discriminator = form.value[1]
94
- const bindingSym = form.value[2]
95
- if (!isSymbol(bindingSym)) {
96
- throw new EvaluationError('catch binding must be a symbol', {
97
- form,
98
- env,
99
- })
100
- }
101
- catchClauses.push({
102
- discriminator,
103
- binding: bindingSym.name,
104
- body: form.value.slice(3),
105
- })
106
- continue
107
- }
108
- if (head === 'finally') {
109
- if (i !== forms.length - 1) {
110
- throw new EvaluationError(
111
- 'finally clause must be the last in try expression',
112
- {
113
- form,
114
- env,
115
- }
116
- )
117
- }
118
- finallyForms = form.value.slice(1)
119
- continue
120
- }
121
- }
122
- bodyForms.push(form)
123
- }
124
-
125
- function matchesDiscriminator(
126
- discriminator: CljValue,
127
- thrown: CljValue
128
- ): boolean {
129
- const disc = ctx.evaluate(discriminator, env)
130
- if (isKeyword(disc)) {
131
- if (disc.name === ':default') return true
132
- if (!isMap(thrown)) return false
133
- const typeEntry = thrown.entries.find(
134
- ([k]) => isKeyword(k) && k.name === ':type'
135
- )
136
- if (!typeEntry) return false
137
- return isEqual(typeEntry[1], disc)
138
- }
139
- if (isAFunction(disc)) {
140
- const result = ctx.applyFunction(disc, [thrown], env)
141
- return isTruthy(result)
142
- }
143
- throw new EvaluationError(
144
- 'catch discriminator must be a keyword or a predicate function',
145
- { discriminator: disc, env }
146
- )
147
- }
98
+ const { bodyForms, catchClauses, finallyForms } = parseTryStructure(
99
+ list,
100
+ env
101
+ )
148
102
 
149
- let result: CljValue = cljNil()
103
+ let result: CljValue = v.nil()
150
104
  let pendingThrow: unknown = null
151
105
 
152
106
  try {
@@ -158,9 +112,9 @@ function evaluateTry(
158
112
  if (e instanceof CljThrownSignal) {
159
113
  thrownValue = e.value
160
114
  } else if (e instanceof EvaluationError) {
161
- thrownValue = cljMap([
162
- [cljKeyword(':type'), cljKeyword(':error/runtime')],
163
- [cljKeyword(':message'), cljString(e.message)],
115
+ thrownValue = v.map([
116
+ [v.keyword(':type'), v.keyword(':error/runtime')],
117
+ [v.keyword(':message'), v.string(e.message)],
164
118
  ])
165
119
  } else {
166
120
  throw e
@@ -168,7 +122,7 @@ function evaluateTry(
168
122
 
169
123
  let handled = false
170
124
  for (const clause of catchClauses) {
171
- if (matchesDiscriminator(clause.discriminator, thrownValue)) {
125
+ if (matchesDiscriminator(clause.discriminator, thrownValue, env, ctx)) {
172
126
  const catchEnv = extend([clause.binding], [thrownValue], env)
173
127
  result = ctx.evaluateForms(clause.body, catchEnv)
174
128
  handled = true
@@ -205,6 +159,46 @@ function evalQuasiquote(
205
159
  return evaluateQuasiquote(list.value[1], env, new Map(), ctx)
206
160
  }
207
161
 
162
+ /**
163
+ * Merge reader-attached symbol metadata with source-position metadata
164
+ * (:line, :column, :file) derived from the current evaluation context.
165
+ * Returns undefined if there is nothing to attach.
166
+ */
167
+ function buildVarMeta(
168
+ symMeta: CljMap | undefined,
169
+ ctx: EvaluationContext,
170
+ nameVal?: CljValue
171
+ ): CljMap | undefined {
172
+ const pos = nameVal ? getPos(nameVal) : undefined
173
+ const hasPosInfo = pos && ctx.currentSource
174
+
175
+ if (!symMeta && !hasPosInfo) return undefined
176
+
177
+ const posEntries: [CljValue, CljValue][] = []
178
+ if (hasPosInfo) {
179
+ const { line, col } = getLineCol(ctx.currentSource!, pos!.start)
180
+ const lineOffset = ctx.currentLineOffset ?? 0
181
+ const colOffset = ctx.currentColOffset ?? 0
182
+ posEntries.push([v.keyword(':line'), v.number(line + lineOffset)])
183
+ posEntries.push([
184
+ v.keyword(':column'),
185
+ v.number(line === 1 ? col + colOffset : col),
186
+ ])
187
+ if (ctx.currentFile) {
188
+ posEntries.push([v.keyword(':file'), v.string(ctx.currentFile)])
189
+ }
190
+ }
191
+
192
+ // Preserve all existing symMeta entries except the three we're stamping.
193
+ const POS_KEYS = new Set([':line', ':column', ':file'])
194
+ const baseEntries = (symMeta?.entries ?? []).filter(
195
+ ([k]) => !(k.kind === 'keyword' && POS_KEYS.has(k.name))
196
+ )
197
+
198
+ const allEntries = [...baseEntries, ...posEntries]
199
+ return allEntries.length > 0 ? v.map(allEntries) : undefined
200
+ }
201
+
208
202
  function evaluateDef(
209
203
  list: CljList,
210
204
  env: Env,
@@ -221,19 +215,28 @@ function evaluateDef(
221
215
  // (def name) with no value is a bare declaration — a no-op in the evaluator.
222
216
  // This lets .clj source files declare runtime-injected symbols so that
223
217
  // clojure-lsp can resolve them, without clobbering the native binding.
224
- if (list.value[2] === undefined) return cljNil()
218
+ if (list.value[2] === undefined) return v.nil()
225
219
 
226
220
  const nsEnv = getNamespaceEnv(env)
227
221
  const cljNs = nsEnv.ns!
228
222
  const newValue = ctx.evaluate(list.value[2], env)
229
223
 
224
+ // Compute source position metadata (:line/:column/:file) if available.
225
+ const varMeta = buildVarMeta(name.meta, ctx, name)
226
+
230
227
  const existing = cljNs.vars.get(name.name)
231
228
  if (existing) {
232
229
  existing.value = newValue
230
+ if (varMeta) {
231
+ existing.meta = varMeta
232
+ if (hasDynamicMeta(varMeta)) existing.dynamic = true
233
+ }
233
234
  } else {
234
- cljNs.vars.set(name.name, cljVar(cljNs.name, name.name, newValue))
235
+ const newVar = v.var(cljNs.name, name.name, newValue, varMeta)
236
+ if (hasDynamicMeta(varMeta)) newVar.dynamic = true
237
+ cljNs.vars.set(name.name, newVar)
235
238
  }
236
- return cljNil()
239
+ return v.nil()
237
240
  }
238
241
 
239
242
  const evaluateNs = (
@@ -241,16 +244,16 @@ const evaluateNs = (
241
244
  _env: Env,
242
245
  _ctx: EvaluationContext
243
246
  ): CljValue => {
244
- return cljNil() // special form handled by the environment, no effects here
247
+ return v.nil() // special form handled by the environment, no effects here
245
248
  }
246
249
 
247
250
  function evaluateIf(list: CljList, env: Env, ctx: EvaluationContext): CljValue {
248
251
  const condition = ctx.evaluate(list.value[1], env)
249
- if (!isFalsy(condition)) {
252
+ if (!is.falsy(condition)) {
250
253
  return ctx.evaluate(list.value[2], env)
251
254
  }
252
255
  if (!list.value[3]) {
253
- return cljNil() // no-else case, return nil
256
+ return v.nil() // no-else case, return nil
254
257
  }
255
258
  return ctx.evaluate(list.value[3], env)
256
259
  }
@@ -265,18 +268,7 @@ function evaluateLet(
265
268
  ctx: EvaluationContext
266
269
  ): CljValue {
267
270
  const bindings = list.value[1]
268
- if (!isVector(bindings)) {
269
- throw new EvaluationError('Bindings must be a vector', {
270
- bindings,
271
- env,
272
- })
273
- }
274
- if (bindings.value.length % 2 !== 0) {
275
- throw new EvaluationError(
276
- 'Bindings must be a balanced pair of keys and values',
277
- { bindings, env }
278
- )
279
- }
271
+ validateBindingVector(bindings, 'let', env)
280
272
  const body = list.value.slice(2)
281
273
  let localEnv = env
282
274
  for (let i = 0; i < bindings.value.length; i += 2) {
@@ -298,30 +290,112 @@ function evaluateFn(
298
290
  env: Env,
299
291
  _ctx: EvaluationContext
300
292
  ): CljValue {
301
- const arities = parseArities(list.value.slice(1), env)
293
+ const rest = list.value.slice(1)
294
+ // (fn name [...] ...) — optional name symbol before the param vector/arities
295
+ let fnName: string | undefined
296
+ let arityForms = rest
297
+ if (rest[0]?.kind === 'symbol') {
298
+ fnName = rest[0].name
299
+ arityForms = rest.slice(1)
300
+ }
301
+ const arities = parseArities(arityForms, env)
302
302
  for (const arity of arities) {
303
303
  assertRecurInTailPosition(arity.body)
304
304
  }
305
- return cljMultiArityFunction(arities, env)
305
+ const fn = v.multiArityFunction(arities, env)
306
+ if (fnName) {
307
+ fn.name = fnName
308
+ // Bind the name in the fn's closure env so the body can call itself by name.
309
+ // We wrap the captured env in a new frame containing name → fn, then point
310
+ // fn.env at that frame so the binding is visible during application.
311
+ const selfEnv = makeEnv(env)
312
+ selfEnv.bindings.set(fnName, fn)
313
+ fn.env = selfEnv
314
+ }
315
+ return fn
316
+ }
317
+
318
+ function evaluateLetfn(
319
+ list: CljList,
320
+ env: Env,
321
+ ctx: EvaluationContext
322
+ ): CljValue {
323
+ // (letfn [(f1 [x] ...) (f2 [x] ...)] body...)
324
+ const fnSpecs = list.value[1]
325
+ if (!is.vector(fnSpecs)) {
326
+ throw new EvaluationError('letfn binding specs must be a vector', {
327
+ fnSpecs,
328
+ env,
329
+ })
330
+ }
331
+ const body = list.value.slice(2)
332
+
333
+ // Create a shared env frame for all the fns to close over
334
+ const sharedEnv = makeEnv(env)
335
+
336
+ // First pass: create all fn objects in the shared env
337
+ for (const spec of fnSpecs.value) {
338
+ if (!is.list(spec) || spec.value.length < 2 || !is.symbol(spec.value[0])) {
339
+ throw new EvaluationError(
340
+ 'letfn specs must be (name [params] body...) forms',
341
+ { spec }
342
+ )
343
+ }
344
+ const name = spec.value[0].name
345
+ const arityForms = spec.value.slice(1)
346
+ const arities = parseArities(arityForms, sharedEnv)
347
+ for (const arity of arities) {
348
+ assertRecurInTailPosition(arity.body)
349
+ }
350
+ const fn = v.multiArityFunction(arities, sharedEnv)
351
+ fn.name = name
352
+ sharedEnv.bindings.set(name, fn)
353
+ }
354
+
355
+ // Second pass: point all fn envs to the shared env so they can see each other
356
+ for (const spec of fnSpecs.value) {
357
+ const name = (spec as CljList).value[0] as { name: string }
358
+ const fn = sharedEnv.bindings.get(name.name) as CljFunction
359
+ fn.env = sharedEnv
360
+ }
361
+
362
+ return ctx.evaluateForms(body, sharedEnv)
363
+ }
364
+
365
+ function mergeDocIntoMeta(base: CljMap | undefined, docstring: string): CljMap {
366
+ const docEntry: [CljValue, CljValue] = [
367
+ v.keyword(':doc'),
368
+ v.string(docstring),
369
+ ]
370
+ const existing = (base?.entries ?? []).filter(
371
+ ([k]) => !(k.kind === 'keyword' && k.name === ':doc')
372
+ )
373
+ return { kind: 'map', entries: [...existing, docEntry] }
306
374
  }
307
375
 
308
376
  function evaluateDefmacro(
309
377
  list: CljList,
310
378
  env: Env,
311
- _ctx: EvaluationContext
379
+ ctx: EvaluationContext
312
380
  ): CljValue {
313
381
  const name = list.value[1]
314
- if (!isSymbol(name)) {
382
+ if (!is.symbol(name)) {
315
383
  throw new EvaluationError('First element of defmacro must be a symbol', {
316
384
  name,
317
385
  list,
318
386
  env,
319
387
  })
320
388
  }
321
- const arities = parseArities(list.value.slice(2), env)
322
- const macro = cljMultiArityMacro(arities, env)
323
- define(name.name, macro, getRootEnv(env))
324
- return cljNil()
389
+ const rest = list.value.slice(2)
390
+ const docstring = rest[0]?.kind === 'string' ? rest[0].value : undefined
391
+ const arityForms = docstring ? rest.slice(1) : rest
392
+ const arities = parseArities(arityForms, env)
393
+ const macro = v.multiArityMacro(arities, env)
394
+ macro.name = name.name
395
+ const varMeta = buildVarMeta(name.meta, ctx, name)
396
+ const finalMeta = docstring ? mergeDocIntoMeta(varMeta, docstring) : varMeta
397
+ internVar(name.name, macro, getNamespaceEnv(env), finalMeta)
398
+ return v.nil()
325
399
  }
326
400
 
327
401
  function evaluateLoop(
@@ -330,18 +404,7 @@ function evaluateLoop(
330
404
  ctx: EvaluationContext
331
405
  ): CljValue {
332
406
  const loopBindings = list.value[1]
333
- if (!isVector(loopBindings)) {
334
- throw new EvaluationError('loop bindings must be a vector', {
335
- loopBindings,
336
- env,
337
- })
338
- }
339
- if (loopBindings.value.length % 2 !== 0) {
340
- throw new EvaluationError(
341
- 'loop bindings must be a balanced pair of keys and values',
342
- { loopBindings, env }
343
- )
344
- }
407
+ validateBindingVector(loopBindings, 'loop', env)
345
408
  const loopBody = list.value.slice(2)
346
409
  assertRecurInTailPosition(loopBody)
347
410
 
@@ -413,7 +476,7 @@ function evaluateDefmulti(
413
476
  ctx: EvaluationContext
414
477
  ): CljValue {
415
478
  const mmName = list.value[1]
416
- if (!isSymbol(mmName)) {
479
+ if (!is.symbol(mmName)) {
417
480
  throw new EvaluationError('defmulti: first argument must be a symbol', {
418
481
  list,
419
482
  env,
@@ -421,11 +484,11 @@ function evaluateDefmulti(
421
484
  }
422
485
  const dispatchFnExpr = list.value[2]
423
486
  let dispatchFn: CljFunction | CljNativeFunction
424
- if (isKeyword(dispatchFnExpr)) {
487
+ if (is.keyword(dispatchFnExpr)) {
425
488
  dispatchFn = keywordToDispatchFn(dispatchFnExpr)
426
489
  } else {
427
490
  const evaluated = ctx.evaluate(dispatchFnExpr, env)
428
- if (!isAFunction(evaluated)) {
491
+ if (!is.aFunction(evaluated)) {
429
492
  throw new EvaluationError(
430
493
  'defmulti: dispatch-fn must be a function or keyword',
431
494
  { list, env }
@@ -433,9 +496,9 @@ function evaluateDefmulti(
433
496
  }
434
497
  dispatchFn = evaluated
435
498
  }
436
- const mm = cljMultiMethod(mmName.name, dispatchFn, [])
437
- define(mmName.name, mm, getNamespaceEnv(env))
438
- return cljNil()
499
+ const mm = v.multiMethod(mmName.name, dispatchFn, [])
500
+ internVar(mmName.name, mm, getNamespaceEnv(env))
501
+ return v.nil()
439
502
  }
440
503
 
441
504
  function evaluateDefmethod(
@@ -444,7 +507,7 @@ function evaluateDefmethod(
444
507
  ctx: EvaluationContext
445
508
  ): CljValue {
446
509
  const mmName = list.value[1]
447
- if (!isSymbol(mmName)) {
510
+ if (!is.symbol(mmName)) {
448
511
  throw new EvaluationError('defmethod: first argument must be a symbol', {
449
512
  list,
450
513
  env,
@@ -452,18 +515,18 @@ function evaluateDefmethod(
452
515
  }
453
516
  const dispatchVal = ctx.evaluate(list.value[2], env)
454
517
  const existing = lookup(mmName.name, env)
455
- if (!isMultiMethod(existing)) {
518
+ if (!is.multiMethod(existing)) {
456
519
  throw new EvaluationError(
457
520
  `defmethod: ${mmName.name} is not a multimethod`,
458
521
  { list, env }
459
522
  )
460
523
  }
461
524
  const arities = parseArities([list.value[3], ...list.value.slice(4)], env)
462
- const methodFn = cljMultiArityFunction(arities, env)
463
- const isDefault = isKeyword(dispatchVal) && dispatchVal.name === ':default'
525
+ const methodFn = v.multiArityFunction(arities, env)
526
+ const isDefault = is.keyword(dispatchVal) && dispatchVal.name === ':default'
464
527
  let updated: CljMultiMethod
465
528
  if (isDefault) {
466
- updated = cljMultiMethod(
529
+ updated = v.multiMethod(
467
530
  existing.name,
468
531
  existing.dispatchFn,
469
532
  existing.methods,
@@ -471,24 +534,30 @@ function evaluateDefmethod(
471
534
  )
472
535
  } else {
473
536
  const filtered = existing.methods.filter(
474
- (m) => !isEqual(m.dispatchVal, dispatchVal)
537
+ (m) => !is.equal(m.dispatchVal, dispatchVal)
475
538
  )
476
- updated = cljMultiMethod(existing.name, existing.dispatchFn, [
539
+ updated = v.multiMethod(existing.name, existing.dispatchFn, [
477
540
  ...filtered,
478
541
  { dispatchVal, fn: methodFn },
479
542
  ])
480
543
  }
481
- define(mmName.name, updated, getNamespaceEnv(env))
482
- return cljNil()
544
+ // Update the var's value in place if possible, otherwise fall back to define
545
+ const eVar = lookupVar(mmName.name, env)
546
+ if (eVar) {
547
+ eVar.value = updated
548
+ } else {
549
+ define(mmName.name, updated, getNamespaceEnv(env))
550
+ }
551
+ return v.nil()
483
552
  }
484
553
 
485
554
  function evaluateVar(
486
555
  list: CljList,
487
556
  env: Env,
488
- _ctx: EvaluationContext
557
+ ctx: EvaluationContext
489
558
  ): CljValue {
490
559
  const sym = list.value[1]
491
- if (!isSymbol(sym)) {
560
+ if (!is.symbol(sym)) {
492
561
  throw new EvaluationError('var expects a symbol', { list })
493
562
  }
494
563
 
@@ -497,19 +566,13 @@ function evaluateVar(
497
566
  const alias = sym.name.slice(0, slashIdx)
498
567
  const localName = sym.name.slice(slashIdx + 1)
499
568
  const nsEnv = getNamespaceEnv(env)
500
- // Try alias lookup (CljNamespace) first
501
- const aliasCljNs = nsEnv.ns?.aliases.get(alias)
502
- if (aliasCljNs) {
503
- const v = aliasCljNs.vars.get(localName)
504
- if (!v) throw new EvaluationError(`Var ${sym.name} not found`, { sym })
505
- return v
506
- }
507
- // Fall back to full namespace Env chain (handles clojure.core/sym etc.)
508
- const targetEnv = getRootEnv(env).resolveNs?.(alias) ?? null
509
- if (!targetEnv) {
569
+ // Resolve alias: local :as alias first, then full namespace name
570
+ const targetNs =
571
+ nsEnv.ns?.aliases.get(alias) ?? ctx.resolveNs(alias) ?? null
572
+ if (!targetNs) {
510
573
  throw new EvaluationError(`No such namespace: ${alias}`, { sym })
511
574
  }
512
- const v = lookupVar(localName, targetEnv)
575
+ const v = targetNs.vars.get(localName)
513
576
  if (!v) throw new EvaluationError(`Var ${sym.name} not found`, { sym })
514
577
  return v
515
578
  }
@@ -524,6 +587,138 @@ function evaluateVar(
524
587
  return v
525
588
  }
526
589
 
590
+ function evaluateBinding(
591
+ list: CljList,
592
+ env: Env,
593
+ ctx: EvaluationContext
594
+ ): CljValue {
595
+ const bindings = list.value[1]
596
+ if (!is.vector(bindings)) {
597
+ throw new EvaluationError('binding requires a vector of bindings', {
598
+ list,
599
+ env,
600
+ })
601
+ }
602
+ if (bindings.value.length % 2 !== 0) {
603
+ throw new EvaluationError(
604
+ 'binding vector must have an even number of forms',
605
+ { list, env }
606
+ )
607
+ }
608
+ const body = list.value.slice(2)
609
+ const boundVars: import('../types').CljVar[] = []
610
+
611
+ for (let i = 0; i < bindings.value.length; i += 2) {
612
+ const sym = bindings.value[i]
613
+ if (!is.symbol(sym)) {
614
+ throw new EvaluationError('binding left-hand side must be a symbol', {
615
+ sym,
616
+ })
617
+ }
618
+ const newVal = ctx.evaluate(bindings.value[i + 1], env)
619
+ const v = lookupVar(sym.name, env)
620
+ if (!v) {
621
+ throw new EvaluationError(
622
+ `No var found for symbol '${sym.name}' in binding form`,
623
+ { sym }
624
+ )
625
+ }
626
+ if (!v.dynamic) {
627
+ throw new EvaluationError(
628
+ `Cannot use binding with non-dynamic var ${v.ns}/${v.name}. Mark it dynamic with (def ^:dynamic ${sym.name} ...)`,
629
+ { sym }
630
+ )
631
+ }
632
+ v.bindingStack ??= []
633
+ v.bindingStack.push(newVal)
634
+ boundVars.push(v)
635
+ }
636
+
637
+ try {
638
+ return ctx.evaluateForms(body, env)
639
+ } finally {
640
+ for (const v of boundVars) {
641
+ v.bindingStack!.pop()
642
+ }
643
+ }
644
+ }
645
+
646
+ function evaluateSet(
647
+ list: CljList,
648
+ env: Env,
649
+ ctx: EvaluationContext
650
+ ): CljValue {
651
+ if (list.value.length !== 3) {
652
+ throw new EvaluationError(
653
+ `set! requires exactly 2 arguments, got ${list.value.length - 1}`,
654
+ { list, env }
655
+ )
656
+ }
657
+ const symForm = list.value[1]
658
+ if (!is.symbol(symForm)) {
659
+ throw new EvaluationError(
660
+ `set! first argument must be a symbol, got ${symForm.kind}`,
661
+ { symForm, env }
662
+ )
663
+ }
664
+ const v = lookupVar(symForm.name, env)
665
+ if (!v) {
666
+ throw new EvaluationError(
667
+ `Unable to resolve var: ${symForm.name} in this context`,
668
+ { symForm, env }
669
+ )
670
+ }
671
+ if (!v.dynamic) {
672
+ throw new EvaluationError(
673
+ `Cannot set! non-dynamic var ${v.ns}/${v.name}. Mark it with ^:dynamic.`,
674
+ { symForm, env }
675
+ )
676
+ }
677
+ if (!v.bindingStack || v.bindingStack.length === 0) {
678
+ throw new EvaluationError(
679
+ `Cannot set! ${v.ns}/${v.name} — no active binding. Use set! only inside a (binding [...] ...) form.`,
680
+ { symForm, env }
681
+ )
682
+ }
683
+ const newVal = ctx.evaluate(list.value[2], env)
684
+ v.bindingStack[v.bindingStack.length - 1] = newVal
685
+ return newVal
686
+ }
687
+
688
+ function evaluateDelay(
689
+ list: CljList,
690
+ env: Env,
691
+ ctx: EvaluationContext
692
+ ): CljValue {
693
+ const body = list.value.slice(1)
694
+ return v.delay(() => ctx.evaluateForms(body, env))
695
+ }
696
+
697
+ function evaluateLazySeqForm(
698
+ list: CljList,
699
+ env: Env,
700
+ ctx: EvaluationContext
701
+ ): CljValue {
702
+ const body = list.value.slice(1)
703
+ return v.lazySeq(() => ctx.evaluateForms(body, env))
704
+ }
705
+
706
+ // --- ASYNC BLOCK HANDLER (experimental) ---
707
+ // Gateway into the async sub-evaluator. See async-evaluator.ts.
708
+ // To revert: remove this function, the `async` case below, and the import above.
709
+ function evaluateAsyncBlock(
710
+ list: CljList,
711
+ env: Env,
712
+ ctx: EvaluationContext
713
+ ): CljValue {
714
+ const body = list.value.slice(1)
715
+ if (body.length === 0) return v.pending(Promise.resolve(v.nil()))
716
+ const asyncCtx = createAsyncEvalCtx(ctx)
717
+ const promise = asyncCtx.evaluateForms(body, env)
718
+ return v.pending(promise)
719
+ }
720
+ // --- END ASYNC BLOCK HANDLER ---
721
+
527
722
  type SpecialFormEvaluatorFn = (
528
723
  list: CljList,
529
724
  env: Env,
@@ -546,6 +741,18 @@ const specialFormEvaluatorEntries = {
546
741
  defmulti: evaluateDefmulti,
547
742
  defmethod: evaluateDefmethod,
548
743
  var: evaluateVar,
744
+ binding: evaluateBinding,
745
+ 'set!': evaluateSet,
746
+ letfn: evaluateLetfn,
747
+ delay: evaluateDelay,
748
+ 'lazy-seq': evaluateLazySeqForm,
749
+ // --- ASYNC (experimental) ---
750
+ async: evaluateAsyncBlock,
751
+ // --- END ASYNC ---
752
+ // --- JS INTEROP ---
753
+ '.': evaluateDot,
754
+ 'js/new': evaluateNew,
755
+ // --- END JS INTEROP ---
549
756
  } as const satisfies Record<
550
757
  keyof typeof specialFormKeywords,
551
758
  SpecialFormEvaluatorFn