conjure-js 0.0.13 → 0.0.14

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 (58) hide show
  1. package/dist-cli/conjure-js.mjs +2328 -2089
  2. package/dist-vite-plugin/index.mjs +2327 -2088
  3. package/package.json +1 -1
  4. package/src/bin/version.ts +1 -1
  5. package/src/core/assertions.ts +10 -3
  6. package/src/core/bootstrap.ts +7 -23
  7. package/src/core/compiler/binding.ts +164 -0
  8. package/src/core/compiler/callable.ts +41 -0
  9. package/src/core/compiler/compile-env.ts +40 -0
  10. package/src/core/compiler/control-flow.ts +79 -0
  11. package/src/core/compiler/index.ts +121 -0
  12. package/src/core/env.ts +4 -4
  13. package/src/core/errors.ts +1 -0
  14. package/src/core/evaluator/apply.ts +7 -3
  15. package/src/core/evaluator/arity.ts +16 -6
  16. package/src/core/evaluator/async-evaluator.ts +68 -89
  17. package/src/core/evaluator/collections.ts +9 -4
  18. package/src/core/evaluator/destructure.ts +45 -55
  19. package/src/core/evaluator/dispatch.ts +21 -24
  20. package/src/core/evaluator/evaluate.ts +14 -2
  21. package/src/core/evaluator/expand.ts +5 -7
  22. package/src/core/evaluator/js-interop.ts +46 -33
  23. package/src/core/evaluator/quasiquote.ts +7 -11
  24. package/src/core/evaluator/recur-check.ts +1 -1
  25. package/src/core/evaluator/special-forms.ts +18 -38
  26. package/src/core/index.ts +1 -1
  27. package/src/core/keywords.ts +105 -0
  28. package/src/core/modules/core/index.ts +131 -0
  29. package/src/core/{stdlib → modules/core/stdlib}/arithmetic.ts +6 -6
  30. package/src/core/{stdlib → modules/core/stdlib}/async-fns.ts +6 -6
  31. package/src/core/{stdlib → modules/core/stdlib}/atoms.ts +7 -7
  32. package/src/core/{stdlib → modules/core/stdlib}/errors.ts +4 -4
  33. package/src/core/{stdlib → modules/core/stdlib}/hof.ts +6 -6
  34. package/src/core/{stdlib → modules/core/stdlib}/lazy.ts +4 -4
  35. package/src/core/{stdlib → modules/core/stdlib}/maps-sets.ts +6 -6
  36. package/src/core/{stdlib → modules/core/stdlib}/meta.ts +5 -5
  37. package/src/core/{stdlib → modules/core/stdlib}/predicates.ts +7 -7
  38. package/src/core/modules/core/stdlib/print.ts +108 -0
  39. package/src/core/{stdlib → modules/core/stdlib}/regex.ts +5 -5
  40. package/src/core/{stdlib → modules/core/stdlib}/seq.ts +7 -7
  41. package/src/core/{stdlib → modules/core/stdlib}/strings.ts +6 -6
  42. package/src/core/{stdlib → modules/core/stdlib}/transducers.ts +6 -6
  43. package/src/core/{stdlib → modules/core/stdlib}/utils.ts +10 -10
  44. package/src/core/{stdlib → modules/core/stdlib}/vars.ts +4 -4
  45. package/src/core/{stdlib → modules/core/stdlib}/vectors.ts +6 -6
  46. package/src/core/modules/js/index.ts +402 -0
  47. package/src/core/ns-forms.ts +25 -17
  48. package/src/core/positions.ts +22 -2
  49. package/src/core/printer.ts +162 -53
  50. package/src/core/reader.ts +25 -22
  51. package/src/core/registry.ts +10 -10
  52. package/src/core/runtime.ts +23 -23
  53. package/src/core/session.ts +17 -7
  54. package/src/core/tokenizer.ts +14 -4
  55. package/src/core/transformations.ts +48 -29
  56. package/src/core/types.ts +57 -81
  57. package/src/core/core-module.ts +0 -303
  58. package/src/core/stdlib/js-namespace.ts +0 -344
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "conjure-js",
3
3
  "private": false,
