conjure-js 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cli/conjure-js.mjs +9360 -5298
- package/dist-vite-plugin/index.mjs +9463 -5185
- package/package.json +3 -1
- package/src/bin/cli.ts +2 -2
- package/src/bin/nrepl-symbol.ts +150 -0
- package/src/bin/nrepl.ts +289 -167
- package/src/bin/version.ts +1 -1
- package/src/clojure/core.clj +757 -29
- package/src/clojure/core.clj.d.ts +75 -131
- package/src/clojure/generated/builtin-namespace-registry.ts +4 -0
- package/src/clojure/generated/clojure-core-source.ts +758 -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 +20 -6
- 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 +28 -5
- package/src/core/evaluator/destructure.ts +180 -69
- package/src/core/evaluator/dispatch.ts +12 -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 +234 -191
- package/src/core/factories.ts +182 -3
- package/src/core/index.ts +54 -4
- package/src/core/module.ts +136 -0
- package/src/core/ns-forms.ts +107 -0
- package/src/core/printer.ts +371 -11
- package/src/core/reader.ts +84 -33
- package/src/core/registry.ts +209 -0
- package/src/core/runtime.ts +376 -0
- package/src/core/session.ts +253 -487
- package/src/core/stdlib/arithmetic.ts +528 -194
- package/src/core/stdlib/async-fns.ts +132 -0
- package/src/core/stdlib/atoms.ts +291 -56
- package/src/core/stdlib/errors.ts +54 -50
- package/src/core/stdlib/hof.ts +82 -166
- 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 +61 -30
- package/src/core/stdlib/predicates.ts +325 -187
- 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 +292 -130
- package/src/core/stdlib/vars.ts +27 -27
- package/src/core/stdlib/vectors.ts +122 -0
- package/src/core/tokenizer.ts +2 -2
- package/src/core/transformations.ts +117 -9
- package/src/core/types.ts +98 -2
- package/src/host/node-host-module.ts +74 -0
- package/src/{vite-plugin-clj/nrepl-relay.ts → nrepl/relay.ts} +72 -11
- package/src/vite-plugin-clj/codegen.ts +87 -95
- package/src/vite-plugin-clj/index.ts +178 -23
- 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 -72
- package/src/clojure/demo.clj.d.ts +0 -0
- package/src/core/core-env.ts +0 -61
- package/src/core/stdlib/collections.ts +0 -739
- package/src/host/node.ts +0 -55
package/src/core/factories.ts
CHANGED
|
@@ -3,17 +3,24 @@ import type {
|
|
|
3
3
|
Arity,
|
|
4
4
|
CljAtom,
|
|
5
5
|
CljBoolean,
|
|
6
|
+
CljCons,
|
|
7
|
+
CljDelay,
|
|
6
8
|
CljFunction,
|
|
9
|
+
CljJsValue,
|
|
7
10
|
CljKeyword,
|
|
11
|
+
CljLazySeq,
|
|
8
12
|
CljList,
|
|
9
13
|
CljMacro,
|
|
10
14
|
CljMap,
|
|
11
15
|
CljMultiMethod,
|
|
16
|
+
CljNamespace,
|
|
12
17
|
CljNativeFunction,
|
|
13
18
|
CljNil,
|
|
14
19
|
CljNumber,
|
|
20
|
+
CljPending,
|
|
15
21
|
CljReduced,
|
|
16
22
|
CljRegex,
|
|
23
|
+
CljSet,
|
|
17
24
|
CljString,
|
|
18
25
|
CljSymbol,
|
|
19
26
|
CljValue,
|
|
@@ -39,6 +46,7 @@ export const cljSymbol = <T extends string>(name: T) =>
|
|
|
39
46
|
({ kind: 'symbol', name }) as const satisfies CljSymbol
|
|
40
47
|
export const cljList = <T extends CljValue[]>(value: T) =>
|
|
41
48
|
({ kind: 'list', value }) as const satisfies CljList
|
|
49
|
+
export const cljSet = (values: CljValue[]): CljSet => ({ kind: 'set', values })
|
|
42
50
|
export const cljVector = <T extends CljValue[]>(value: T) =>
|
|
43
51
|
({ kind: 'vector', value }) as const satisfies CljVector
|
|
44
52
|
export const cljMap = <T extends [CljValue, CljValue][]>(entries: T) =>
|
|
@@ -71,7 +79,11 @@ export const cljNativeFunction = <
|
|
|
71
79
|
({ kind: 'native-function', name, fn }) as const satisfies CljNativeFunction
|
|
72
80
|
export const cljNativeFunctionWithContext = <
|
|
73
81
|
T extends string,
|
|
74
|
-
U extends (
|
|
82
|
+
U extends (
|
|
83
|
+
ctx: EvaluationContext,
|
|
84
|
+
callEnv: Env,
|
|
85
|
+
...args: CljValue[]
|
|
86
|
+
) => CljValue,
|
|
75
87
|
>(
|
|
76
88
|
name: T,
|
|
77
89
|
fn: U
|
|
@@ -110,8 +122,12 @@ export const cljRegex = (pattern: string, flags: string = ''): CljRegex => ({
|
|
|
110
122
|
flags,
|
|
111
123
|
})
|
|
112
124
|
|
|
113
|
-
export const cljVar = (
|
|
114
|
-
|
|
125
|
+
export const cljVar = (
|
|
126
|
+
ns: string,
|
|
127
|
+
name: string,
|
|
128
|
+
value: CljValue,
|
|
129
|
+
meta?: CljMap
|
|
130
|
+
): CljVar => ({ kind: 'var', ns, name, value, meta })
|
|
115
131
|
|
|
116
132
|
export const cljAtom = (value: CljValue): CljAtom => ({ kind: 'atom', value })
|
|
117
133
|
export const cljReduced = (value: CljValue): CljReduced => ({
|
|
@@ -122,6 +138,50 @@ export const cljVolatile = (value: CljValue): CljVolatile => ({
|
|
|
122
138
|
kind: 'volatile',
|
|
123
139
|
value,
|
|
124
140
|
})
|
|
141
|
+
export const cljDelay = (thunk: () => CljValue): CljDelay => ({
|
|
142
|
+
kind: 'delay',
|
|
143
|
+
thunk,
|
|
144
|
+
realized: false,
|
|
145
|
+
})
|
|
146
|
+
export const cljLazySeq = (thunk: () => CljValue): CljLazySeq => ({
|
|
147
|
+
kind: 'lazy-seq',
|
|
148
|
+
thunk,
|
|
149
|
+
realized: false,
|
|
150
|
+
})
|
|
151
|
+
export const cljCons = (head: CljValue, tail: CljValue): CljCons => ({
|
|
152
|
+
kind: 'cons',
|
|
153
|
+
head,
|
|
154
|
+
tail,
|
|
155
|
+
})
|
|
156
|
+
export const cljNamespace = (name: string): CljNamespace => ({
|
|
157
|
+
kind: 'namespace',
|
|
158
|
+
name,
|
|
159
|
+
vars: new Map(),
|
|
160
|
+
aliases: new Map(),
|
|
161
|
+
readerAliases: new Map(),
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
export const cljJsValue = (value: unknown): CljJsValue => ({
|
|
165
|
+
kind: 'js-value',
|
|
166
|
+
value,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// --- ASYNC (experimental) ---
|
|
170
|
+
export const cljPending = (promise: Promise<CljValue>): CljPending => {
|
|
171
|
+
const pending: CljPending = { kind: 'pending', promise }
|
|
172
|
+
// Track fulfillment so the printer can show #<Pending @val> when already settled.
|
|
173
|
+
promise.then(
|
|
174
|
+
(v) => {
|
|
175
|
+
pending.resolved = true
|
|
176
|
+
pending.resolvedValue = v
|
|
177
|
+
},
|
|
178
|
+
() => {
|
|
179
|
+
/* rejection — no resolved state; printer shows #<Pending> */
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
return pending
|
|
183
|
+
}
|
|
184
|
+
// --- END ASYNC ---
|
|
125
185
|
|
|
126
186
|
export const withDoc = <T extends CljNativeFunction | CljFunction>(
|
|
127
187
|
fn: T,
|
|
@@ -142,6 +202,57 @@ export const withDoc = <T extends CljNativeFunction | CljFunction>(
|
|
|
142
202
|
]),
|
|
143
203
|
})
|
|
144
204
|
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// NativeFnBuilder — fluent construction API for native functions
|
|
207
|
+
//
|
|
208
|
+
// Satisfies CljNativeFunction structurally, so it can be stored in any
|
|
209
|
+
// registry or record that expects CljNativeFunction — no .build() call needed.
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
export type NativeFnBuilder = CljNativeFunction & {
|
|
213
|
+
/** Attach doc-string and optional arglists metadata. */
|
|
214
|
+
doc(text: string, arglists?: string[][]): NativeFnBuilder
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function buildDocMeta(text: string, arglists?: string[][]): CljMap {
|
|
218
|
+
return cljMap([
|
|
219
|
+
[cljKeyword(':doc'), cljString(text)],
|
|
220
|
+
...(arglists
|
|
221
|
+
? ([
|
|
222
|
+
[
|
|
223
|
+
cljKeyword(':arglists'),
|
|
224
|
+
cljVector(arglists.map((args) => cljVector(args.map(cljSymbol)))),
|
|
225
|
+
],
|
|
226
|
+
] as [CljValue, CljValue][])
|
|
227
|
+
: []),
|
|
228
|
+
])
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function makeNativeFnBuilder(def: CljNativeFunction): NativeFnBuilder {
|
|
232
|
+
// Reconstruct a plain CljNativeFunction explicitly so that the spread
|
|
233
|
+
// inside .doc() never accidentally picks up builder methods from a previous
|
|
234
|
+
// clone round.
|
|
235
|
+
const plain: CljNativeFunction = {
|
|
236
|
+
kind: 'native-function',
|
|
237
|
+
name: def.name,
|
|
238
|
+
fn: def.fn,
|
|
239
|
+
...(def.fnWithContext !== undefined
|
|
240
|
+
? { fnWithContext: def.fnWithContext }
|
|
241
|
+
: {}),
|
|
242
|
+
...(def.meta !== undefined ? { meta: def.meta } : {}),
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
...plain,
|
|
247
|
+
doc(text: string, arglists?: string[][]): NativeFnBuilder {
|
|
248
|
+
return makeNativeFnBuilder({
|
|
249
|
+
...plain,
|
|
250
|
+
meta: buildDocMeta(text, arglists),
|
|
251
|
+
})
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
145
256
|
export const cljMultiMethod = (
|
|
146
257
|
name: string,
|
|
147
258
|
dispatchFn: CljFunction | CljNativeFunction,
|
|
@@ -157,3 +268,71 @@ export const cljMultiMethod = (
|
|
|
157
268
|
methods,
|
|
158
269
|
defaultMethod,
|
|
159
270
|
})
|
|
271
|
+
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
// v — unified value factory namespace
|
|
274
|
+
//
|
|
275
|
+
// Mirrors the cljXxx standalone functions but collected under one object so
|
|
276
|
+
// stdlib files need only a single import. Primitive factories are thin
|
|
277
|
+
// aliases; nativeFn / nativeFnCtx return a NativeFnBuilder with .doc().
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
export const v = {
|
|
281
|
+
// primitives
|
|
282
|
+
number: cljNumber,
|
|
283
|
+
string: cljString,
|
|
284
|
+
boolean: cljBoolean,
|
|
285
|
+
keyword: cljKeyword,
|
|
286
|
+
nil: cljNil,
|
|
287
|
+
symbol: cljSymbol,
|
|
288
|
+
kw: cljKeyword,
|
|
289
|
+
|
|
290
|
+
// collections
|
|
291
|
+
list: cljList,
|
|
292
|
+
vector: cljVector,
|
|
293
|
+
map: cljMap,
|
|
294
|
+
set: cljSet,
|
|
295
|
+
cons: cljCons,
|
|
296
|
+
|
|
297
|
+
// callables
|
|
298
|
+
function: cljFunction,
|
|
299
|
+
multiArityFunction: cljMultiArityFunction,
|
|
300
|
+
macro: cljMacro,
|
|
301
|
+
multiArityMacro: cljMultiArityMacro,
|
|
302
|
+
multiMethod: cljMultiMethod,
|
|
303
|
+
|
|
304
|
+
// fluent native function builders
|
|
305
|
+
nativeFn(
|
|
306
|
+
name: string,
|
|
307
|
+
fn: (...args: CljValue[]) => CljValue
|
|
308
|
+
): NativeFnBuilder {
|
|
309
|
+
return makeNativeFnBuilder({ kind: 'native-function', name, fn })
|
|
310
|
+
},
|
|
311
|
+
nativeFnCtx(
|
|
312
|
+
name: string,
|
|
313
|
+
fn: (ctx: EvaluationContext, callEnv: Env, ...args: CljValue[]) => CljValue
|
|
314
|
+
): NativeFnBuilder {
|
|
315
|
+
return makeNativeFnBuilder({
|
|
316
|
+
kind: 'native-function',
|
|
317
|
+
name,
|
|
318
|
+
fn: () => {
|
|
319
|
+
throw new EvaluationError('Native function called without context', {
|
|
320
|
+
name,
|
|
321
|
+
})
|
|
322
|
+
},
|
|
323
|
+
fnWithContext: fn,
|
|
324
|
+
})
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
// other value types
|
|
328
|
+
var: cljVar,
|
|
329
|
+
atom: cljAtom,
|
|
330
|
+
regex: cljRegex,
|
|
331
|
+
reduced: cljReduced,
|
|
332
|
+
volatile: cljVolatile,
|
|
333
|
+
delay: cljDelay,
|
|
334
|
+
lazySeq: cljLazySeq,
|
|
335
|
+
namespace: cljNamespace,
|
|
336
|
+
pending: cljPending,
|
|
337
|
+
jsValue: cljJsValue,
|
|
338
|
+
}
|
package/src/core/index.ts
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
// Session API
|
|
2
|
-
export {
|
|
3
|
-
|
|
2
|
+
export {
|
|
3
|
+
createSession,
|
|
4
|
+
snapshotSession,
|
|
5
|
+
createSessionFromSnapshot,
|
|
6
|
+
} from './session'
|
|
7
|
+
export type { Session, SessionSnapshot, SessionOptions } from './session'
|
|
8
|
+
|
|
9
|
+
// Runtime API (advanced embedding)
|
|
10
|
+
export { createRuntime, restoreRuntime } from './runtime'
|
|
11
|
+
export type { Runtime, RuntimeSnapshot, RuntimeOptions } from './runtime'
|
|
12
|
+
|
|
13
|
+
// Module system
|
|
14
|
+
export { resolveModuleOrder } from './module'
|
|
15
|
+
export { makeCoreModule } from './core-module'
|
|
16
|
+
export type {
|
|
17
|
+
RuntimeModule,
|
|
18
|
+
NamespaceDeclaration,
|
|
19
|
+
VarDeclaration,
|
|
20
|
+
VarMap,
|
|
21
|
+
ModuleContext,
|
|
22
|
+
} from './module'
|
|
4
23
|
|
|
5
24
|
// Conversions
|
|
6
25
|
export { cljToJs, jsToClj, ConversionError } from './conversions'
|
|
26
|
+
export type { FunctionApplier } from './conversions'
|
|
7
27
|
|
|
8
28
|
// Evaluator
|
|
9
|
-
export {
|
|
29
|
+
export {
|
|
30
|
+
applyFunction,
|
|
31
|
+
evaluateWithMeasurements,
|
|
32
|
+
} from './evaluator'
|
|
10
33
|
|
|
11
34
|
// Errors
|
|
12
35
|
export { EvaluationError, ReaderError, TokenizerError } from './errors'
|
|
@@ -25,9 +48,14 @@ export {
|
|
|
25
48
|
cljFunction,
|
|
26
49
|
cljMultiArityFunction,
|
|
27
50
|
cljNativeFunction,
|
|
51
|
+
cljNativeFunctionWithContext,
|
|
28
52
|
cljMacro,
|
|
29
53
|
cljMultiArityMacro,
|
|
30
54
|
cljVar,
|
|
55
|
+
cljNamespace,
|
|
56
|
+
cljPending,
|
|
57
|
+
// fluent builder — use v.nativeFn / v.nativeFnCtx for module authoring
|
|
58
|
+
v,
|
|
31
59
|
} from './factories'
|
|
32
60
|
|
|
33
61
|
// Assertions
|
|
@@ -47,6 +75,7 @@ export {
|
|
|
47
75
|
isCollection,
|
|
48
76
|
isEqual,
|
|
49
77
|
isVar,
|
|
78
|
+
isNamespace,
|
|
50
79
|
} from './assertions'
|
|
51
80
|
|
|
52
81
|
// Env
|
|
@@ -58,8 +87,26 @@ export { valueToString } from './transformations'
|
|
|
58
87
|
// Printer
|
|
59
88
|
export { printString } from './printer'
|
|
60
89
|
|
|
61
|
-
// Tokenizer (public for tooling consumers)
|
|
90
|
+
// Tokenizer + Reader (public for tooling consumers and EDN parsing)
|
|
62
91
|
export { tokenize } from './tokenizer'
|
|
92
|
+
export { readForms } from './reader'
|
|
93
|
+
|
|
94
|
+
// readString — parse a single EDN source string to a CljValue.
|
|
95
|
+
// Equivalent to Clojure's read-string. Useful for deserialising results
|
|
96
|
+
// returned by remote nodes over the mesh wire.
|
|
97
|
+
import { tokenize as _tokenize } from './tokenizer'
|
|
98
|
+
import { readForms as _readForms } from './reader'
|
|
99
|
+
import type { CljValue as _CljValue } from './types'
|
|
100
|
+
|
|
101
|
+
export function readString(source: string): _CljValue {
|
|
102
|
+
const tokens = _tokenize(source)
|
|
103
|
+
const forms = _readForms(tokens)
|
|
104
|
+
if (forms.length === 0) throw new Error('readString: empty input')
|
|
105
|
+
return forms[0]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// JS interop — import map type for user-defined session entrypoints
|
|
109
|
+
export type ImportMap = Record<string, unknown>
|
|
63
110
|
|
|
64
111
|
// Types
|
|
65
112
|
export type {
|
|
@@ -78,6 +125,9 @@ export type {
|
|
|
78
125
|
CljMacro,
|
|
79
126
|
CljVar,
|
|
80
127
|
CljNamespace,
|
|
128
|
+
CljPending, // experimental
|
|
81
129
|
Env,
|
|
82
130
|
Arity,
|
|
131
|
+
IOContext,
|
|
132
|
+
EvaluationContext,
|
|
83
133
|
} from './types'
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { CljMap, CljNamespace, CljValue } from './types'
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// RuntimeModule — declarative unit of capability installation into a runtime.
|
|
5
|
+
// Modules are plain data. `vars(ctx)` is the only computation, called once per
|
|
6
|
+
// namespace declaration at install time in dependency order.
|
|
7
|
+
//
|
|
8
|
+
// Additive namespace model: multiple modules may contribute vars to the same
|
|
9
|
+
// namespace. The uniqueness invariant is at the (namespace, varName) level —
|
|
10
|
+
// two modules cannot declare the same var in the same namespace.
|
|
11
|
+
//
|
|
12
|
+
// `dependsOn` lists namespace names. A module that lists 'clojure.core' will
|
|
13
|
+
// be installed after ALL modules that contribute vars to 'clojure.core'.
|
|
14
|
+
// If no installed module provides a depended-on namespace, resolveModuleOrder
|
|
15
|
+
// throws immediately with a clear message.
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export type RuntimeModule = {
|
|
19
|
+
id: string
|
|
20
|
+
dependsOn?: string[] // namespace names that must be fully installed before this module
|
|
21
|
+
declareNs: NamespaceDeclaration[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type NamespaceDeclaration = {
|
|
25
|
+
name: string // the namespace this declaration contributes vars into
|
|
26
|
+
vars(ctx: ModuleContext): VarMap
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type VarDeclaration = {
|
|
30
|
+
value: CljValue
|
|
31
|
+
meta?: CljMap
|
|
32
|
+
dynamic?: boolean
|
|
33
|
+
macro?: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type VarMap = Map<string, VarDeclaration>
|
|
37
|
+
|
|
38
|
+
export type ModuleContext = {
|
|
39
|
+
// Read-only view of already-installed state at construction time
|
|
40
|
+
getVar(ns: string, name: string): CljValue | null
|
|
41
|
+
getNamespace(name: string): CljNamespace | null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// resolveModuleOrder — returns modules in dependency-safe install order.
|
|
46
|
+
//
|
|
47
|
+
// Algorithm (Kahn's topological sort):
|
|
48
|
+
// 1. Build nsProviders: namespace → [module IDs that contribute to it]
|
|
49
|
+
// 2. For each module M and each dep namespace in M.dependsOn:
|
|
50
|
+
// - If no provider exists → throw (missing dep)
|
|
51
|
+
// - For each provider P where P.id ≠ M.id → add edge P → M
|
|
52
|
+
// 3. Run Kahn's algorithm; throw on cycle
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
export function resolveModuleOrder(
|
|
56
|
+
modules: RuntimeModule[],
|
|
57
|
+
existingNamespaces?: Set<string>
|
|
58
|
+
): RuntimeModule[] {
|
|
59
|
+
// Index modules by ID for quick lookup
|
|
60
|
+
const byId = new Map<string, RuntimeModule>()
|
|
61
|
+
for (const m of modules) {
|
|
62
|
+
if (byId.has(m.id)) {
|
|
63
|
+
throw new Error(`Duplicate module ID: '${m.id}'`)
|
|
64
|
+
}
|
|
65
|
+
byId.set(m.id, m)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Build namespace → provider module IDs map
|
|
69
|
+
const nsProviders = new Map<string, string[]>()
|
|
70
|
+
for (const m of modules) {
|
|
71
|
+
for (const decl of m.declareNs) {
|
|
72
|
+
const providers = nsProviders.get(decl.name) ?? []
|
|
73
|
+
providers.push(m.id)
|
|
74
|
+
nsProviders.set(decl.name, providers)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Build dependency edges: providerID → [dependentIDs]
|
|
79
|
+
// inDegree tracks how many unresolved dependencies each module has
|
|
80
|
+
const graph = new Map<string, string[]>()
|
|
81
|
+
const inDegree = new Map<string, number>()
|
|
82
|
+
for (const m of modules) {
|
|
83
|
+
graph.set(m.id, [])
|
|
84
|
+
inDegree.set(m.id, 0)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const m of modules) {
|
|
88
|
+
for (const depNs of m.dependsOn ?? []) {
|
|
89
|
+
// A dep is satisfied if some already-installed namespace provides it
|
|
90
|
+
if (existingNamespaces?.has(depNs)) continue
|
|
91
|
+
|
|
92
|
+
const providers = nsProviders.get(depNs)
|
|
93
|
+
if (!providers || providers.length === 0) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`No module provides namespace '${depNs}' (required by '${m.id}')`
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
for (const providerId of providers) {
|
|
99
|
+
// Self-exclusion: a module that both contributes to and depends on the
|
|
100
|
+
// same namespace does not create a self-loop.
|
|
101
|
+
if (providerId === m.id) continue
|
|
102
|
+
graph.get(providerId)!.push(m.id)
|
|
103
|
+
inDegree.set(m.id, inDegree.get(m.id)! + 1)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Kahn's algorithm — start with nodes that have no unresolved deps
|
|
109
|
+
const queue: string[] = []
|
|
110
|
+
for (const [id, degree] of inDegree) {
|
|
111
|
+
if (degree === 0) queue.push(id)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const result: RuntimeModule[] = []
|
|
115
|
+
while (queue.length > 0) {
|
|
116
|
+
const id = queue.shift()!
|
|
117
|
+
result.push(byId.get(id)!)
|
|
118
|
+
for (const dependentId of graph.get(id)!) {
|
|
119
|
+
const newDegree = inDegree.get(dependentId)! - 1
|
|
120
|
+
inDegree.set(dependentId, newDegree)
|
|
121
|
+
if (newDegree === 0) queue.push(dependentId)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// If not all modules were processed, there is a cycle
|
|
126
|
+
if (result.length !== modules.length) {
|
|
127
|
+
const unprocessed = modules
|
|
128
|
+
.map((m) => m.id)
|
|
129
|
+
.filter((id) => !result.some((m) => m.id === id))
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Circular dependency detected in module system. Modules in cycle: ${unprocessed.join(', ')}`
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return result
|
|
136
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { isKeyword, isList, isSymbol } from './assertions'
|
|
2
|
+
import type { CljValue, Token, TokenSymbol } from './types'
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Token scan helpers — lightweight pre-parse scans for ns form metadata.
|
|
6
|
+
// These are semantic (module declaration analysis), not syntactic (parsing).
|
|
7
|
+
// Exported so session.evaluate and runtime.loadFile can reuse them.
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
// Looks for the pattern: LParen Symbol("ns") Symbol(name) at the top of the
|
|
11
|
+
// token stream. Returns the namespace name or null.
|
|
12
|
+
export function extractNsNameFromTokens(tokens: Token[]): string | null {
|
|
13
|
+
const meaningful = tokens.filter((t) => t.kind !== 'Comment')
|
|
14
|
+
if (meaningful.length < 3) return null
|
|
15
|
+
if (meaningful[0].kind !== 'LParen') return null
|
|
16
|
+
if (meaningful[1].kind !== 'Symbol' || meaningful[1].value !== 'ns')
|
|
17
|
+
return null
|
|
18
|
+
if (meaningful[2].kind !== 'Symbol') return null
|
|
19
|
+
return meaningful[2].value
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Returns Map { 'alias' -> 'full.ns.name' } for all [some.ns :as alias] and
|
|
23
|
+
// [some.ns :as-alias alias] specs found in the ns form's :require clauses.
|
|
24
|
+
// Runs before readForms so the reader can expand ::alias/foo at read time.
|
|
25
|
+
export function extractAliasMapFromTokens(
|
|
26
|
+
tokens: Token[]
|
|
27
|
+
): Map<string, string> {
|
|
28
|
+
const aliases = new Map<string, string>()
|
|
29
|
+
const meaningful = tokens.filter(
|
|
30
|
+
(t) => t.kind !== 'Comment' && t.kind !== 'Whitespace'
|
|
31
|
+
)
|
|
32
|
+
if (meaningful.length < 3) return aliases
|
|
33
|
+
if (meaningful[0].kind !== 'LParen') return aliases
|
|
34
|
+
if (meaningful[1].kind !== 'Symbol' || meaningful[1].value !== 'ns')
|
|
35
|
+
return aliases
|
|
36
|
+
|
|
37
|
+
let i = 3 // skip ( ns <name>
|
|
38
|
+
let depth = 1
|
|
39
|
+
while (i < meaningful.length && depth > 0) {
|
|
40
|
+
const tok = meaningful[i]
|
|
41
|
+
if (tok.kind === 'LParen') {
|
|
42
|
+
depth++
|
|
43
|
+
i++
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
if (tok.kind === 'RParen') {
|
|
47
|
+
depth--
|
|
48
|
+
i++
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
if (tok.kind === 'LBracket') {
|
|
52
|
+
let j = i + 1
|
|
53
|
+
let nsSym: string | null = null
|
|
54
|
+
while (j < meaningful.length && meaningful[j].kind !== 'RBracket') {
|
|
55
|
+
const t = meaningful[j]
|
|
56
|
+
if (t.kind === 'Symbol' && nsSym === null) {
|
|
57
|
+
nsSym = t.value
|
|
58
|
+
}
|
|
59
|
+
if (
|
|
60
|
+
t.kind === 'Keyword' &&
|
|
61
|
+
(t.value === ':as' || t.value === ':as-alias')
|
|
62
|
+
) {
|
|
63
|
+
j++
|
|
64
|
+
if (
|
|
65
|
+
j < meaningful.length &&
|
|
66
|
+
meaningful[j].kind === 'Symbol' &&
|
|
67
|
+
nsSym
|
|
68
|
+
) {
|
|
69
|
+
aliases.set((meaningful[j] as TokenSymbol).value, nsSym)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
j++
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
i++
|
|
76
|
+
}
|
|
77
|
+
return aliases
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function findNsForm(forms: CljValue[]) {
|
|
81
|
+
const nsForm = forms.find(
|
|
82
|
+
(f) =>
|
|
83
|
+
isList(f) &&
|
|
84
|
+
f.value.length > 0 &&
|
|
85
|
+
isSymbol(f.value[0]) &&
|
|
86
|
+
f.value[0].name === 'ns'
|
|
87
|
+
)
|
|
88
|
+
if (!nsForm || !isList(nsForm)) return null
|
|
89
|
+
return nsForm
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function extractRequireClauses(forms: CljValue[]): CljValue[][] {
|
|
93
|
+
const nsForm = findNsForm(forms)
|
|
94
|
+
if (!nsForm) return []
|
|
95
|
+
const clauses: CljValue[][] = []
|
|
96
|
+
for (let i = 2; i < nsForm.value.length; i++) {
|
|
97
|
+
const clause = nsForm.value[i]
|
|
98
|
+
if (
|
|
99
|
+
isList(clause) &&
|
|
100
|
+
isKeyword(clause.value[0]) &&
|
|
101
|
+
clause.value[0].name === ':require'
|
|
102
|
+
) {
|
|
103
|
+
clauses.push(clause.value.slice(1))
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return clauses
|
|
107
|
+
}
|