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.
- package/dist-cli/conjure-js.mjs +9336 -5028
- package/dist-vite-plugin/index.mjs +10455 -0
- package/package.json +9 -2
- package/src/bin/cli.ts +2 -2
- package/src/bin/nrepl-symbol.ts +150 -0
- package/src/bin/nrepl.ts +301 -157
- package/src/bin/version.ts +1 -1
- package/src/clojure/core.clj +764 -29
- package/src/clojure/core.clj.d.ts +76 -4
- package/src/clojure/demo/math.clj +5 -1
- package/src/clojure/generated/builtin-namespace-registry.ts +4 -0
- package/src/clojure/generated/clojure-core-source.ts +765 -29
- package/src/clojure/generated/clojure-set-source.ts +136 -0
- package/src/clojure/generated/clojure-walk-source.ts +72 -0
- package/src/clojure/set.clj +132 -0
- package/src/clojure/set.clj.d.ts +20 -0
- package/src/clojure/string.clj.d.ts +14 -0
- package/src/clojure/walk.clj +68 -0
- package/src/clojure/walk.clj.d.ts +7 -0
- package/src/core/assertions.ts +114 -6
- package/src/core/bootstrap.ts +337 -0
- package/src/core/conversions.ts +48 -31
- package/src/core/core-module.ts +303 -0
- package/src/core/env.ts +42 -7
- package/src/core/errors.ts +8 -0
- package/src/core/evaluator/apply.ts +40 -25
- package/src/core/evaluator/arity.ts +8 -8
- package/src/core/evaluator/async-evaluator.ts +565 -0
- package/src/core/evaluator/collections.ts +30 -4
- package/src/core/evaluator/destructure.ts +180 -69
- package/src/core/evaluator/dispatch.ts +24 -14
- package/src/core/evaluator/evaluate.ts +22 -20
- package/src/core/evaluator/expand.ts +45 -15
- package/src/core/evaluator/form-parsers.ts +178 -0
- package/src/core/evaluator/index.ts +7 -9
- package/src/core/evaluator/js-interop.ts +189 -0
- package/src/core/evaluator/quasiquote.ts +14 -8
- package/src/core/evaluator/recur-check.ts +6 -6
- package/src/core/evaluator/special-forms.ts +380 -173
- package/src/core/factories.ts +182 -3
- package/src/core/index.ts +55 -5
- package/src/core/module.ts +136 -0
- package/src/core/ns-forms.ts +107 -0
- package/src/core/positions.ts +9 -2
- package/src/core/printer.ts +371 -11
- package/src/core/reader.ts +127 -29
- package/src/core/registry.ts +209 -0
- package/src/core/runtime.ts +376 -0
- package/src/core/session.ts +263 -478
- package/src/core/stdlib/arithmetic.ts +516 -215
- package/src/core/stdlib/async-fns.ts +132 -0
- package/src/core/stdlib/atoms.ts +286 -63
- package/src/core/stdlib/errors.ts +54 -50
- package/src/core/stdlib/hof.ts +74 -173
- package/src/core/stdlib/js-namespace.ts +344 -0
- package/src/core/stdlib/lazy.ts +34 -0
- package/src/core/stdlib/maps-sets.ts +322 -0
- package/src/core/stdlib/meta.ts +109 -28
- package/src/core/stdlib/predicates.ts +322 -196
- package/src/core/stdlib/regex.ts +126 -98
- package/src/core/stdlib/seq.ts +564 -0
- package/src/core/stdlib/strings.ts +164 -135
- package/src/core/stdlib/transducers.ts +95 -100
- package/src/core/stdlib/utils.ts +283 -147
- package/src/core/stdlib/vars.ts +27 -27
- package/src/core/stdlib/vectors.ts +122 -0
- package/src/core/tokenizer.ts +13 -3
- package/src/core/transformations.ts +117 -9
- package/src/core/types.ts +118 -6
- package/src/host/node-host-module.ts +74 -0
- package/src/nrepl/relay.ts +432 -0
- package/src/vite-plugin-clj/codegen.ts +87 -95
- package/src/vite-plugin-clj/index.ts +242 -18
- package/src/vite-plugin-clj/namespace-utils.ts +39 -0
- package/src/vite-plugin-clj/static-analysis.ts +211 -0
- package/src/clojure/demo.clj +0 -63
- package/src/clojure/demo.clj.d.ts +0 -0
- package/src/core/core-env.ts +0 -60
- package/src/core/stdlib/collections.ts +0 -784
- package/src/host/node.ts +0 -55
|
@@ -1,32 +1,24 @@
|
|
|
1
|
+
import { is } from '../assertions'
|
|
1
2
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
85
|
+
return v.nativeFn(`kw:${kw.name}`, (...args: CljValue[]) => {
|
|
61
86
|
const target = args[0]
|
|
62
|
-
if (!
|
|
63
|
-
const entry = target.entries.find(([k]) =>
|
|
64
|
-
return entry ? entry[1] :
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 =
|
|
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 =
|
|
162
|
-
[
|
|
163
|
-
[
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (!
|
|
252
|
+
if (!is.falsy(condition)) {
|
|
250
253
|
return ctx.evaluate(list.value[2], env)
|
|
251
254
|
}
|
|
252
255
|
if (!list.value[3]) {
|
|
253
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
379
|
+
ctx: EvaluationContext
|
|
312
380
|
): CljValue {
|
|
313
381
|
const name = list.value[1]
|
|
314
|
-
if (!
|
|
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
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 (
|
|
487
|
+
if (is.keyword(dispatchFnExpr)) {
|
|
425
488
|
dispatchFn = keywordToDispatchFn(dispatchFnExpr)
|
|
426
489
|
} else {
|
|
427
490
|
const evaluated = ctx.evaluate(dispatchFnExpr, env)
|
|
428
|
-
if (!
|
|
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 =
|
|
437
|
-
|
|
438
|
-
return
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
463
|
-
const isDefault =
|
|
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 =
|
|
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) => !
|
|
537
|
+
(m) => !is.equal(m.dispatchVal, dispatchVal)
|
|
475
538
|
)
|
|
476
|
-
updated =
|
|
539
|
+
updated = v.multiMethod(existing.name, existing.dispatchFn, [
|
|
477
540
|
...filtered,
|
|
478
541
|
{ dispatchVal, fn: methodFn },
|
|
479
542
|
])
|
|
480
543
|
}
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
557
|
+
ctx: EvaluationContext
|
|
489
558
|
): CljValue {
|
|
490
559
|
const sym = list.value[1]
|
|
491
|
-
if (!
|
|
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
|
-
//
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
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 =
|
|
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
|