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,29 +1,18 @@
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, internVar, 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
- cljNumber,
24
- cljString,
25
- cljVar,
26
- } from '../factories'
12
+ import { v } from '../factories'
13
+ // --- ASYNC (experimental) ---
14
+ import { createAsyncEvalCtx } from './async-evaluator'
15
+ // --- END ASYNC ---
27
16
  import { getLineCol, getPos } from '../positions'
28
17
  import type {
29
18
  CljFunction,
@@ -38,13 +27,24 @@ import type {
38
27
  } from '../types'
39
28
  import { parseArities, RecurSignal } from './arity'
40
29
  import { destructureBindings } from './destructure'
30
+ import { evaluateDot, evaluateNew } from './js-interop'
41
31
  import { evaluateQuasiquote } from './quasiquote'
42
32
  import { assertRecurInTailPosition } from './recur-check'
33
+ import {
34
+ matchesDiscriminator,
35
+ parseTryStructure,
36
+ validateBindingVector,
37
+ } from './form-parsers'
43
38
 
44
39
  function hasDynamicMeta(meta: CljMap | undefined): boolean {
45
40
  if (!meta) return false
46
41
  for (const [k, v] of meta.entries) {
47
- if (k.kind === 'keyword' && k.name === ':dynamic' && v.kind === 'boolean' && v.value === true) {
42
+ if (
43
+ is.keyword(k) &&
44
+ k.name === ':dynamic' &&
45
+ is.boolean(v) &&
46
+ v.value === true
47
+ ) {
48
48
  return true
49
49
  }
50
50
  }
@@ -69,14 +69,24 @@ export const specialFormKeywords = {
69
69
  var: 'var',
70
70
  binding: 'binding',
71
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 ---
72
82
  } as const
73
83
 
74
84
  function keywordToDispatchFn(kw: CljKeyword): CljNativeFunction {
75
- return cljNativeFunction(`kw:${kw.name}`, (...args: CljValue[]) => {
85
+ return v.nativeFn(`kw:${kw.name}`, (...args: CljValue[]) => {
76
86
  const target = args[0]
77
- if (!isMap(target)) return cljNil()
78
- const entry = target.entries.find(([k]) => isEqual(k, kw))
79
- 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()
80
90
  })
81
91
  }
82
92
 
@@ -85,83 +95,12 @@ function evaluateTry(
85
95
  env: Env,
86
96
  ctx: EvaluationContext
87
97
  ): CljValue {
88
- const forms = list.value.slice(1)
89
- const bodyForms: CljValue[] = []
90
- const catchClauses: Array<{
91
- discriminator: CljValue
92
- binding: string
93
- body: CljValue[]
94
- }> = []
95
- let finallyForms: CljValue[] | null = null
96
-
97
- for (let i = 0; i < forms.length; i++) {
98
- const form = forms[i]
99
- if (isList(form) && form.value.length > 0 && isSymbol(form.value[0])) {
100
- const head = form.value[0].name
101
- if (head === 'catch') {
102
- if (form.value.length < 3) {
103
- throw new EvaluationError(
104
- 'catch requires a discriminator and a binding symbol',
105
- { form, env }
106
- )
107
- }
108
- const discriminator = form.value[1]
109
- const bindingSym = form.value[2]
110
- if (!isSymbol(bindingSym)) {
111
- throw new EvaluationError('catch binding must be a symbol', {
112
- form,
113
- env,
114
- })
115
- }
116
- catchClauses.push({
117
- discriminator,
118
- binding: bindingSym.name,
119
- body: form.value.slice(3),
120
- })
121
- continue
122
- }
123
- if (head === 'finally') {
124
- if (i !== forms.length - 1) {
125
- throw new EvaluationError(
126
- 'finally clause must be the last in try expression',
127
- {
128
- form,
129
- env,
130
- }
131
- )
132
- }
133
- finallyForms = form.value.slice(1)
134
- continue
135
- }
136
- }
137
- bodyForms.push(form)
138
- }
139
-
140
- function matchesDiscriminator(
141
- discriminator: CljValue,
142
- thrown: CljValue
143
- ): boolean {
144
- const disc = ctx.evaluate(discriminator, env)
145
- if (isKeyword(disc)) {
146
- if (disc.name === ':default') return true
147
- if (!isMap(thrown)) return false
148
- const typeEntry = thrown.entries.find(
149
- ([k]) => isKeyword(k) && k.name === ':type'
150
- )
151
- if (!typeEntry) return false
152
- return isEqual(typeEntry[1], disc)
153
- }
154
- if (isAFunction(disc)) {
155
- const result = ctx.applyFunction(disc, [thrown], env)
156
- return isTruthy(result)
157
- }
158
- throw new EvaluationError(
159
- 'catch discriminator must be a keyword or a predicate function',
160
- { discriminator: disc, env }
161
- )
162
- }
98
+ const { bodyForms, catchClauses, finallyForms } = parseTryStructure(
99
+ list,
100
+ env
101
+ )
163
102
 
164
- let result: CljValue = cljNil()
103
+ let result: CljValue = v.nil()
165
104
  let pendingThrow: unknown = null
166
105
 
167
106
  try {
@@ -173,9 +112,9 @@ function evaluateTry(
173
112
  if (e instanceof CljThrownSignal) {
174
113
  thrownValue = e.value
175
114
  } else if (e instanceof EvaluationError) {
176
- thrownValue = cljMap([
177
- [cljKeyword(':type'), cljKeyword(':error/runtime')],
178
- [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)],
179
118
  ])
180
119
  } else {
181
120
  throw e
@@ -183,7 +122,7 @@ function evaluateTry(
183
122
 
184
123
  let handled = false
185
124
  for (const clause of catchClauses) {
186
- if (matchesDiscriminator(clause.discriminator, thrownValue)) {
125
+ if (matchesDiscriminator(clause.discriminator, thrownValue, env, ctx)) {
187
126
  const catchEnv = extend([clause.binding], [thrownValue], env)
188
127
  result = ctx.evaluateForms(clause.body, catchEnv)
189
128
  handled = true
@@ -239,11 +178,14 @@ function buildVarMeta(
239
178
  if (hasPosInfo) {
240
179
  const { line, col } = getLineCol(ctx.currentSource!, pos!.start)
241
180
  const lineOffset = ctx.currentLineOffset ?? 0
242
- const colOffset = ctx.currentColOffset ?? 0
243
- posEntries.push([cljKeyword(':line'), cljNumber(line + lineOffset)])
244
- posEntries.push([cljKeyword(':column'), cljNumber(line === 1 ? col + colOffset : col)])
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
+ ])
245
187
  if (ctx.currentFile) {
246
- posEntries.push([cljKeyword(':file'), cljString(ctx.currentFile)])
188
+ posEntries.push([v.keyword(':file'), v.string(ctx.currentFile)])
247
189
  }
248
190
  }
249
191
 
@@ -254,7 +196,7 @@ function buildVarMeta(
254
196
  )
255
197
 
256
198
  const allEntries = [...baseEntries, ...posEntries]
257
- return allEntries.length > 0 ? cljMap(allEntries) : undefined
199
+ return allEntries.length > 0 ? v.map(allEntries) : undefined
258
200
  }
259
201
 
260
202
  function evaluateDef(
@@ -273,7 +215,7 @@ function evaluateDef(
273
215
  // (def name) with no value is a bare declaration — a no-op in the evaluator.
274
216
  // This lets .clj source files declare runtime-injected symbols so that
275
217
  // clojure-lsp can resolve them, without clobbering the native binding.
276
- if (list.value[2] === undefined) return cljNil()
218
+ if (list.value[2] === undefined) return v.nil()
277
219
 
278
220
  const nsEnv = getNamespaceEnv(env)
279
221
  const cljNs = nsEnv.ns!
@@ -290,11 +232,11 @@ function evaluateDef(
290
232
  if (hasDynamicMeta(varMeta)) existing.dynamic = true
291
233
  }
292
234
  } else {
293
- const v = cljVar(cljNs.name, name.name, newValue, varMeta)
294
- if (hasDynamicMeta(varMeta)) v.dynamic = true
295
- cljNs.vars.set(name.name, v)
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)
296
238
  }
297
- return cljNil()
239
+ return v.nil()
298
240
  }
299
241
 
300
242
  const evaluateNs = (
@@ -302,16 +244,16 @@ const evaluateNs = (
302
244
  _env: Env,
303
245
  _ctx: EvaluationContext
304
246
  ): CljValue => {
305
- return cljNil() // special form handled by the environment, no effects here
247
+ return v.nil() // special form handled by the environment, no effects here
306
248
  }
307
249
 
308
250
  function evaluateIf(list: CljList, env: Env, ctx: EvaluationContext): CljValue {
309
251
  const condition = ctx.evaluate(list.value[1], env)
310
- if (!isFalsy(condition)) {
252
+ if (!is.falsy(condition)) {
311
253
  return ctx.evaluate(list.value[2], env)
312
254
  }
313
255
  if (!list.value[3]) {
314
- return cljNil() // no-else case, return nil
256
+ return v.nil() // no-else case, return nil
315
257
  }
316
258
  return ctx.evaluate(list.value[3], env)
317
259
  }
@@ -326,18 +268,7 @@ function evaluateLet(
326
268
  ctx: EvaluationContext
327
269
  ): CljValue {
328
270
  const bindings = list.value[1]
329
- if (!isVector(bindings)) {
330
- throw new EvaluationError('Bindings must be a vector', {
331
- bindings,
332
- env,
333
- })
334
- }
335
- if (bindings.value.length % 2 !== 0) {
336
- throw new EvaluationError(
337
- 'Bindings must be a balanced pair of keys and values',
338
- { bindings, env }
339
- )
340
- }
271
+ validateBindingVector(bindings, 'let', env)
341
272
  const body = list.value.slice(2)
342
273
  let localEnv = env
343
274
  for (let i = 0; i < bindings.value.length; i += 2) {
@@ -359,30 +290,112 @@ function evaluateFn(
359
290
  env: Env,
360
291
  _ctx: EvaluationContext
361
292
  ): CljValue {
362
- 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)
363
302
  for (const arity of arities) {
364
303
  assertRecurInTailPosition(arity.body)
365
304
  }
366
- 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] }
367
374
  }
368
375
 
369
376
  function evaluateDefmacro(
370
377
  list: CljList,
371
378
  env: Env,
372
- _ctx: EvaluationContext
379
+ ctx: EvaluationContext
373
380
  ): CljValue {
374
381
  const name = list.value[1]
375
- if (!isSymbol(name)) {
382
+ if (!is.symbol(name)) {
376
383
  throw new EvaluationError('First element of defmacro must be a symbol', {
377
384
  name,
378
385
  list,
379
386
  env,
380
387
  })
381
388
  }
382
- const arities = parseArities(list.value.slice(2), env)
383
- const macro = cljMultiArityMacro(arities, env)
384
- internVar(name.name, macro, getNamespaceEnv(env))
385
- 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()
386
399
  }
387
400
 
388
401
  function evaluateLoop(
@@ -391,18 +404,7 @@ function evaluateLoop(
391
404
  ctx: EvaluationContext
392
405
  ): CljValue {
393
406
  const loopBindings = list.value[1]
394
- if (!isVector(loopBindings)) {
395
- throw new EvaluationError('loop bindings must be a vector', {
396
- loopBindings,
397
- env,
398
- })
399
- }
400
- if (loopBindings.value.length % 2 !== 0) {
401
- throw new EvaluationError(
402
- 'loop bindings must be a balanced pair of keys and values',
403
- { loopBindings, env }
404
- )
405
- }
407
+ validateBindingVector(loopBindings, 'loop', env)
406
408
  const loopBody = list.value.slice(2)
407
409
  assertRecurInTailPosition(loopBody)
408
410
 
@@ -474,7 +476,7 @@ function evaluateDefmulti(
474
476
  ctx: EvaluationContext
475
477
  ): CljValue {
476
478
  const mmName = list.value[1]
477
- if (!isSymbol(mmName)) {
479
+ if (!is.symbol(mmName)) {
478
480
  throw new EvaluationError('defmulti: first argument must be a symbol', {
479
481
  list,
480
482
  env,
@@ -482,11 +484,11 @@ function evaluateDefmulti(
482
484
  }
483
485
  const dispatchFnExpr = list.value[2]
484
486
  let dispatchFn: CljFunction | CljNativeFunction
485
- if (isKeyword(dispatchFnExpr)) {
487
+ if (is.keyword(dispatchFnExpr)) {
486
488
  dispatchFn = keywordToDispatchFn(dispatchFnExpr)
487
489
  } else {
488
490
  const evaluated = ctx.evaluate(dispatchFnExpr, env)
489
- if (!isAFunction(evaluated)) {
491
+ if (!is.aFunction(evaluated)) {
490
492
  throw new EvaluationError(
491
493
  'defmulti: dispatch-fn must be a function or keyword',
492
494
  { list, env }
@@ -494,9 +496,9 @@ function evaluateDefmulti(
494
496
  }
495
497
  dispatchFn = evaluated
496
498
  }
497
- const mm = cljMultiMethod(mmName.name, dispatchFn, [])
499
+ const mm = v.multiMethod(mmName.name, dispatchFn, [])
498
500
  internVar(mmName.name, mm, getNamespaceEnv(env))
499
- return cljNil()
501
+ return v.nil()
500
502
  }
501
503
 
502
504
  function evaluateDefmethod(
@@ -505,7 +507,7 @@ function evaluateDefmethod(
505
507
  ctx: EvaluationContext
506
508
  ): CljValue {
507
509
  const mmName = list.value[1]
508
- if (!isSymbol(mmName)) {
510
+ if (!is.symbol(mmName)) {
509
511
  throw new EvaluationError('defmethod: first argument must be a symbol', {
510
512
  list,
511
513
  env,
@@ -513,18 +515,18 @@ function evaluateDefmethod(
513
515
  }
514
516
  const dispatchVal = ctx.evaluate(list.value[2], env)
515
517
  const existing = lookup(mmName.name, env)
516
- if (!isMultiMethod(existing)) {
518
+ if (!is.multiMethod(existing)) {
517
519
  throw new EvaluationError(
518
520
  `defmethod: ${mmName.name} is not a multimethod`,
519
521
  { list, env }
520
522
  )
521
523
  }
522
524
  const arities = parseArities([list.value[3], ...list.value.slice(4)], env)
523
- const methodFn = cljMultiArityFunction(arities, env)
524
- const isDefault = isKeyword(dispatchVal) && dispatchVal.name === ':default'
525
+ const methodFn = v.multiArityFunction(arities, env)
526
+ const isDefault = is.keyword(dispatchVal) && dispatchVal.name === ':default'
525
527
  let updated: CljMultiMethod
526
528
  if (isDefault) {
527
- updated = cljMultiMethod(
529
+ updated = v.multiMethod(
528
530
  existing.name,
529
531
  existing.dispatchFn,
530
532
  existing.methods,
@@ -532,30 +534,30 @@ function evaluateDefmethod(
532
534
  )
533
535
  } else {
534
536
  const filtered = existing.methods.filter(
535
- (m) => !isEqual(m.dispatchVal, dispatchVal)
537
+ (m) => !is.equal(m.dispatchVal, dispatchVal)
536
538
  )
537
- updated = cljMultiMethod(existing.name, existing.dispatchFn, [
539
+ updated = v.multiMethod(existing.name, existing.dispatchFn, [
538
540
  ...filtered,
539
541
  { dispatchVal, fn: methodFn },
540
542
  ])
541
543
  }
542
544
  // Update the var's value in place if possible, otherwise fall back to define
543
- const v = lookupVar(mmName.name, env)
544
- if (v) {
545
- v.value = updated
545
+ const eVar = lookupVar(mmName.name, env)
546
+ if (eVar) {
547
+ eVar.value = updated
546
548
  } else {
547
549
  define(mmName.name, updated, getNamespaceEnv(env))
548
550
  }
549
- return cljNil()
551
+ return v.nil()
550
552
  }
551
553
 
552
554
  function evaluateVar(
553
555
  list: CljList,
554
556
  env: Env,
555
- _ctx: EvaluationContext
557
+ ctx: EvaluationContext
556
558
  ): CljValue {
557
559
  const sym = list.value[1]
558
- if (!isSymbol(sym)) {
560
+ if (!is.symbol(sym)) {
559
561
  throw new EvaluationError('var expects a symbol', { list })
560
562
  }
561
563
 
@@ -564,19 +566,13 @@ function evaluateVar(
564
566
  const alias = sym.name.slice(0, slashIdx)
565
567
  const localName = sym.name.slice(slashIdx + 1)
566
568
  const nsEnv = getNamespaceEnv(env)
567
- // Try alias lookup (CljNamespace) first
568
- const aliasCljNs = nsEnv.ns?.aliases.get(alias)
569
- if (aliasCljNs) {
570
- const v = aliasCljNs.vars.get(localName)
571
- if (!v) throw new EvaluationError(`Var ${sym.name} not found`, { sym })
572
- return v
573
- }
574
- // Fall back to full namespace Env chain (handles clojure.core/sym etc.)
575
- const targetEnv = getRootEnv(env).resolveNs?.(alias) ?? null
576
- 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) {
577
573
  throw new EvaluationError(`No such namespace: ${alias}`, { sym })
578
574
  }
579
- const v = lookupVar(localName, targetEnv)
575
+ const v = targetNs.vars.get(localName)
580
576
  if (!v) throw new EvaluationError(`Var ${sym.name} not found`, { sym })
581
577
  return v
582
578
  }
@@ -597,7 +593,7 @@ function evaluateBinding(
597
593
  ctx: EvaluationContext
598
594
  ): CljValue {
599
595
  const bindings = list.value[1]
600
- if (!isVector(bindings)) {
596
+ if (!is.vector(bindings)) {
601
597
  throw new EvaluationError('binding requires a vector of bindings', {
602
598
  list,
603
599
  env,
@@ -614,11 +610,10 @@ function evaluateBinding(
614
610
 
615
611
  for (let i = 0; i < bindings.value.length; i += 2) {
616
612
  const sym = bindings.value[i]
617
- if (!isSymbol(sym)) {
618
- throw new EvaluationError(
619
- 'binding left-hand side must be a symbol',
620
- { sym }
621
- )
613
+ if (!is.symbol(sym)) {
614
+ throw new EvaluationError('binding left-hand side must be a symbol', {
615
+ sym,
616
+ })
622
617
  }
623
618
  const newVal = ctx.evaluate(bindings.value[i + 1], env)
624
619
  const v = lookupVar(sym.name, env)
@@ -648,7 +643,11 @@ function evaluateBinding(
648
643
  }
649
644
  }
650
645
 
651
- function evaluateSet(list: CljList, env: Env, ctx: EvaluationContext): CljValue {
646
+ function evaluateSet(
647
+ list: CljList,
648
+ env: Env,
649
+ ctx: EvaluationContext
650
+ ): CljValue {
652
651
  if (list.value.length !== 3) {
653
652
  throw new EvaluationError(
654
653
  `set! requires exactly 2 arguments, got ${list.value.length - 1}`,
@@ -656,7 +655,7 @@ function evaluateSet(list: CljList, env: Env, ctx: EvaluationContext): CljValue
656
655
  )
657
656
  }
658
657
  const symForm = list.value[1]
659
- if (!isSymbol(symForm)) {
658
+ if (!is.symbol(symForm)) {
660
659
  throw new EvaluationError(
661
660
  `set! first argument must be a symbol, got ${symForm.kind}`,
662
661
  { symForm, env }
@@ -686,6 +685,40 @@ function evaluateSet(list: CljList, env: Env, ctx: EvaluationContext): CljValue
686
685
  return newVal
687
686
  }
688
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
+
689
722
  type SpecialFormEvaluatorFn = (
690
723
  list: CljList,
691
724
  env: Env,
@@ -710,6 +743,16 @@ const specialFormEvaluatorEntries = {
710
743
  var: evaluateVar,
711
744
  binding: evaluateBinding,
712
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 ---
713
756
  } as const satisfies Record<
714
757
  keyof typeof specialFormKeywords,
715
758
  SpecialFormEvaluatorFn