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.
- package/dist-cli/conjure-js.mjs +2328 -2089
- package/dist-vite-plugin/index.mjs +2327 -2088
- package/package.json +1 -1
- package/src/bin/version.ts +1 -1
- package/src/core/assertions.ts +10 -3
- package/src/core/bootstrap.ts +7 -23
- package/src/core/compiler/binding.ts +164 -0
- package/src/core/compiler/callable.ts +41 -0
- package/src/core/compiler/compile-env.ts +40 -0
- package/src/core/compiler/control-flow.ts +79 -0
- package/src/core/compiler/index.ts +121 -0
- package/src/core/env.ts +4 -4
- package/src/core/errors.ts +1 -0
- package/src/core/evaluator/apply.ts +7 -3
- package/src/core/evaluator/arity.ts +16 -6
- package/src/core/evaluator/async-evaluator.ts +68 -89
- package/src/core/evaluator/collections.ts +9 -4
- package/src/core/evaluator/destructure.ts +45 -55
- package/src/core/evaluator/dispatch.ts +21 -24
- package/src/core/evaluator/evaluate.ts +14 -2
- package/src/core/evaluator/expand.ts +5 -7
- package/src/core/evaluator/js-interop.ts +46 -33
- package/src/core/evaluator/quasiquote.ts +7 -11
- package/src/core/evaluator/recur-check.ts +1 -1
- package/src/core/evaluator/special-forms.ts +18 -38
- package/src/core/index.ts +1 -1
- package/src/core/keywords.ts +105 -0
- package/src/core/modules/core/index.ts +131 -0
- package/src/core/{stdlib → modules/core/stdlib}/arithmetic.ts +6 -6
- package/src/core/{stdlib → modules/core/stdlib}/async-fns.ts +6 -6
- package/src/core/{stdlib → modules/core/stdlib}/atoms.ts +7 -7
- package/src/core/{stdlib → modules/core/stdlib}/errors.ts +4 -4
- package/src/core/{stdlib → modules/core/stdlib}/hof.ts +6 -6
- package/src/core/{stdlib → modules/core/stdlib}/lazy.ts +4 -4
- package/src/core/{stdlib → modules/core/stdlib}/maps-sets.ts +6 -6
- package/src/core/{stdlib → modules/core/stdlib}/meta.ts +5 -5
- package/src/core/{stdlib → modules/core/stdlib}/predicates.ts +7 -7
- package/src/core/modules/core/stdlib/print.ts +108 -0
- package/src/core/{stdlib → modules/core/stdlib}/regex.ts +5 -5
- package/src/core/{stdlib → modules/core/stdlib}/seq.ts +7 -7
- package/src/core/{stdlib → modules/core/stdlib}/strings.ts +6 -6
- package/src/core/{stdlib → modules/core/stdlib}/transducers.ts +6 -6
- package/src/core/{stdlib → modules/core/stdlib}/utils.ts +10 -10
- package/src/core/{stdlib → modules/core/stdlib}/vars.ts +4 -4
- package/src/core/{stdlib → modules/core/stdlib}/vectors.ts +6 -6
- package/src/core/modules/js/index.ts +402 -0
- package/src/core/ns-forms.ts +25 -17
- package/src/core/positions.ts +22 -2
- package/src/core/printer.ts +162 -53
- package/src/core/reader.ts +25 -22
- package/src/core/registry.ts +10 -10
- package/src/core/runtime.ts +23 -23
- package/src/core/session.ts +17 -7
- package/src/core/tokenizer.ts +14 -4
- package/src/core/transformations.ts +48 -29
- package/src/core/types.ts +57 -81
- package/src/core/core-module.ts +0 -303
- package/src/core/stdlib/js-namespace.ts +0 -344
package/package.json
CHANGED
package/src/bin/version.ts
CHANGED
package/src/core/assertions.ts
CHANGED
|
@@ -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
|
-
|
|
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) ||
|
|
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
|
}
|
package/src/core/bootstrap.ts
CHANGED
|
@@ -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' &&
|
|
116
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
60
|
-
if (
|
|
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
|
package/src/core/errors.ts
CHANGED
|
@@ -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 ===
|
|
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 ===
|
|
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 !==
|
|
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(
|
|
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(
|
|
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
|
}
|