4
- "version": "0.0.13",
4
+ "version": "0.0.14",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -1,4 +1,4 @@
1
1
  // This file is auto-generated by scripts/gen-version.mjs
2
2
  // Do not edit manually - changes will be overwritten
3
3
 
4
- export const VERSION = '0.0.13'
4
+ export const VERSION = '0.0.14'
@@ -1,5 +1,4 @@
1
1
  import {
2
- valueKeywords,
3
2
  type CljAtom,
4
3
  type CljBoolean,
5
4
  type CljCons,
@@ -15,6 +14,7 @@ import {
15
14
  type CljNamespace,
16
15
  type CljNativeFunction,
17
16
  type CljNumber,
17
+ type CljPending,
18
18
  type CljReduced,
19
19
  type CljRegex,
20
20
  type CljSet,
@@ -25,7 +25,8 @@ import {
25
25
  type CljVector,
26
26
  type CljVolatile,
27
27
  } from './types.ts'
28
- import { specialFormKeywords } from './evaluator/special-forms.ts'
28
+
29
+ import { specialFormKeywords, valueKeywords } from './keywords.ts'
29
30
 
30
31
  export const isNil = (value: CljValue): boolean => value.kind === 'nil'
31
32
  export const isBoolean = (value: CljValue): value is CljBoolean =>
@@ -67,7 +68,9 @@ export const isJsValue = (value: CljValue): value is CljJsValue =>
67
68
 
68
69
  /** True for any value that can be invoked like a function (IFn). */
69
70
  export const isCallable = (value: CljValue): boolean =>
70
- isAFunction(value) || isKeyword(value) || isMap(value) ||
71
+ isAFunction(value) ||
72
+ isKeyword(value) ||
73
+ isMap(value) ||
71
74
  (isJsValue(value) && typeof value.value === 'function')
72
75
  export const isMultiMethod = (value: CljValue): value is CljMultiMethod =>
73
76
  value.kind === 'multi-method'
@@ -207,6 +210,9 @@ export const isEqual = (a: CljValue, b: CljValue): boolean => {
207
210
  export const isNumber = (value: CljValue): value is CljNumber =>
208
211
  value.kind === 'number'
209
212
 
213
+ export const isPending = (value: CljValue): value is CljPending =>
214
+ value.kind === 'pending'
215
+
210
216
  // Main assertion interface for the entire package
211
217
  export const is = {
212
218
  nil: isNil,
@@ -242,4 +248,5 @@ export const is = {
242
248
  cljValue: isCljValue,
243
249
  equal: isEqual,
244
250
  jsValue: isJsValue,
251
+ pending: isPending,
245
252
  }
@@ -5,6 +5,7 @@ import { v } from './factories'
5
5
  import type { CljNamespace, CljValue, Env, EvaluationContext } from './types'
6
6
  import { ensureNamespaceInRegistry, processRequireSpec } from './registry'
7
7
  import type { NamespaceRegistry } from './registry'
8
+ import { specialFormKeywords } from './keywords'
8
9
 
9
10
  // ---------------------------------------------------------------------------
10
11
  // wireNsCore — wires *ns*, namespace introspection fns, require, and resolve
@@ -112,8 +113,10 @@ export function wireNsCore(
112
113
  if (theVar.ns !== ns.name) return
113
114
  const isPrivate = (theVar.meta?.entries ?? []).some(
114
115
  ([k, val]) =>
115
- k.kind === 'keyword' && k.name === ':private' &&
116
- val.kind === 'boolean' && val.value === true
116
+ k.kind === 'keyword' &&
117
+ k.name === ':private' &&
118
+ val.kind === 'boolean' &&
119
+ val.value === true
117
120
  )
118
121
  if (!isPrivate) entries.push([v.symbol(name), theVar])
119
122
  })
@@ -195,27 +198,7 @@ export function wireNsCore(
195
198
  v.nativeFn('special-symbol?', (sym: CljValue) => {
196
199
  if (sym === undefined || !isSymbol(sym)) return v.boolean(false)
197
200
  const specials = new Set([
198
- 'def',
199
- 'if',
200
- 'do',
201
- 'let',
202
- 'quote',
203
- 'var',
204
- 'fn',
205
- 'loop',
206
- 'recur',
207
- 'throw',
208
- 'try',
209
- 'catch',
210
- 'finally',
211
- 'ns',
212
- 'defmacro',
213
- 'binding',
214
- 'monitor-enter',
215
- 'monitor-exit',
216
- 'new',
217
- 'set!',
218
- '.',
201
+ ...Object.values(specialFormKeywords),
219
202
  'import',
220
203
  ])
221
204
  return v.boolean(specials.has(sym.name))
@@ -306,6 +289,7 @@ export function wireIdeStubs(registry: NamespaceRegistry, coreEnv: Env): void {
306
289
  )
307
290
 
308
291
  // Java class stubs — Cursive references these as bare symbols for type checks
292
+ // other IDE integrations may probe the env to access the capabilities of the runtime
309
293
  for (const javaClass of [
310
294
  'Class',
311
295
  'Object',
@@ -0,0 +1,164 @@
1
+ import { is } from '../assertions.ts'
2
+ import { assertRecurInTailPosition } from '../evaluator/recur-check.ts'
3
+ import { v } from '../factories.ts'
4
+ import type {
5
+ CljList,
6
+ CljValue,
7
+ CompiledExpr,
8
+ CompileEnv,
9
+ CompileFn,
10
+ SlotRef,
11
+ } from '../types.ts'
12
+ import { findLoopTarget } from './compile-env.ts'
13
+ import { compileDo } from './control-flow.ts'
14
+
15
+ const BINDINGS_POS = 1
16
+ const BODY_START_POS = 2
17
+
18
+ export function compileLet(
19
+ node: CljList,
20
+ compileEnv: CompileEnv | null,
21
+ compile: CompileFn
22
+ ): CompiledExpr | null {
23
+ const bindings = node.value[BINDINGS_POS]
24
+ // must be a vector with an even number of elements, else bail out
25
+ if (!is.vector(bindings) || bindings.value.length % 2 !== 0) return null
26
+
27
+ let currentCompileEnv = compileEnv
28
+ const slotInits: Array<[SlotRef, CompiledExpr]> = []
29
+
30
+ for (let i = 0; i < bindings.value.length; i += 2) {
31
+ const pattern = bindings.value[i]
32
+ // destructuring pattern not supported yet
33
+ if (!is.symbol(pattern)) return null
34
+ const slot: SlotRef = { value: null }
35
+
36
+ const compiledInit = compile(bindings.value[i + 1], currentCompileEnv)
37
+ // unsupported init form, bail out
38
+ if (compiledInit === null) return null
39
+ slotInits.push([slot, compiledInit])
40
+
41
+ currentCompileEnv = {
42
+ bindings: new Map([[pattern.name, slot]]),
43
+ outer: currentCompileEnv,
44
+ }
45
+ }
46
+
47
+ // compile the body with full env (all bindings in scope)
48
+ const body = node.value.slice(BODY_START_POS)
49
+ const compiledBody = compileDo(body, currentCompileEnv, compile)
50
+ // unsupported body form, bail out
51
+ if (compiledBody === null) return null
52
+
53
+ return (env, ctx) => {
54
+ // save all previous slot values (handles recursive/nested lets)
55
+ const prevSlotValues = slotInits.map(([slot]) => slot.value)
56
+
57
+ // evaluate inits sequentially, writing. into slots
58
+ for (const [slot, compiledInit] of slotInits) {
59
+ slot.value = compiledInit(env, ctx)
60
+ }
61
+
62
+ const result = compiledBody(env, ctx)
63
+
64
+ // restore prev slot values
65
+ slotInits.forEach(([slot], index) => {
66
+ slot.value = prevSlotValues[index]
67
+ })
68
+
69
+ return result
70
+ }
71
+ }
72
+
73
+ export function compileLoop(
74
+ node: CljList,
75
+ compileEnv: CompileEnv | null,
76
+ compile: CompileFn
77
+ ): CompiledExpr | null {
78
+ const bindings = node.value[BINDINGS_POS]
79
+ if (!is.vector(bindings) || bindings.value.length % 2 !== 0) return null
80
+ const body = node.value.slice(BODY_START_POS)
81
+ assertRecurInTailPosition(body)
82
+
83
+ let currentCompileEnv = compileEnv
84
+ const slotInits: Array<[SlotRef, CompiledExpr]> = []
85
+ const namedSlots: Array<[string, SlotRef]> = []
86
+
87
+ for (let i = 0; i < bindings.value.length; i += 2) {
88
+ const pattern = bindings.value[i]
89
+ // destructuring pattern not supported yet
90
+ if (!is.symbol(pattern)) return null
91
+ const compiledInit = compile(bindings.value[i + 1], currentCompileEnv)
92
+ // unsuported init, bail out
93
+ if (compiledInit === null) return null
94
+ const slot: SlotRef = { value: null }
95
+ slotInits.push([slot, compiledInit])
96
+ namedSlots.push([pattern.name, slot])
97
+
98
+ currentCompileEnv = {
99
+ bindings: new Map([[pattern.name, slot]]),
100
+ outer: currentCompileEnv,
101
+ }
102
+ }
103
+ const slots = slotInits.map((entry) => entry[0])
104
+ const recurTarget: { args: CljValue[] | null } = { args: null }
105
+ // map of ALL slots, outer: compileEnv, loop: { slots, recurTarget }
106
+ const loopCompileEnv = {
107
+ bindings: new Map(namedSlots),
108
+ outer: compileEnv,
109
+ loop: {
110
+ slots,
111
+ recurTarget,
112
+ },
113
+ }
114
+ const compiledBody = compileDo(body, loopCompileEnv, compile)
115
+ if (compiledBody === null) return null
116
+
117
+ return (env, ctx) => {
118
+ for (const [slot, compiledInit] of slotInits) {
119
+ slot.value = compiledInit(env, ctx)
120
+ }
121
+
122
+ while (true) {
123
+ recurTarget.args = null
124
+ const result = compiledBody(env, ctx)
125
+ if (recurTarget.args !== null) {
126
+ // rebind!
127
+ for (let i = 0; i < slots.length; i++) {
128
+ slots[i].value = recurTarget.args[i]
129
+ }
130
+ } else {
131
+ return result
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ export function compileRecur(
138
+ node: CljList,
139
+ compileEnv: CompileEnv | null,
140
+ compile: CompileFn
141
+ ): CompiledExpr | null {
142
+ const loopInfo = findLoopTarget(compileEnv)
143
+ // no compiler loop in scope, bail out
144
+ // this will fallback to a thrown CljSignal in the interpreter
145
+ if (loopInfo === null) return null
146
+ const { recurTarget, slots } = loopInfo
147
+ const argForms = node.value.slice(BINDINGS_POS)
148
+ // Arity mismatch, bail out
149
+ if (argForms.length !== slots.length) return null
150
+
151
+ const compiledArgs: CompiledExpr[] = []
152
+ for (const arg of argForms) {
153
+ const compiled = compile(arg, compileEnv)
154
+ if (compiled === null) return null
155
+ compiledArgs.push(compiled)
156
+ }
157
+ return (env, ctx) => {
158
+ // important: evaluate ALL new values before writting ANY slot
159
+ const newArgs = compiledArgs.map((compiledArg) => compiledArg(env, ctx))
160
+ recurTarget.args = newArgs
161
+ // return value ignored, loop checks recurTarget.args
162
+ return v.nil()
163
+ }
164
+ }
@@ -0,0 +1,41 @@
1
+ import { is } from '../assertions.ts'
2
+ import { EvaluationError } from '../errors.ts'
3
+ import { dispatchMultiMethod } from '../evaluator/dispatch.ts'
4
+ import { maybeHydrateErrorPos } from '../positions.ts'
5
+ import { printString } from '../printer.ts'
6
+ import type { CljList, CompiledExpr, CompileEnv, CompileFn } from '../types.ts'
7
+
8
+ export function compileCall(
9
+ node: CljList,
10
+ compileEnv: CompileEnv | null,
11
+ compile: CompileFn
12
+ ): CompiledExpr | null {
13
+ const head = node.value[0]
14
+ const compiledOp = compile(head, compileEnv)
15
+ if (compiledOp === null) return null
16
+ const compiledArgs: CompiledExpr[] = []
17
+ for (const arg of node.value.slice(1)) {
18
+ const compiled = compile(arg, compileEnv)
19
+ // Uncompilable argument, bail out
20
+ if (compiled === null) return null
21
+ compiledArgs.push(compiled)
22
+ }
23
+ return (env, ctx) => {
24
+ const op = compiledOp(env, ctx)
25
+ if (is.multiMethod(op)) {
26
+ const args = compiledArgs.map((c) => c!(env, ctx))
27
+ return dispatchMultiMethod(op, args, ctx, env)
28
+ }
29
+ if (!is.callable(op)) {
30
+ const name = is.symbol(head) ? head.name : printString(head)
31
+ throw new EvaluationError(`${name} is not callable`, { list: node, env })
32
+ }
33
+ const args = compiledArgs.map((carg) => carg!(env, ctx))
34
+ try {
35
+ return ctx.applyCallable(op, args, env)
36
+ } catch (ex) {
37
+ maybeHydrateErrorPos(ex, node)
38
+ throw ex
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,40 @@
1
+ import type { CompileEnv, SlotRef } from '../types.ts'
2
+
3
+ /**
4
+ * Finds a slot in the compile environment by symbol name.
5
+ * Returns null if the slot is not found.
6
+ * Slots are equivalent to local bindings in the interpreter.
7
+ * The difference is that slots use array indexing instead of recursive name lookup.
8
+ * Slots are allocated at compile time, which reduces object allocation overhead.
9
+ * The values are swapped temporarily duration evaluation of compiled code.
10
+ */
11
+ export function findSlot(
12
+ symbolName: string,
13
+ compileEnv: CompileEnv | null
14
+ ): SlotRef | null {
15
+ let current: CompileEnv | null = compileEnv
16
+ while (current) {
17
+ const slot = current.bindings.get(symbolName)
18
+ if (slot !== undefined) return slot
19
+ current = current.outer
20
+ }
21
+ return null
22
+ }
23
+
24
+ /**
25
+ * Finds the closest loop target in the compile environment.
26
+ * Used for loop/recur compilation, this eliminates the overhead of throwing a RecurSignal,
27
+ * The strategy used by the evaluator. Instead, a loop start marks a target in the compileEnv
28
+ * the recur finds the nearest target and updates the bindings.
29
+ * The compiled loop will observe the bindings after evaluating the body,
30
+ * while they are not null, the loop will continue.
31
+ */
32
+ export function findLoopTarget(compileEnv: CompileEnv | null) {
33
+ if (compileEnv === null) return null
34
+ let current: CompileEnv | null = compileEnv
35
+ while (current) {
36
+ if (current.loop) return current.loop
37
+ current = current.outer
38
+ }
39
+ return null
40
+ }
@@ -0,0 +1,79 @@
1
+ import { is } from '../assertions.ts'
2
+ import { v } from '../factories.ts'
3
+ import type {
4
+ CljList,
5
+ CljValue,
6
+ CompiledExpr,
7
+ CompileEnv,
8
+ CompileFn,
9
+ } from '../types.ts'
10
+
11
+ const IF_TEST_POS = 1
12
+ const IF_THEN_POS = 2
13
+ const IF_ELSE_POS = 3
14
+
15
+ /**
16
+ * Compiles an if expression to a js closure.
17
+ * Short-circuits: only evaluates taken branch after test result.
18
+ * Returns null if test, then, or else cannot be compiled.
19
+ * Falling back to the interpreter.
20
+ *
21
+ * (if test then else?)
22
+ */
23
+ export function compileIf(
24
+ node: CljList,
25
+ compileEnv: CompileEnv | null,
26
+ compile: CompileFn
27
+ ): CompiledExpr | null {
28
+ const compiledTest = compile(node.value[IF_TEST_POS], compileEnv)
29
+ const compiledThen = compile(node.value[IF_THEN_POS], compileEnv)
30
+ const hasElse = node.value.length > IF_ELSE_POS
31
+ const compiledElse = hasElse
32
+ ? compile(node.value[IF_ELSE_POS], compileEnv)
33
+ : null
34
+ if (
35
+ compiledTest === null ||
36
+ compiledThen === null ||
37
+ (hasElse && compiledElse === null)
38
+ ) {
39
+ return null
40
+ }
41
+
42
+ return (env, ctx) => {
43
+ if (is.truthy(compiledTest(env, ctx))) {
44
+ return compiledThen(env, ctx)
45
+ } else {
46
+ return compiledElse ? compiledElse(env, ctx) : v.nil()
47
+ }
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Compiles a do expression to a js closure.
53
+ * Evaluates all forms in sequence, returns last result.
54
+ * Returns null if any form cannot be compiled.
55
+ * Falling back to the interpreter.
56
+ *
57
+ * (do e1 e2 ... eN)
58
+ */
59
+ export function compileDo(
60
+ node: CljValue[],
61
+ compileEnv: CompileEnv | null,
62
+ compile: CompileFn
63
+ ): CompiledExpr | null {
64
+ const compiledForms: CompiledExpr[] = []
65
+ for (const form of node) {
66
+ const compiled = compile(form, compileEnv)
67
+ // Can't compile a form, so bail out
68
+ if (compiled === null) return null
69
+ compiledForms.push(compiled)
70
+ }
71
+
72
+ return (env, ctx) => {
73
+ let result: CljValue = v.nil()
74
+ for (const compiled of compiledForms) {
75
+ result = compiled(env, ctx)
76
+ }
77
+ return result
78
+ }
79
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Compiler — Incremental Implementation
3
+ *
4
+ * Transforms AST nodes into compiled closures that eliminate
5
+ * interpreter dispatch overhead. Supports:
6
+ * - Phase 1: Literals, symbols
7
+ * - Phase 2: if, do
8
+ * - Phase 3: let with slot indexing
9
+ * - Phase 4: fn with compile-once caching
10
+ * - Phase 5: loop/recur → while
11
+ *
12
+ * Returns null for unsupported forms (fallback to interpreter).
13
+ * See ./evaluate.ts:evaluateWithContext for the entry point.
14
+ */
15
+ import { is } from '../assertions.ts'
16
+ import { lookup } from '../env.ts'
17
+ import { specialFormKeywords, valueKeywords } from '../keywords.ts'
18
+ import {
19
+ type CljList,
20
+ type CljSymbol,
21
+ type CljValue,
22
+ type CompiledExpr,
23
+ type CompileEnv,
24
+ type CompileFn,
25
+ } from '../types.ts'
26
+ import { compileLet, compileLoop, compileRecur } from './binding.ts'
27
+ import { compileCall } from './callable.ts'
28
+ import { findSlot } from './compile-env.ts'
29
+ import { compileDo, compileIf } from './control-flow.ts'
30
+
31
+ /**
32
+ * Export the compiler functions for use in the evaluator
33
+ * Ideally external consumers should only use the compile function,
34
+ * not it's children!
35
+ */
36
+ export { compileDo, compileIf, compileLet, compileLoop, compileRecur }
37
+
38
+ function compileList(
39
+ node: CljList,
40
+ compileEnv: CompileEnv | null,
41
+ compile: CompileFn
42
+ ): CompiledExpr | null {
43
+ if (node.value.length === 0) return () => node
44
+ const head = node.value[0]
45
+ // First check supported special forms
46
+ if (is.symbol(head)) {
47
+ switch (head.name) {
48
+ case specialFormKeywords.if:
49
+ return compileIf(node, compileEnv, compile)
50
+ case specialFormKeywords.do:
51
+ return compileDo(node.value.slice(1), compileEnv, compile)
52
+ case specialFormKeywords.let:
53
+ return compileLet(node, compileEnv, compile)
54
+ case specialFormKeywords.loop:
55
+ return compileLoop(node, compileEnv, compile)
56
+ case specialFormKeywords.recur:
57
+ return compileRecur(node, compileEnv, compile)
58
+ }
59
+ }
60
+
61
+ if (!is.specialForm(head)) {
62
+ // Otherwise, compile as a callable
63
+ return compileCall(node, compileEnv, compile)
64
+ }
65
+
66
+ // Unsupported form, bail out
67
+ return null
68
+ }
69
+
70
+ /**
71
+ * Compiles a symbol to a compiled expression.
72
+ * It will look for the symbol as a slot in the compile environment,
73
+ * if found, it will return a closure that directly accesses the slot value.
74
+ * If not found, it will return a closure that looks up the symbol
75
+ * in the local evaluation environment.
76
+ */
77
+ function compileSymbol(
78
+ node: CljSymbol,
79
+ compileEnv: CompileEnv | null
80
+ ): CompiledExpr | null {
81
+ const symbolName = node.name
82
+ const slashIdx = symbolName.indexOf('/')
83
+ if (slashIdx > 0 && slashIdx < symbolName.length - 1) {
84
+ // qualified symbol not supported yet
85
+ return null
86
+ }
87
+ const slot = findSlot(symbolName, compileEnv)
88
+ if (slot !== null) {
89
+ return (_env, _ctx) => slot.value! // direct slot access, no lookup
90
+ }
91
+ // Regular lookup
92
+ return (env, _ctx) => lookup(symbolName, env)
93
+ }
94
+
95
+ /**
96
+ * A pure function that compiles a node to a compiled expression.
97
+ * The goal is to remove the overhead of interpreting the AST structure.
98
+ * Non-supported nodes return null and fallback to the evaluator.
99
+ */
100
+ export function compile(
101
+ node: CljValue,
102
+ compileEnv: CompileEnv | null = null
103
+ ): CompiledExpr | null {
104
+ switch (node.kind) {
105
+ // Self evaluating forms compile to constant closures
106
+ case valueKeywords.number:
107
+ case valueKeywords.string:
108
+ case valueKeywords.keyword:
109
+ case valueKeywords.nil:
110
+ case valueKeywords.boolean:
111
+ case valueKeywords.regex:
112
+ return () => node
113
+ case valueKeywords.symbol: {
114
+ return compileSymbol(node, compileEnv)
115
+ }
116
+ case valueKeywords.list: {
117
+ return compileList(node, compileEnv, compile)
118
+ }
119
+ }
120
+ return null
121
+ }
package/src/core/env.ts CHANGED
@@ -43,9 +43,9 @@ export function lookup(name: string, env: Env): CljValue {
43
43
  // Local bindings are stored as plain values — do NOT auto-deref.
44
44
  // A var stored in a local binding (e.g. from `(var foo)`) is a first-class value.
45
45
  if (raw !== undefined) return raw
46
- const v = current.ns?.vars.get(name)
46
+ const theVar = current.ns?.vars.get(name)
47
47
  // Namespace vars are always auto-deref'd: `foo` resolves to the var's current value.
48
- if (v !== undefined) return derefValue(v)
48
+ if (theVar !== undefined) return derefValue(theVar)
49
49
  current = current.outer
50
50
  }
51
51
  throw new EvaluationError(`Symbol ${name} not found`, { name })
@@ -56,8 +56,8 @@ export function tryLookup(name: string, env: Env): CljValue | undefined {
56
56
  while (current) {
57
57
  const raw = current.bindings.get(name)
58
58
  if (raw !== undefined) return raw
59
- const v = current.ns?.vars.get(name)
60
- if (v !== undefined) return derefValue(v)
59
+ const theVar = current.ns?.vars.get(name)
60
+ if (theVar !== undefined) return derefValue(theVar)
61
61
  current = current.outer
62
62
  }
63
63
  return undefined
@@ -45,3 +45,4 @@ export class CljThrownSignal {
45
45
  this.value = value
46
46
  }
47
47
  }
48
+
@@ -1,6 +1,7 @@
1
1
  import { is } from '../assertions'
2
2
  import { EvaluationError } from '../errors'
3
3
  import { cljNil } from '../factories'
4
+ import { valueKeywords } from '../keywords'
4
5
  import { printString } from '../printer'
5
6
  import type {
6
7
  CljFunction,
@@ -19,14 +20,14 @@ export function applyFunctionWithContext(
19
20
  ctx: EvaluationContext,
20
21
  callEnv: Env
21
22
  ): CljValue {
22
- if (fn.kind === 'native-function') {
23
+ if (fn.kind === valueKeywords.nativeFunction) {
23
24
  // New path, native fns receive evaluation context as first argument
24
25
  if (fn.fnWithContext) {
25
26
  return fn.fnWithContext(ctx, callEnv, ...args)
26
27
  }
27
28
  return fn.fn(...args)
28
29
  }
29
- if (fn.kind === 'function') {
30
+ if (fn.kind === valueKeywords.function) {
30
31
  const arity = resolveArity(fn.arities, args.length)
31
32
  let currentArgs = args
32
33
  while (true) {
@@ -39,6 +40,9 @@ export function applyFunctionWithContext(
39
40
  callEnv
40
41
  )
41
42
  try {
43
+ if (arity.compiledBody) {
44
+ return arity.compiledBody(localEnv, ctx)
45
+ }
42
46
  return ctx.evaluateForms(arity.body, localEnv)
43
47
  } catch (e) {
44
48
  if (e instanceof RecurSignal) {
@@ -91,7 +95,7 @@ export function applyCallableWithContext(
91
95
  return applyFunctionWithContext(fn, args, ctx, callEnv)
92
96
  }
93
97
  if (is.jsValue(fn)) {
94
- if (typeof fn.value !== 'function') {
98
+ if (typeof fn.value !== valueKeywords.function) {
95
99
  throw new EvaluationError(
96
100
  `js-value is not callable: ${typeof fn.value}`,
97
101
  { fn, args }
@@ -12,6 +12,8 @@ import type {
12
12
  } from '../types'
13
13
  import { destructureBindings } from './destructure'
14
14
 
15
+ const REST_SYMBOL = '&'
16
+
15
17
  export class RecurSignal {
16
18
  args: CljValue[]
17
19
  constructor(args: CljValue[]) {
@@ -23,24 +25,32 @@ export function parseParamVector(
23
25
  args: CljVector,
24
26
  env: Env
25
27
  ): { params: DestructurePattern[]; restParam: DestructurePattern | null } {
26
- const ampIdx = args.value.findIndex((a) => is.symbol(a) && a.name === '&')
28
+ const ampIdx = args.value.findIndex(
29
+ (a) => is.symbol(a) && a.name === REST_SYMBOL
30
+ )
27
31
  let params: DestructurePattern[] = []
28
32
  let restParam: DestructurePattern | null = null
29
33
  if (ampIdx === -1) {
30
34
  params = args.value as DestructurePattern[]
31
35
  } else {
32
36
  const ampsCount = args.value.filter(
33
- (a) => is.symbol(a) && a.name === '&'
37
+ (a) => is.symbol(a) && a.name === REST_SYMBOL
34
38
  ).length
35
39
  if (ampsCount > 1) {
36
- throw new EvaluationError('& can only appear once', { args, env })
37
- }
38
- if (ampIdx !== args.value.length - 2) {
39
- throw new EvaluationError('& must be second-to-last argument', {
40
+ throw new EvaluationError(`${REST_SYMBOL} can only appear once`, {
40
41
  args,
41
42
  env,
42
43
  })
43
44
  }
45
+ if (ampIdx !== args.value.length - 2) {
46
+ throw new EvaluationError(
47
+ `${REST_SYMBOL} must be second-to-last argument`,
48
+ {
49
+ args,
50
+ env,
51
+ }
52
+ )
53
+ }
44
54
  params = args.value.slice(0, ampIdx) as DestructurePattern[]
45
55
  restParam = args.value[ampIdx + 1] as DestructurePattern
46
56
  }