conjure-js 0.0.1
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/conjure +0 -0
- package/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/dist/assets/editor.worker-CdQrwHl8.js +26 -0
- package/dist/assets/main-A7ZMId9A.css +1 -0
- package/dist/assets/main-CmI-7epE.js +3137 -0
- package/dist/index.html +195 -0
- package/dist/vite.svg +1 -0
- package/package.json +68 -0
- package/src/bin/__fixtures__/smoke/app/lib.clj +4 -0
- package/src/bin/__fixtures__/smoke/app/main.clj +4 -0
- package/src/bin/__fixtures__/smoke/repl-smoke.ts +12 -0
- package/src/bin/bencode.ts +205 -0
- package/src/bin/cli.ts +250 -0
- package/src/bin/nrepl-utils.ts +59 -0
- package/src/bin/nrepl.ts +393 -0
- package/src/bin/version.ts +4 -0
- package/src/clojure/core.clj +620 -0
- package/src/clojure/core.clj.d.ts +189 -0
- package/src/clojure/demo/math.clj +16 -0
- package/src/clojure/demo/math.clj.d.ts +4 -0
- package/src/clojure/demo.clj +42 -0
- package/src/clojure/demo.clj.d.ts +0 -0
- package/src/clojure/generated/builtin-namespace-registry.ts +14 -0
- package/src/clojure/generated/clojure-core-source.ts +623 -0
- package/src/clojure/generated/clojure-string-source.ts +196 -0
- package/src/clojure/string.clj +192 -0
- package/src/clojure/string.clj.d.ts +25 -0
- package/src/core/assertions.ts +134 -0
- package/src/core/conversions.ts +108 -0
- package/src/core/core-env.ts +58 -0
- package/src/core/env.ts +78 -0
- package/src/core/errors.ts +39 -0
- package/src/core/evaluator/apply.ts +114 -0
- package/src/core/evaluator/arity.ts +174 -0
- package/src/core/evaluator/collections.ts +25 -0
- package/src/core/evaluator/destructure.ts +247 -0
- package/src/core/evaluator/dispatch.ts +73 -0
- package/src/core/evaluator/evaluate.ts +100 -0
- package/src/core/evaluator/expand.ts +79 -0
- package/src/core/evaluator/index.ts +72 -0
- package/src/core/evaluator/quasiquote.ts +87 -0
- package/src/core/evaluator/recur-check.ts +109 -0
- package/src/core/evaluator/special-forms.ts +517 -0
- package/src/core/factories.ts +155 -0
- package/src/core/gensym.ts +9 -0
- package/src/core/index.ts +76 -0
- package/src/core/positions.ts +38 -0
- package/src/core/printer.ts +86 -0
- package/src/core/reader.ts +559 -0
- package/src/core/scanners.ts +93 -0
- package/src/core/session.ts +610 -0
- package/src/core/stdlib/arithmetic.ts +361 -0
- package/src/core/stdlib/atoms.ts +88 -0
- package/src/core/stdlib/collections.ts +784 -0
- package/src/core/stdlib/errors.ts +81 -0
- package/src/core/stdlib/hof.ts +307 -0
- package/src/core/stdlib/meta.ts +48 -0
- package/src/core/stdlib/predicates.ts +240 -0
- package/src/core/stdlib/regex.ts +238 -0
- package/src/core/stdlib/strings.ts +311 -0
- package/src/core/stdlib/transducers.ts +256 -0
- package/src/core/stdlib/utils.ts +287 -0
- package/src/core/tokenizer.ts +437 -0
- package/src/core/transformations.ts +75 -0
- package/src/core/types.ts +258 -0
- package/src/main.ts +1 -0
- package/src/monaco-esm.d.ts +7 -0
- package/src/playground/clojure-tokens.ts +67 -0
- package/src/playground/editor.worker.ts +5 -0
- package/src/playground/find-form.ts +138 -0
- package/src/playground/playground.ts +342 -0
- package/src/playground/samples/00-welcome.clj +385 -0
- package/src/playground/samples/01-collections.clj +191 -0
- package/src/playground/samples/02-higher-order-functions.clj +215 -0
- package/src/playground/samples/03-destructuring.clj +194 -0
- package/src/playground/samples/04-strings-and-regex.clj +202 -0
- package/src/playground/samples/05-error-handling.clj +212 -0
- package/src/repl/repl.ts +116 -0
- package/tsconfig.build.json +10 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import { isKeyword, isList, isSymbol, isVector } from './assertions'
|
|
2
|
+
import { loadCoreFunctions } from './core-env'
|
|
3
|
+
import { define, lookup, makeEnv, tryLookup } from './env'
|
|
4
|
+
import { valueToString } from './transformations'
|
|
5
|
+
import { createEvaluationContext, RecurSignal } from './evaluator'
|
|
6
|
+
import { CljThrownSignal, EvaluationError, ReaderError } from './errors'
|
|
7
|
+
import { cljNativeFunction, cljNil } from './factories'
|
|
8
|
+
import { formatErrorContext } from './positions'
|
|
9
|
+
import { printString } from './printer'
|
|
10
|
+
import { readForms } from './reader'
|
|
11
|
+
import { tokenize } from './tokenizer'
|
|
12
|
+
import type { CljValue, Env, Token, TokenSymbol } from './types'
|
|
13
|
+
import { builtInNamespaceSources } from '../clojure/generated/builtin-namespace-registry'
|
|
14
|
+
|
|
15
|
+
type NamespaceRegistry = Map<string, Env>
|
|
16
|
+
|
|
17
|
+
type SessionOptions = {
|
|
18
|
+
output?: (text: string) => void
|
|
19
|
+
entries?: string[]
|
|
20
|
+
sourceRoots?: string[]
|
|
21
|
+
readFile?: (filePath: string) => string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type Session = {
|
|
25
|
+
registry: NamespaceRegistry
|
|
26
|
+
readonly currentNs: string
|
|
27
|
+
setNs: (namespace: string) => void
|
|
28
|
+
getNs: (namespace: string) => Env | null
|
|
29
|
+
loadFile: (source: string, nsName?: string) => string
|
|
30
|
+
evaluate: (source: string) => CljValue
|
|
31
|
+
evaluateForms: (forms: CljValue[]) => CljValue
|
|
32
|
+
addSourceRoot: (path: string) => void
|
|
33
|
+
getCompletions: (prefix: string, nsName?: string) => string[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Lightweight token scan to extract the namespace name before full parsing.
|
|
37
|
+
// Looks for the pattern: LParen Symbol("ns") Symbol(name) at the top of the token stream.
|
|
38
|
+
function extractNsNameFromTokens(tokens: Token[]): string | null {
|
|
39
|
+
const meaningful = tokens.filter((t) => t.kind !== 'Comment')
|
|
40
|
+
if (meaningful.length < 3) return null
|
|
41
|
+
if (meaningful[0].kind !== 'LParen') return null
|
|
42
|
+
if (meaningful[1].kind !== 'Symbol' || meaningful[1].value !== 'ns')
|
|
43
|
+
return null
|
|
44
|
+
if (meaningful[2].kind !== 'Symbol') return null
|
|
45
|
+
return meaningful[2].value
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Lightweight token scan to extract :as alias pairs from the ns form's :require clauses.
|
|
49
|
+
// Returns Map { 'alias' -> 'full.ns.name' } for all [some.ns :as alias] specs found.
|
|
50
|
+
// This runs before readForms so the reader can expand ::alias/foo at read time.
|
|
51
|
+
function extractAliasMapFromTokens(tokens: Token[]): Map<string, string> {
|
|
52
|
+
const aliases = new Map<string, string>()
|
|
53
|
+
const meaningful = tokens.filter(
|
|
54
|
+
(t) => t.kind !== 'Comment' && t.kind !== 'Whitespace'
|
|
55
|
+
)
|
|
56
|
+
// Must start with (ns ...)
|
|
57
|
+
if (meaningful.length < 3) return aliases
|
|
58
|
+
if (meaningful[0].kind !== 'LParen') return aliases
|
|
59
|
+
if (meaningful[1].kind !== 'Symbol' || meaningful[1].value !== 'ns')
|
|
60
|
+
return aliases
|
|
61
|
+
|
|
62
|
+
// Walk the top-level ns form tracking paren depth.
|
|
63
|
+
// For each [ vector ] we encounter, check for ns-sym :as alias-sym.
|
|
64
|
+
let i = 3 // skip ( ns <name>
|
|
65
|
+
let depth = 1
|
|
66
|
+
while (i < meaningful.length && depth > 0) {
|
|
67
|
+
const tok = meaningful[i]
|
|
68
|
+
if (tok.kind === 'LParen') {
|
|
69
|
+
depth++
|
|
70
|
+
i++
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
if (tok.kind === 'RParen') {
|
|
74
|
+
depth--
|
|
75
|
+
i++
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
if (tok.kind === 'LBracket') {
|
|
79
|
+
// Scan through this vector for: first Symbol (ns name) + :as + Symbol (alias)
|
|
80
|
+
let j = i + 1
|
|
81
|
+
let nsSym: string | null = null
|
|
82
|
+
while (j < meaningful.length && meaningful[j].kind !== 'RBracket') {
|
|
83
|
+
const t = meaningful[j]
|
|
84
|
+
if (t.kind === 'Symbol' && nsSym === null) {
|
|
85
|
+
nsSym = t.value
|
|
86
|
+
}
|
|
87
|
+
if (
|
|
88
|
+
t.kind === 'Keyword' &&
|
|
89
|
+
(t.value === ':as' || t.value === ':as-alias')
|
|
90
|
+
) {
|
|
91
|
+
j++
|
|
92
|
+
if (
|
|
93
|
+
j < meaningful.length &&
|
|
94
|
+
meaningful[j].kind === 'Symbol' &&
|
|
95
|
+
nsSym
|
|
96
|
+
) {
|
|
97
|
+
aliases.set((meaningful[j] as TokenSymbol).value, nsSym)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
j++
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
i++
|
|
104
|
+
}
|
|
105
|
+
return aliases
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function findNsForm(forms: CljValue[]) {
|
|
109
|
+
const nsForm = forms.find(
|
|
110
|
+
(f) => isList(f) && isSymbol(f.value[0]) && f.value[0].name === 'ns'
|
|
111
|
+
)
|
|
112
|
+
if (!nsForm || !isList(nsForm)) return null
|
|
113
|
+
return nsForm
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function extractRequireClauses(forms: CljValue[]): CljValue[][] {
|
|
117
|
+
const nsForm = findNsForm(forms)
|
|
118
|
+
if (!nsForm) return []
|
|
119
|
+
const clauses: CljValue[][] = []
|
|
120
|
+
for (let i = 2; i < nsForm.value.length; i++) {
|
|
121
|
+
const clause = nsForm.value[i]
|
|
122
|
+
if (
|
|
123
|
+
isList(clause) &&
|
|
124
|
+
isKeyword(clause.value[0]) &&
|
|
125
|
+
clause.value[0].name === ':require'
|
|
126
|
+
) {
|
|
127
|
+
clauses.push(clause.value.slice(1))
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return clauses
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function processRequireSpec(
|
|
134
|
+
spec: CljValue,
|
|
135
|
+
currentEnv: Env,
|
|
136
|
+
registry: NamespaceRegistry,
|
|
137
|
+
resolveNamespace?: (nsName: string) => boolean
|
|
138
|
+
): void {
|
|
139
|
+
if (!isVector(spec)) {
|
|
140
|
+
throw new EvaluationError(
|
|
141
|
+
'require spec must be a vector, e.g. [my.ns :as alias]',
|
|
142
|
+
{ spec }
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const elements = spec.value
|
|
147
|
+
if (elements.length === 0 || !isSymbol(elements[0])) {
|
|
148
|
+
throw new EvaluationError(
|
|
149
|
+
'First element of require spec must be a namespace symbol',
|
|
150
|
+
{ spec }
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const nsName = elements[0].name
|
|
155
|
+
|
|
156
|
+
// :as-alias creates a reader alias without loading the namespace.
|
|
157
|
+
// The namespace need not exist — the alias is only used for ::alias/foo expansion.
|
|
158
|
+
const hasAsAlias = elements.some(
|
|
159
|
+
(el) => isKeyword(el) && el.name === ':as-alias'
|
|
160
|
+
)
|
|
161
|
+
if (hasAsAlias) {
|
|
162
|
+
let i = 1
|
|
163
|
+
while (i < elements.length) {
|
|
164
|
+
const kw = elements[i]
|
|
165
|
+
if (!isKeyword(kw)) {
|
|
166
|
+
throw new EvaluationError(
|
|
167
|
+
`Expected keyword in require spec, got ${kw.kind}`,
|
|
168
|
+
{ spec, position: i }
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
if (kw.name === ':as-alias') {
|
|
172
|
+
i++
|
|
173
|
+
const alias = elements[i]
|
|
174
|
+
if (!alias || !isSymbol(alias)) {
|
|
175
|
+
throw new EvaluationError(':as-alias expects a symbol alias', {
|
|
176
|
+
spec,
|
|
177
|
+
position: i,
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
if (!currentEnv.readerAliases) {
|
|
181
|
+
currentEnv.readerAliases = new Map()
|
|
182
|
+
}
|
|
183
|
+
currentEnv.readerAliases.set(alias.name, nsName)
|
|
184
|
+
i++
|
|
185
|
+
} else {
|
|
186
|
+
throw new EvaluationError(
|
|
187
|
+
`:as-alias specs only support :as-alias, got ${kw.name}`,
|
|
188
|
+
{ spec }
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let targetEnv = registry.get(nsName)
|
|
196
|
+
if (!targetEnv && resolveNamespace) {
|
|
197
|
+
resolveNamespace(nsName)
|
|
198
|
+
targetEnv = registry.get(nsName)
|
|
199
|
+
}
|
|
200
|
+
if (!targetEnv) {
|
|
201
|
+
throw new EvaluationError(
|
|
202
|
+
`Namespace ${nsName} not found. Only already-loaded namespaces can be required.`,
|
|
203
|
+
{ nsName }
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let i = 1
|
|
208
|
+
while (i < elements.length) {
|
|
209
|
+
const kw = elements[i]
|
|
210
|
+
if (!isKeyword(kw)) {
|
|
211
|
+
throw new EvaluationError(
|
|
212
|
+
`Expected keyword in require spec, got ${kw.kind}`,
|
|
213
|
+
{ spec, position: i }
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (kw.name === ':as') {
|
|
218
|
+
i++
|
|
219
|
+
const alias = elements[i]
|
|
220
|
+
if (!alias || !isSymbol(alias)) {
|
|
221
|
+
throw new EvaluationError(':as expects a symbol alias', {
|
|
222
|
+
spec,
|
|
223
|
+
position: i,
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
if (!currentEnv.aliases) {
|
|
227
|
+
currentEnv.aliases = new Map()
|
|
228
|
+
}
|
|
229
|
+
currentEnv.aliases.set(alias.name, targetEnv)
|
|
230
|
+
i++
|
|
231
|
+
} else if (kw.name === ':refer') {
|
|
232
|
+
i++
|
|
233
|
+
const symsVec = elements[i]
|
|
234
|
+
if (!symsVec || !isVector(symsVec)) {
|
|
235
|
+
throw new EvaluationError(':refer expects a vector of symbols', {
|
|
236
|
+
spec,
|
|
237
|
+
position: i,
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
for (const sym of symsVec.value) {
|
|
241
|
+
if (!isSymbol(sym)) {
|
|
242
|
+
throw new EvaluationError(':refer vector must contain only symbols', {
|
|
243
|
+
spec,
|
|
244
|
+
sym,
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
let value: CljValue
|
|
248
|
+
try {
|
|
249
|
+
value = lookup(sym.name, targetEnv)
|
|
250
|
+
} catch {
|
|
251
|
+
throw new EvaluationError(
|
|
252
|
+
`Symbol ${sym.name} not found in namespace ${nsName}`,
|
|
253
|
+
{ nsName, symbol: sym.name }
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
define(sym.name, value, currentEnv)
|
|
257
|
+
}
|
|
258
|
+
i++
|
|
259
|
+
} else {
|
|
260
|
+
throw new EvaluationError(
|
|
261
|
+
`Unknown require option ${kw.name}. Supported: :as, :refer`,
|
|
262
|
+
{ spec, keyword: kw.name }
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// Clone helpers — used by snapshotSession / createSessionFromSnapshot
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
function cloneEnv(env: Env, memo: Map<Env, Env>): Env {
|
|
273
|
+
if (memo.has(env)) return memo.get(env)!
|
|
274
|
+
const cloned: Env = {
|
|
275
|
+
bindings: new Map(env.bindings),
|
|
276
|
+
outer: null,
|
|
277
|
+
namespace: env.namespace,
|
|
278
|
+
}
|
|
279
|
+
memo.set(env, cloned)
|
|
280
|
+
if (env.outer) cloned.outer = cloneEnv(env.outer, memo)
|
|
281
|
+
if (env.aliases)
|
|
282
|
+
cloned.aliases = new Map(
|
|
283
|
+
[...env.aliases].map(([k, v]) => [k, cloneEnv(v, memo)])
|
|
284
|
+
)
|
|
285
|
+
if (env.readerAliases) cloned.readerAliases = new Map(env.readerAliases)
|
|
286
|
+
return cloned
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function cloneRegistry(registry: NamespaceRegistry): NamespaceRegistry {
|
|
290
|
+
const memo = new Map<Env, Env>()
|
|
291
|
+
const next = new Map<string, Env>()
|
|
292
|
+
for (const [name, env] of registry) next.set(name, cloneEnv(env, memo))
|
|
293
|
+
return next
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
// SessionState — the minimal data a session operates on
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
type SessionState = {
|
|
301
|
+
registry: NamespaceRegistry
|
|
302
|
+
currentNs: string
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// buildSessionApi — single source of truth for all session behavior.
|
|
307
|
+
// Accepts a pre-populated registry (fresh or cloned) and wires up all
|
|
308
|
+
// closures + the public API. Always re-wires resolveNs and require since
|
|
309
|
+
// both must close over the specific registry instance they receive.
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
function buildSessionApi(
|
|
313
|
+
state: SessionState,
|
|
314
|
+
options?: SessionOptions
|
|
315
|
+
): Session {
|
|
316
|
+
const registry = state.registry
|
|
317
|
+
let currentNs = state.currentNs
|
|
318
|
+
|
|
319
|
+
const coreEnv = registry.get('clojure.core')!
|
|
320
|
+
coreEnv.resolveNs = (name: string) => registry.get(name) ?? null
|
|
321
|
+
|
|
322
|
+
// Always re-wire print/println so snapshot-derived sessions get the right
|
|
323
|
+
// emit target. Falls back to console.log when no output callback is provided.
|
|
324
|
+
const emitFn = options?.output ?? ((text: string) => console.log(text))
|
|
325
|
+
define(
|
|
326
|
+
'println',
|
|
327
|
+
cljNativeFunction('println', (...args: CljValue[]) => {
|
|
328
|
+
emitFn(args.map(valueToString).join(' '))
|
|
329
|
+
return cljNil()
|
|
330
|
+
}),
|
|
331
|
+
coreEnv
|
|
332
|
+
)
|
|
333
|
+
define(
|
|
334
|
+
'print',
|
|
335
|
+
cljNativeFunction('print', (...args: CljValue[]) => {
|
|
336
|
+
emitFn(args.map(valueToString).join(' '))
|
|
337
|
+
return cljNil()
|
|
338
|
+
}),
|
|
339
|
+
coreEnv
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
// Mutable source roots — seeded from options, growable via addSourceRoot.
|
|
343
|
+
const sourceRoots = new Set<string>(options?.sourceRoots ?? [])
|
|
344
|
+
|
|
345
|
+
function addSourceRoot(path: string) {
|
|
346
|
+
sourceRoots.add(path)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// One shared evaluation context for the lifetime of this session.
|
|
350
|
+
const ctx = createEvaluationContext()
|
|
351
|
+
|
|
352
|
+
function resolveNamespace(nsName: string): boolean {
|
|
353
|
+
const builtInLoader = builtInNamespaceSources[nsName]
|
|
354
|
+
if (builtInLoader) {
|
|
355
|
+
loadFile(builtInLoader(), nsName)
|
|
356
|
+
return true
|
|
357
|
+
}
|
|
358
|
+
if (!options?.readFile || sourceRoots.size === 0) {
|
|
359
|
+
return false
|
|
360
|
+
}
|
|
361
|
+
for (const root of sourceRoots) {
|
|
362
|
+
const filePath = `${root.replace(/\/$/, '')}/${nsName.replace(/\./g, '/')}.clj`
|
|
363
|
+
try {
|
|
364
|
+
const source = options.readFile(filePath)
|
|
365
|
+
if (source) {
|
|
366
|
+
loadFile(source)
|
|
367
|
+
return true
|
|
368
|
+
}
|
|
369
|
+
} catch {
|
|
370
|
+
continue
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return false
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function ensureNs(name: string): Env {
|
|
377
|
+
if (!registry.has(name)) {
|
|
378
|
+
const nsEnv = makeEnv(coreEnv)
|
|
379
|
+
nsEnv.namespace = name
|
|
380
|
+
registry.set(name, nsEnv)
|
|
381
|
+
}
|
|
382
|
+
return registry.get(name)!
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function setNs(name: string) {
|
|
386
|
+
ensureNs(name)
|
|
387
|
+
currentNs = name
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function getNs(name: string): Env | null {
|
|
391
|
+
return registry.get(name) ?? null
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
define(
|
|
395
|
+
'require',
|
|
396
|
+
cljNativeFunction('require', (...args: CljValue[]) => {
|
|
397
|
+
const currentEnv = registry.get(currentNs)!
|
|
398
|
+
for (const arg of args) {
|
|
399
|
+
processRequireSpec(arg, currentEnv, registry, resolveNamespace)
|
|
400
|
+
}
|
|
401
|
+
return cljNil()
|
|
402
|
+
}),
|
|
403
|
+
coreEnv
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
define(
|
|
407
|
+
'resolve',
|
|
408
|
+
cljNativeFunction('resolve', (sym: CljValue) => {
|
|
409
|
+
if (!isSymbol(sym)) return cljNil()
|
|
410
|
+
const slashIdx = sym.name.indexOf('/')
|
|
411
|
+
if (slashIdx > 0) {
|
|
412
|
+
const nsName = sym.name.slice(0, slashIdx)
|
|
413
|
+
const symName = sym.name.slice(slashIdx + 1)
|
|
414
|
+
const nsEnv = registry.get(nsName) ?? null
|
|
415
|
+
if (!nsEnv) return cljNil()
|
|
416
|
+
return tryLookup(symName, nsEnv) ?? cljNil()
|
|
417
|
+
}
|
|
418
|
+
const currentEnv = registry.get(currentNs)!
|
|
419
|
+
return tryLookup(sym.name, currentEnv) ?? cljNil()
|
|
420
|
+
}),
|
|
421
|
+
coreEnv
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
function processNsRequires(forms: CljValue[], env: Env) {
|
|
425
|
+
const requireClauses = extractRequireClauses(forms)
|
|
426
|
+
for (const specs of requireClauses) {
|
|
427
|
+
for (const spec of specs) {
|
|
428
|
+
processRequireSpec(spec, env, registry, resolveNamespace)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function loadFile(source: string, nsName?: string): string {
|
|
434
|
+
const tokens = tokenize(source)
|
|
435
|
+
const targetNs = extractNsNameFromTokens(tokens) ?? nsName ?? 'user'
|
|
436
|
+
const aliasMap = extractAliasMapFromTokens(tokens)
|
|
437
|
+
const forms = readForms(tokens, targetNs, aliasMap)
|
|
438
|
+
const env = ensureNs(targetNs)
|
|
439
|
+
processNsRequires(forms, env)
|
|
440
|
+
for (const form of forms) {
|
|
441
|
+
const expanded = ctx.expandAll(form, env)
|
|
442
|
+
ctx.evaluate(expanded, env)
|
|
443
|
+
}
|
|
444
|
+
return targetNs
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const api: Session = {
|
|
448
|
+
registry,
|
|
449
|
+
get currentNs() {
|
|
450
|
+
return currentNs
|
|
451
|
+
},
|
|
452
|
+
setNs,
|
|
453
|
+
getNs,
|
|
454
|
+
loadFile,
|
|
455
|
+
addSourceRoot,
|
|
456
|
+
evaluate(source: string) {
|
|
457
|
+
try {
|
|
458
|
+
const tokens = tokenize(source)
|
|
459
|
+
const env = getNs(currentNs)!
|
|
460
|
+
// Seed alias map from tokens (new aliases declared in this source) and
|
|
461
|
+
// from env.aliases/:as (prior require calls) and env.readerAliases/:as-alias.
|
|
462
|
+
const aliasMap = extractAliasMapFromTokens(tokens)
|
|
463
|
+
env.aliases?.forEach((nsEnv, alias) => {
|
|
464
|
+
if (nsEnv.namespace) aliasMap.set(alias, nsEnv.namespace)
|
|
465
|
+
})
|
|
466
|
+
env.readerAliases?.forEach((nsName, alias) => {
|
|
467
|
+
aliasMap.set(alias, nsName)
|
|
468
|
+
})
|
|
469
|
+
const forms = readForms(tokens, currentNs, aliasMap)
|
|
470
|
+
processNsRequires(forms, env)
|
|
471
|
+
let result: CljValue = cljNil()
|
|
472
|
+
for (const form of forms) {
|
|
473
|
+
const expanded = ctx.expandAll(form, env)
|
|
474
|
+
result = ctx.evaluate(expanded, env)
|
|
475
|
+
}
|
|
476
|
+
return result
|
|
477
|
+
} catch (e) {
|
|
478
|
+
if (e instanceof CljThrownSignal) {
|
|
479
|
+
throw new EvaluationError(
|
|
480
|
+
`Unhandled throw: ${printString(e.value)}`,
|
|
481
|
+
{ thrownValue: e.value }
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
if (e instanceof RecurSignal) {
|
|
485
|
+
throw new EvaluationError('recur called outside of loop or fn', {
|
|
486
|
+
args: e.args,
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
if (
|
|
490
|
+
(e instanceof EvaluationError || e instanceof ReaderError) &&
|
|
491
|
+
e.pos
|
|
492
|
+
) {
|
|
493
|
+
e.message += formatErrorContext(source, e.pos)
|
|
494
|
+
}
|
|
495
|
+
throw e
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
evaluateForms(forms: CljValue[]) {
|
|
499
|
+
try {
|
|
500
|
+
const env = getNs(currentNs)!
|
|
501
|
+
let result: CljValue = cljNil()
|
|
502
|
+
for (const form of forms) {
|
|
503
|
+
const expanded = ctx.expandAll(form, env)
|
|
504
|
+
result = ctx.evaluate(expanded, env)
|
|
505
|
+
}
|
|
506
|
+
return result
|
|
507
|
+
} catch (e) {
|
|
508
|
+
if (e instanceof CljThrownSignal) {
|
|
509
|
+
throw new EvaluationError(
|
|
510
|
+
`Unhandled throw: ${printString(e.value)}`,
|
|
511
|
+
{ thrownValue: e.value }
|
|
512
|
+
)
|
|
513
|
+
}
|
|
514
|
+
if (e instanceof RecurSignal) {
|
|
515
|
+
throw new EvaluationError('recur called outside of loop or fn', {
|
|
516
|
+
args: e.args,
|
|
517
|
+
})
|
|
518
|
+
}
|
|
519
|
+
throw e
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
getCompletions(prefix: string, nsName?: string): string[] {
|
|
523
|
+
let env: Env | null = registry.get(nsName ?? currentNs) ?? null
|
|
524
|
+
const seen = new Set<string>()
|
|
525
|
+
while (env) {
|
|
526
|
+
for (const key of env.bindings.keys()) seen.add(key)
|
|
527
|
+
env = env.outer
|
|
528
|
+
}
|
|
529
|
+
const candidates = [...seen]
|
|
530
|
+
if (!prefix) return candidates.sort()
|
|
531
|
+
return candidates.filter((k) => k.startsWith(prefix)).sort()
|
|
532
|
+
},
|
|
533
|
+
}
|
|
534
|
+
return api
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ---------------------------------------------------------------------------
|
|
538
|
+
// Public API
|
|
539
|
+
// ---------------------------------------------------------------------------
|
|
540
|
+
|
|
541
|
+
export function createSession(options?: SessionOptions): Session {
|
|
542
|
+
const registry: NamespaceRegistry = new Map()
|
|
543
|
+
|
|
544
|
+
const coreEnv = makeEnv()
|
|
545
|
+
coreEnv.namespace = 'clojure.core'
|
|
546
|
+
loadCoreFunctions(coreEnv, options?.output)
|
|
547
|
+
registry.set('clojure.core', coreEnv)
|
|
548
|
+
|
|
549
|
+
const userEnv = makeEnv(coreEnv)
|
|
550
|
+
userEnv.namespace = 'user'
|
|
551
|
+
registry.set('user', userEnv)
|
|
552
|
+
|
|
553
|
+
const session = buildSessionApi({ registry, currentNs: 'user' }, options)
|
|
554
|
+
|
|
555
|
+
const coreLoader = builtInNamespaceSources['clojure.core']
|
|
556
|
+
if (!coreLoader) {
|
|
557
|
+
throw new Error('Missing built-in clojure.core source in registry')
|
|
558
|
+
}
|
|
559
|
+
session.loadFile(coreLoader(), 'clojure.core')
|
|
560
|
+
|
|
561
|
+
for (const source of options?.entries ?? []) {
|
|
562
|
+
session.loadFile(source)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return session
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* A snapshot of a session's registry + current namespace.
|
|
570
|
+
* Produced by snapshotSession; consumed by createSessionFromSnapshot.
|
|
571
|
+
* CljValue objects are shared (they are immutable); only the Env containers
|
|
572
|
+
* are deep-copied so each derived session gets independent namespace state.
|
|
573
|
+
*/
|
|
574
|
+
export type SessionSnapshot = {
|
|
575
|
+
registry: NamespaceRegistry
|
|
576
|
+
currentNs: string
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Capture a deep clone of the session's env state.
|
|
581
|
+
* Typically called once after createSession() and before any user code is
|
|
582
|
+
* evaluated, to produce a pristine snapshot that can be cloned cheaply per test.
|
|
583
|
+
*/
|
|
584
|
+
export function snapshotSession(session: Session): SessionSnapshot {
|
|
585
|
+
return {
|
|
586
|
+
registry: cloneRegistry(session.registry),
|
|
587
|
+
currentNs: session.currentNs,
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Create a new session from a previously captured snapshot.
|
|
593
|
+
* Skips the core.clj bootstrap — the cloned registry already contains the
|
|
594
|
+
* fully-expanded core environment. Re-wires all registry-dependent closures
|
|
595
|
+
* (resolveNs, require) to the new registry instance.
|
|
596
|
+
*/
|
|
597
|
+
export function createSessionFromSnapshot(
|
|
598
|
+
snapshot: SessionSnapshot,
|
|
599
|
+
options?: SessionOptions
|
|
600
|
+
): Session {
|
|
601
|
+
const registry = cloneRegistry(snapshot.registry)
|
|
602
|
+
const session = buildSessionApi(
|
|
603
|
+
{ registry, currentNs: snapshot.currentNs },
|
|
604
|
+
options
|
|
605
|
+
)
|
|
606
|
+
for (const source of options?.entries ?? []) {
|
|
607
|
+
session.loadFile(source)
|
|
608
|
+
}
|
|
609
|
+
return session
|
|
610
|
+
}
|