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/stdlib/hof.ts
CHANGED
|
@@ -1,106 +1,28 @@
|
|
|
1
1
|
// Higher-order functions: map, filter, reduce, apply, partial, comp,
|
|
2
2
|
// map-indexed, identity
|
|
3
|
-
import {
|
|
4
|
-
isAFunction,
|
|
5
|
-
isCallable,
|
|
6
|
-
isNil,
|
|
7
|
-
isReduced,
|
|
8
|
-
isSeqable,
|
|
9
|
-
} from '../assertions'
|
|
3
|
+
import { is } from '../assertions'
|
|
10
4
|
import { EvaluationError } from '../errors'
|
|
11
|
-
import {
|
|
12
|
-
cljNativeFunction,
|
|
13
|
-
cljNativeFunctionWithContext,
|
|
14
|
-
withDoc,
|
|
15
|
-
} from '../factories'
|
|
5
|
+
import { v } from '../factories'
|
|
16
6
|
import { printString } from '../printer'
|
|
17
7
|
import { toSeq } from '../transformations'
|
|
18
8
|
import type { CljValue, Env, EvaluationContext } from '../types'
|
|
19
9
|
|
|
20
10
|
export const hofFunctions: Record<string, CljValue> = {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// (
|
|
24
|
-
// ctx: EvaluationContext,
|
|
25
|
-
// fn: CljValue | undefined,
|
|
26
|
-
// collection: CljValue | undefined
|
|
27
|
-
// ): CljValue => {
|
|
28
|
-
// if (fn === undefined) {
|
|
29
|
-
// throw new EvaluationError(
|
|
30
|
-
// `map expects a function as first argument, got nil`,
|
|
31
|
-
// { fn }
|
|
32
|
-
// )
|
|
33
|
-
// }
|
|
34
|
-
// if (!isAFunction(fn)) {
|
|
35
|
-
// throw new EvaluationError(
|
|
36
|
-
// `map expects a function as first argument, got ${printString(fn)}`,
|
|
37
|
-
// { fn }
|
|
38
|
-
// )
|
|
39
|
-
// }
|
|
40
|
-
// if (collection === undefined) {
|
|
41
|
-
// return cljNil()
|
|
42
|
-
// }
|
|
43
|
-
// if (!isCollection(collection)) {
|
|
44
|
-
// throw new EvaluationError(
|
|
45
|
-
// `map expects a collection, got ${printString(collection)}`,
|
|
46
|
-
// { collection }
|
|
47
|
-
// )
|
|
48
|
-
// }
|
|
49
|
-
|
|
50
|
-
// const wrap = isVector(collection) ? cljVector : cljList
|
|
51
|
-
// return wrap(
|
|
52
|
-
// toSeq(collection).map((item) => ctx.applyFunction(fn, [item]))
|
|
53
|
-
// )
|
|
54
|
-
// }
|
|
55
|
-
// ),
|
|
56
|
-
// filter: cljNativeFunctionWithContext(
|
|
57
|
-
// 'filter',
|
|
58
|
-
// (
|
|
59
|
-
// ctx: EvaluationContext,
|
|
60
|
-
// fn: CljValue | undefined,
|
|
61
|
-
// collection: CljValue | undefined
|
|
62
|
-
// ): CljValue => {
|
|
63
|
-
// if (fn === undefined) {
|
|
64
|
-
// throw new EvaluationError(
|
|
65
|
-
// `filter expects a function as first argument, got nil`,
|
|
66
|
-
// { fn }
|
|
67
|
-
// )
|
|
68
|
-
// }
|
|
69
|
-
// if (!isAFunction(fn)) {
|
|
70
|
-
// throw new EvaluationError(
|
|
71
|
-
// `filter expects a function as first argument, got ${printString(fn)}`,
|
|
72
|
-
// { fn }
|
|
73
|
-
// )
|
|
74
|
-
// }
|
|
75
|
-
// if (collection === undefined) {
|
|
76
|
-
// return cljNil()
|
|
77
|
-
// }
|
|
78
|
-
// if (!isCollection(collection)) {
|
|
79
|
-
// throw new EvaluationError(
|
|
80
|
-
// `filter expects a collection, got ${printString(collection)}`,
|
|
81
|
-
// { collection }
|
|
82
|
-
// )
|
|
83
|
-
// }
|
|
84
|
-
|
|
85
|
-
// const wrap = isVector(collection) ? cljVector : cljList
|
|
86
|
-
// return wrap(
|
|
87
|
-
// toSeq(collection).filter((item) =>
|
|
88
|
-
// isTruthy(ctx.applyFunction(fn, [item]))
|
|
89
|
-
// )
|
|
90
|
-
// )
|
|
91
|
-
// }
|
|
92
|
-
// ),
|
|
93
|
-
reduce: withDoc(
|
|
94
|
-
cljNativeFunctionWithContext(
|
|
11
|
+
reduce: v
|
|
12
|
+
.nativeFnCtx(
|
|
95
13
|
'reduce',
|
|
96
|
-
(
|
|
14
|
+
function reduce(
|
|
97
15
|
ctx: EvaluationContext,
|
|
98
16
|
callEnv: Env,
|
|
99
17
|
fn: CljValue,
|
|
100
18
|
...rest: CljValue[]
|
|
101
|
-
)
|
|
102
|
-
if (fn === undefined || !
|
|
103
|
-
throw EvaluationError.atArg(
|
|
19
|
+
) {
|
|
20
|
+
if (fn === undefined || !is.aFunction(fn)) {
|
|
21
|
+
throw EvaluationError.atArg(
|
|
22
|
+
`reduce expects a function as first argument${fn !== undefined ? `, got ${printString(fn)}` : ''}`,
|
|
23
|
+
{ fn },
|
|
24
|
+
0
|
|
25
|
+
)
|
|
104
26
|
}
|
|
105
27
|
if (rest.length === 0 || rest.length > 2) {
|
|
106
28
|
throw new EvaluationError(
|
|
@@ -124,9 +46,13 @@ export const hofFunctions: Record<string, CljValue> = {
|
|
|
124
46
|
return init!
|
|
125
47
|
}
|
|
126
48
|
|
|
127
|
-
if (!
|
|
49
|
+
if (!is.seqable(collection)) {
|
|
128
50
|
// collection is at args[rest.length]: 1 for (reduce f coll), 2 for (reduce f init coll)
|
|
129
|
-
throw EvaluationError.atArg(
|
|
51
|
+
throw EvaluationError.atArg(
|
|
52
|
+
`reduce expects a collection or string, got ${printString(collection)}`,
|
|
53
|
+
{ collection },
|
|
54
|
+
rest.length
|
|
55
|
+
)
|
|
130
56
|
}
|
|
131
57
|
|
|
132
58
|
const items = toSeq(collection)
|
|
@@ -142,7 +68,7 @@ export const hofFunctions: Record<string, CljValue> = {
|
|
|
142
68
|
let acc = items[0]
|
|
143
69
|
for (let i = 1; i < items.length; i++) {
|
|
144
70
|
const result = ctx.applyFunction(fn, [acc, items[i]], callEnv)
|
|
145
|
-
if (
|
|
71
|
+
if (is.reduced(result)) return result.value
|
|
146
72
|
acc = result
|
|
147
73
|
}
|
|
148
74
|
return acc
|
|
@@ -151,21 +77,22 @@ export const hofFunctions: Record<string, CljValue> = {
|
|
|
151
77
|
let acc = init!
|
|
152
78
|
for (const item of items) {
|
|
153
79
|
const result = ctx.applyFunction(fn, [acc, item], callEnv)
|
|
154
|
-
if (
|
|
80
|
+
if (is.reduced(result)) return result.value
|
|
155
81
|
acc = result
|
|
156
82
|
}
|
|
157
83
|
return acc
|
|
158
84
|
}
|
|
85
|
+
)
|
|
86
|
+
.doc(
|
|
87
|
+
'Reduces a collection to a single value by iteratively applying f. (reduce f coll) or (reduce f init coll).',
|
|
88
|
+
[
|
|
89
|
+
['f', 'coll'],
|
|
90
|
+
['f', 'val', 'coll'],
|
|
91
|
+
]
|
|
159
92
|
),
|
|
160
|
-
'Reduces a collection to a single value by iteratively applying f. (reduce f coll) or (reduce f init coll).',
|
|
161
|
-
[
|
|
162
|
-
['f', 'coll'],
|
|
163
|
-
['f', 'val', 'coll'],
|
|
164
|
-
]
|
|
165
|
-
),
|
|
166
93
|
|
|
167
|
-
apply:
|
|
168
|
-
|
|
94
|
+
apply: v
|
|
95
|
+
.nativeFnCtx(
|
|
169
96
|
'apply',
|
|
170
97
|
(
|
|
171
98
|
ctx: EvaluationContext,
|
|
@@ -173,8 +100,12 @@ export const hofFunctions: Record<string, CljValue> = {
|
|
|
173
100
|
fn: CljValue | undefined,
|
|
174
101
|
...rest: CljValue[]
|
|
175
102
|
) => {
|
|
176
|
-
if (fn === undefined || !
|
|
177
|
-
throw EvaluationError.atArg(
|
|
103
|
+
if (fn === undefined || !is.callable(fn)) {
|
|
104
|
+
throw EvaluationError.atArg(
|
|
105
|
+
`apply expects a callable as first argument${fn !== undefined ? `, got ${printString(fn)}` : ''}`,
|
|
106
|
+
{ fn },
|
|
107
|
+
0
|
|
108
|
+
)
|
|
178
109
|
}
|
|
179
110
|
if (rest.length === 0) {
|
|
180
111
|
throw new EvaluationError('apply expects at least 2 arguments', {
|
|
@@ -182,32 +113,41 @@ export const hofFunctions: Record<string, CljValue> = {
|
|
|
182
113
|
})
|
|
183
114
|
}
|
|
184
115
|
const lastArg = rest[rest.length - 1]
|
|
185
|
-
if (!
|
|
116
|
+
if (!is.nil(lastArg) && !is.seqable(lastArg)) {
|
|
186
117
|
// last arg is at index rest.length (fn=0, rest[0]=1, ..., rest[n-1]=n)
|
|
187
|
-
throw EvaluationError.atArg(
|
|
118
|
+
throw EvaluationError.atArg(
|
|
119
|
+
`apply expects a collection or string as last argument, got ${printString(lastArg)}`,
|
|
120
|
+
{ lastArg },
|
|
121
|
+
rest.length
|
|
122
|
+
)
|
|
188
123
|
}
|
|
189
124
|
|
|
190
125
|
const args = [
|
|
191
126
|
...rest.slice(0, -1),
|
|
192
|
-
...(
|
|
127
|
+
...(is.nil(lastArg) ? [] : toSeq(lastArg)),
|
|
193
128
|
]
|
|
194
129
|
return ctx.applyCallable(fn, args, callEnv)
|
|
195
130
|
}
|
|
131
|
+
)
|
|
132
|
+
.doc(
|
|
133
|
+
'Calls f with the elements of the last argument (a collection) as its arguments, optionally prepended by fixed args.',
|
|
134
|
+
[
|
|
135
|
+
['f', 'args'],
|
|
136
|
+
['f', '&', 'args'],
|
|
137
|
+
]
|
|
196
138
|
),
|
|
197
|
-
'Calls f with the elements of the last argument (a collection) as its arguments, optionally prepended by fixed args.',
|
|
198
|
-
[
|
|
199
|
-
['f', 'args'],
|
|
200
|
-
['f', '&', 'args'],
|
|
201
|
-
]
|
|
202
|
-
),
|
|
203
139
|
|
|
204
|
-
partial:
|
|
205
|
-
|
|
206
|
-
if (fn === undefined || !
|
|
207
|
-
throw EvaluationError.atArg(
|
|
140
|
+
partial: v
|
|
141
|
+
.nativeFn('partial', (fn: CljValue, ...preArgs: CljValue[]) => {
|
|
142
|
+
if (fn === undefined || !is.callable(fn)) {
|
|
143
|
+
throw EvaluationError.atArg(
|
|
144
|
+
`partial expects a callable as first argument${fn !== undefined ? `, got ${printString(fn)}` : ''}`,
|
|
145
|
+
{ fn },
|
|
146
|
+
0
|
|
147
|
+
)
|
|
208
148
|
}
|
|
209
149
|
const capturedFn = fn
|
|
210
|
-
return
|
|
150
|
+
return v.nativeFnCtx(
|
|
211
151
|
'partial',
|
|
212
152
|
(ctx: EvaluationContext, callEnv: Env, ...moreArgs: CljValue[]) => {
|
|
213
153
|
return ctx.applyCallable(
|
|
@@ -217,22 +157,27 @@ export const hofFunctions: Record<string, CljValue> = {
|
|
|
217
157
|
)
|
|
218
158
|
}
|
|
219
159
|
)
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
160
|
+
})
|
|
161
|
+
.doc(
|
|
162
|
+
'Returns a function that calls f with pre-applied args prepended to any additional arguments.',
|
|
163
|
+
[['f', '&', 'args']]
|
|
164
|
+
),
|
|
224
165
|
|
|
225
|
-
comp:
|
|
226
|
-
|
|
166
|
+
comp: v
|
|
167
|
+
.nativeFn('comp', (...fns: CljValue[]) => {
|
|
227
168
|
if (fns.length === 0) {
|
|
228
|
-
return
|
|
169
|
+
return v.nativeFn('identity', (x: CljValue) => x)
|
|
229
170
|
}
|
|
230
|
-
const badIdx = fns.findIndex((f) => !
|
|
171
|
+
const badIdx = fns.findIndex((f) => !is.callable(f))
|
|
231
172
|
if (badIdx !== -1) {
|
|
232
|
-
throw EvaluationError.atArg(
|
|
173
|
+
throw EvaluationError.atArg(
|
|
174
|
+
'comp expects functions or other callable values (keywords, maps)',
|
|
175
|
+
{ fns },
|
|
176
|
+
badIdx
|
|
177
|
+
)
|
|
233
178
|
}
|
|
234
179
|
const capturedFns = fns
|
|
235
|
-
return
|
|
180
|
+
return v.nativeFnCtx(
|
|
236
181
|
'composed',
|
|
237
182
|
(ctx: EvaluationContext, callEnv: Env, ...args: CljValue[]) => {
|
|
238
183
|
let result = ctx.applyCallable(
|
|
@@ -246,47 +191,18 @@ export const hofFunctions: Record<string, CljValue> = {
|
|
|
246
191
|
return result
|
|
247
192
|
}
|
|
248
193
|
)
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
// 'map-indexed': cljNativeFunctionWithContext(
|
|
255
|
-
// 'map-indexed',
|
|
256
|
-
// (ctx: EvaluationContext, fn: CljValue, coll: CljValue): CljValue => {
|
|
257
|
-
// if (fn === undefined || !isAFunction(fn)) {
|
|
258
|
-
// throw new EvaluationError(
|
|
259
|
-
// `map-indexed expects a function as first argument${fn !== undefined ? `, got ${printString(fn)}` : ''}`,
|
|
260
|
-
// { fn }
|
|
261
|
-
// )
|
|
262
|
-
// }
|
|
263
|
-
// if (coll === undefined || !isCollection(coll)) {
|
|
264
|
-
// throw new EvaluationError(
|
|
265
|
-
// `map-indexed expects a collection as second argument${coll !== undefined ? `, got ${printString(coll)}` : ''}`,
|
|
266
|
-
// { coll }
|
|
267
|
-
// )
|
|
268
|
-
// }
|
|
269
|
-
// const items = toSeq(coll)
|
|
270
|
-
// const wrap = isVector(coll) ? cljVector : cljList
|
|
271
|
-
// return wrap(
|
|
272
|
-
// items.map((item, idx) =>
|
|
273
|
-
// ctx.applyFunction(fn as CljFunction | CljNativeFunction, [
|
|
274
|
-
// cljNumber(idx),
|
|
275
|
-
// item,
|
|
276
|
-
// ])
|
|
277
|
-
// )
|
|
278
|
-
// )
|
|
279
|
-
// }
|
|
280
|
-
// ),
|
|
194
|
+
})
|
|
195
|
+
.doc(
|
|
196
|
+
'Returns the composition of fns, applied right-to-left. (comp f g) is equivalent to (fn [x] (f (g x))). Accepts any callable: functions, keywords, and maps.',
|
|
197
|
+
[[], ['f'], ['f', 'g'], ['f', 'g', '&', 'fns']]
|
|
198
|
+
),
|
|
281
199
|
|
|
282
|
-
identity:
|
|
283
|
-
|
|
200
|
+
identity: v
|
|
201
|
+
.nativeFn('identity', (x: CljValue) => {
|
|
284
202
|
if (x === undefined) {
|
|
285
203
|
throw EvaluationError.atArg('identity expects one argument', {}, 0)
|
|
286
204
|
}
|
|
287
205
|
return x
|
|
288
|
-
})
|
|
289
|
-
'Returns its single argument unchanged.',
|
|
290
|
-
[['x']]
|
|
291
|
-
),
|
|
206
|
+
})
|
|
207
|
+
.doc('Returns its single argument unchanged.', [['x']]),
|
|
292
208
|
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// js namespace — ambient JS interop utilities.
|
|
2
|
+
// Installed automatically alongside clojure.core. No explicit require needed.
|
|
3
|
+
// Users inject host globals (js/Math, js/console, etc.) via createSession({ hostBindings }).
|
|
4
|
+
import { EvaluationError } from '../errors'
|
|
5
|
+
import { v } from '../factories'
|
|
6
|
+
import { cljToJs, jsToClj } from '../evaluator/js-interop'
|
|
7
|
+
import type { RuntimeModule, VarMap } from '../module'
|
|
8
|
+
import type { CljValue, Env, EvaluationContext } from '../types'
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert a Clojure key value to a JS object key string.
|
|
16
|
+
* Strings, keywords, and numbers are allowed — JS coerces numbers anyway.
|
|
17
|
+
*/
|
|
18
|
+
function resolveJsKey(key: CljValue, fnName: string): string {
|
|
19
|
+
if (key.kind === 'string') return key.value
|
|
20
|
+
if (key.kind === 'keyword') return key.name.slice(1) // strip leading ':'
|
|
21
|
+
if (key.kind === 'number') return String(key.value) // JS coerces obj[0] to obj["0"]
|
|
22
|
+
throw new EvaluationError(
|
|
23
|
+
`${fnName}: key must be a string, keyword, or number, got ${key.kind}`,
|
|
24
|
+
{ key }
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Extract the raw value from a target CljValue for use in js/get and js/set!.
|
|
30
|
+
* Mirrors extractRawTarget in js-interop.ts: CljJsValue, CljString, CljNumber,
|
|
31
|
+
* CljBoolean are all valid targets — JS auto-boxes primitives for property access.
|
|
32
|
+
* Nil and other Clojure types are rejected.
|
|
33
|
+
*/
|
|
34
|
+
function extractJsTarget(val: CljValue, fnName: string): unknown {
|
|
35
|
+
switch (val.kind) {
|
|
36
|
+
case 'js-value': return val.value
|
|
37
|
+
case 'string':
|
|
38
|
+
case 'number':
|
|
39
|
+
case 'boolean': return val.value
|
|
40
|
+
case 'nil':
|
|
41
|
+
throw new EvaluationError(
|
|
42
|
+
`${fnName}: cannot access properties on nil`,
|
|
43
|
+
{ val }
|
|
44
|
+
)
|
|
45
|
+
default:
|
|
46
|
+
throw new EvaluationError(
|
|
47
|
+
`${fnName}: expected a js-value or primitive, got ${val.kind}`,
|
|
48
|
+
{ val }
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Module
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
export function makeJsModule(): RuntimeModule {
|
|
58
|
+
return {
|
|
59
|
+
id: 'conjure-js/js-namespace',
|
|
60
|
+
declareNs: [
|
|
61
|
+
{
|
|
62
|
+
name: 'js',
|
|
63
|
+
vars(_ctx): VarMap {
|
|
64
|
+
const map = new Map<string, { value: CljValue }>()
|
|
65
|
+
|
|
66
|
+
// (js/get obj key) / (js/get obj key not-found)
|
|
67
|
+
// Dynamic property access. Primitives (string, number, boolean) are valid
|
|
68
|
+
// targets — same auto-boxing JS applies. Optional not-found default is returned
|
|
69
|
+
// when the property is absent (undefined), allowing idiomatic nil defaults.
|
|
70
|
+
map.set('get', {
|
|
71
|
+
value: v.nativeFn('js/get', (obj: CljValue, key: CljValue, ...rest: CljValue[]) => {
|
|
72
|
+
const raw = extractJsTarget(obj, 'js/get') as Record<string, unknown>
|
|
73
|
+
const jsKey = resolveJsKey(key, 'js/get')
|
|
74
|
+
const result = raw[jsKey]
|
|
75
|
+
if (result === undefined && rest.length > 0) return rest[0]
|
|
76
|
+
return jsToClj(result)
|
|
77
|
+
}),
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// (js/set! obj key val) — mutate a property; returns val
|
|
81
|
+
map.set('set!', {
|
|
82
|
+
value: v.nativeFnCtx(
|
|
83
|
+
'js/set!',
|
|
84
|
+
(ctx: EvaluationContext, callEnv: Env, obj: CljValue, key: CljValue, val: CljValue) => {
|
|
85
|
+
const raw = extractJsTarget(obj, 'js/set!') as Record<string, unknown>
|
|
86
|
+
const jsKey = resolveJsKey(key, 'js/set!')
|
|
87
|
+
raw[jsKey] = cljToJs(val, ctx, callEnv)
|
|
88
|
+
return val
|
|
89
|
+
}
|
|
90
|
+
),
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// (js/call fn & args) — call a JS function with no this binding
|
|
94
|
+
map.set('call', {
|
|
95
|
+
value: v.nativeFnCtx(
|
|
96
|
+
'js/call',
|
|
97
|
+
(ctx: EvaluationContext, callEnv: Env, fn: CljValue, ...args: CljValue[]) => {
|
|
98
|
+
const rawFn = fn.kind === 'js-value' ? fn.value : undefined
|
|
99
|
+
if (typeof rawFn !== 'function') {
|
|
100
|
+
throw new EvaluationError(
|
|
101
|
+
`js/call: expected a js-value wrapping a function, got ${fn.kind}`,
|
|
102
|
+
{ fn }
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
const jsArgs = args.map((a) => cljToJs(a, ctx, callEnv))
|
|
106
|
+
return jsToClj((rawFn as (...a: unknown[]) => unknown)(...jsArgs))
|
|
107
|
+
}
|
|
108
|
+
),
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// (js/typeof x) — typeof equivalent for CljValues.
|
|
112
|
+
// Clojure primitives have unambiguous JS typeof values; js-value delegates to
|
|
113
|
+
// the raw typeof. Functions and other Clojure types throw — they're not at the
|
|
114
|
+
// JS boundary.
|
|
115
|
+
map.set('typeof', {
|
|
116
|
+
value: v.nativeFn('js/typeof', (x: CljValue) => {
|
|
117
|
+
switch (x.kind) {
|
|
118
|
+
case 'nil': return v.string('object') // typeof null === 'object'
|
|
119
|
+
case 'number': return v.string('number')
|
|
120
|
+
case 'string': return v.string('string')
|
|
121
|
+
case 'boolean': return v.string('boolean')
|
|
122
|
+
case 'js-value': return v.string(typeof x.value)
|
|
123
|
+
default:
|
|
124
|
+
throw new EvaluationError(
|
|
125
|
+
`js/typeof: cannot determine JS type of Clojure ${x.kind}`,
|
|
126
|
+
{ x }
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
}),
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// (js/instanceof? obj cls) — obj instanceof cls
|
|
133
|
+
map.set('instanceof?', {
|
|
134
|
+
value: v.nativeFn('js/instanceof?', (obj: CljValue, cls: CljValue) => {
|
|
135
|
+
if (obj.kind !== 'js-value') {
|
|
136
|
+
throw new EvaluationError(
|
|
137
|
+
`js/instanceof?: expected js-value, got ${obj.kind}`,
|
|
138
|
+
{ obj }
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
if (cls.kind !== 'js-value') {
|
|
142
|
+
throw new EvaluationError(
|
|
143
|
+
`js/instanceof?: expected js-value constructor, got ${cls.kind}`,
|
|
144
|
+
{ cls }
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
return v.boolean(
|
|
148
|
+
obj.value instanceof (cls.value as new (...a: unknown[]) => unknown)
|
|
149
|
+
)
|
|
150
|
+
}),
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// (js/array? x) — Array.isArray on the raw value
|
|
154
|
+
map.set('array?', {
|
|
155
|
+
value: v.nativeFn('js/array?', (x: CljValue) => {
|
|
156
|
+
if (x.kind !== 'js-value') return v.boolean(false)
|
|
157
|
+
return v.boolean(Array.isArray(x.value))
|
|
158
|
+
}),
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// (js/null? x) — true if x is nil (JS null comes in as CljNil)
|
|
162
|
+
map.set('null?', {
|
|
163
|
+
value: v.nativeFn('js/null?', (x: CljValue) => {
|
|
164
|
+
return v.boolean(x.kind === 'nil')
|
|
165
|
+
}),
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// (js/undefined? x) — true if x is CljJsValue wrapping undefined
|
|
169
|
+
map.set('undefined?', {
|
|
170
|
+
value: v.nativeFn('js/undefined?', (x: CljValue) => {
|
|
171
|
+
return v.boolean(x.kind === 'js-value' && x.value === undefined)
|
|
172
|
+
}),
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// (js/some? x) — true if x is neither null (nil) nor undefined
|
|
176
|
+
map.set('some?', {
|
|
177
|
+
value: v.nativeFn('js/some?', (x: CljValue) => {
|
|
178
|
+
if (x.kind === 'nil') return v.boolean(false)
|
|
179
|
+
if (x.kind === 'js-value' && x.value === undefined) return v.boolean(false)
|
|
180
|
+
return v.boolean(true)
|
|
181
|
+
}),
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// (js/get-in obj path) / (js/get-in obj path not-found)
|
|
185
|
+
// Deep property access. path must be a CljVector of string/keyword/number keys.
|
|
186
|
+
map.set('get-in', {
|
|
187
|
+
value: v.nativeFn('js/get-in', (obj: CljValue, path: CljValue, ...rest: CljValue[]) => {
|
|
188
|
+
if (path.kind !== 'vector') {
|
|
189
|
+
throw new EvaluationError(
|
|
190
|
+
`js/get-in: path must be a vector, got ${path.kind}`,
|
|
191
|
+
{ path }
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
// Validate root eagerly — nil root is always an error
|
|
195
|
+
if (obj.kind === 'nil') {
|
|
196
|
+
throw new EvaluationError(
|
|
197
|
+
'js/get-in: cannot access properties on nil',
|
|
198
|
+
{ obj }
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
const notFound = rest.length > 0 ? rest[0] : v.jsValue(undefined)
|
|
202
|
+
let current: CljValue = obj
|
|
203
|
+
for (const key of path.value) {
|
|
204
|
+
if (current.kind === 'nil') return notFound
|
|
205
|
+
if (current.kind === 'js-value' && current.value === undefined) return notFound
|
|
206
|
+
const raw = extractJsTarget(current, 'js/get-in') as Record<string, unknown>
|
|
207
|
+
const jsKey = resolveJsKey(key, 'js/get-in')
|
|
208
|
+
current = jsToClj((raw as Record<string, unknown>)[jsKey])
|
|
209
|
+
}
|
|
210
|
+
if (current.kind === 'js-value' && current.value === undefined && rest.length > 0) {
|
|
211
|
+
return notFound
|
|
212
|
+
}
|
|
213
|
+
return current
|
|
214
|
+
}),
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// (js/prop key) / (js/prop key not-found)
|
|
218
|
+
// Returns a single-arg function that reads the given property from an object.
|
|
219
|
+
// Use with map/filter: (map (js/prop "name") users)
|
|
220
|
+
map.set('prop', {
|
|
221
|
+
value: v.nativeFn('js/prop', (key: CljValue, ...rest: CljValue[]) => {
|
|
222
|
+
const notFound = rest.length > 0 ? rest[0] : v.nil()
|
|
223
|
+
return v.nativeFn('js/prop-accessor', (obj: CljValue) => {
|
|
224
|
+
const raw = extractJsTarget(obj, 'js/prop') as Record<string, unknown>
|
|
225
|
+
const jsKey = resolveJsKey(key, 'js/prop')
|
|
226
|
+
const result = raw[jsKey]
|
|
227
|
+
if (result === undefined) return notFound
|
|
228
|
+
return jsToClj(result)
|
|
229
|
+
})
|
|
230
|
+
}),
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
// (js/method key & partialArgs)
|
|
234
|
+
// Returns a function that calls the named method on an object, prepending any partial args.
|
|
235
|
+
// (map (js/method "trim") strings)
|
|
236
|
+
// (map (js/method "toFixed" 2) numbers)
|
|
237
|
+
map.set('method', {
|
|
238
|
+
value: v.nativeFn('js/method', (key: CljValue, ...partialArgs: CljValue[]) => {
|
|
239
|
+
return v.nativeFnCtx(
|
|
240
|
+
'js/method-caller',
|
|
241
|
+
(ctx: EvaluationContext, callEnv: Env, obj: CljValue, ...callArgs: CljValue[]) => {
|
|
242
|
+
const rawObj = extractJsTarget(obj, 'js/method') as Record<string, unknown>
|
|
243
|
+
const jsKey = resolveJsKey(key, 'js/method')
|
|
244
|
+
const method = rawObj[jsKey]
|
|
245
|
+
if (typeof method !== 'function') {
|
|
246
|
+
throw new EvaluationError(
|
|
247
|
+
`js/method: property '${jsKey}' is not callable`,
|
|
248
|
+
{ jsKey }
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
const allArgs = [...partialArgs, ...callArgs].map((a) => cljToJs(a, ctx, callEnv))
|
|
252
|
+
return jsToClj((method as (...a: unknown[]) => unknown).apply(rawObj, allArgs))
|
|
253
|
+
}
|
|
254
|
+
)
|
|
255
|
+
}),
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
// (js/merge obj1 obj2 ...) — Object.assign into a fresh object
|
|
259
|
+
map.set('merge', {
|
|
260
|
+
value: v.nativeFnCtx(
|
|
261
|
+
'js/merge',
|
|
262
|
+
(ctx: EvaluationContext, callEnv: Env, ...args: CljValue[]) => {
|
|
263
|
+
const result = Object.assign({}, ...args.map((a) => cljToJs(a, ctx, callEnv)))
|
|
264
|
+
return v.jsValue(result)
|
|
265
|
+
}
|
|
266
|
+
),
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
// (js/seq arr) — JS array → Clojure vector with elements converted via jsToClj
|
|
270
|
+
map.set('seq', {
|
|
271
|
+
value: v.nativeFn('js/seq', (arr: CljValue) => {
|
|
272
|
+
if (arr.kind !== 'js-value' || !Array.isArray(arr.value)) {
|
|
273
|
+
throw new EvaluationError(
|
|
274
|
+
`js/seq: expected a js-value wrapping an array, got ${arr.kind}`,
|
|
275
|
+
{ arr }
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
return v.vector((arr.value as unknown[]).map(jsToClj))
|
|
279
|
+
}),
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
// (js/array & args) — variadic args → JS array as CljJsValue
|
|
283
|
+
map.set('array', {
|
|
284
|
+
value: v.nativeFnCtx(
|
|
285
|
+
'js/array',
|
|
286
|
+
(ctx: EvaluationContext, callEnv: Env, ...args: CljValue[]) => {
|
|
287
|
+
return v.jsValue(args.map((a) => cljToJs(a, ctx, callEnv)))
|
|
288
|
+
}
|
|
289
|
+
),
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
// (js/obj key val key val ...) — variadic key-val pairs → JS plain object as CljJsValue
|
|
293
|
+
map.set('obj', {
|
|
294
|
+
value: v.nativeFnCtx(
|
|
295
|
+
'js/obj',
|
|
296
|
+
(ctx: EvaluationContext, callEnv: Env, ...args: CljValue[]) => {
|
|
297
|
+
if (args.length % 2 !== 0) {
|
|
298
|
+
throw new EvaluationError(
|
|
299
|
+
'js/obj: requires even number of arguments',
|
|
300
|
+
{ count: args.length }
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
const result: Record<string, unknown> = {}
|
|
304
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
305
|
+
const jsKey = resolveJsKey(args[i], 'js/obj')
|
|
306
|
+
result[jsKey] = cljToJs(args[i + 1], ctx, callEnv)
|
|
307
|
+
}
|
|
308
|
+
return v.jsValue(result)
|
|
309
|
+
}
|
|
310
|
+
),
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
// (js/keys obj) — Object.keys equivalent → Clojure vector of strings
|
|
314
|
+
map.set('keys', {
|
|
315
|
+
value: v.nativeFn('js/keys', (obj: CljValue) => {
|
|
316
|
+
const raw = extractJsTarget(obj, 'js/keys') as Record<string, unknown>
|
|
317
|
+
return v.vector(Object.keys(raw).map(v.string))
|
|
318
|
+
}),
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
// (js/values obj) — Object.values equivalent → Clojure vector, elements via jsToClj
|
|
322
|
+
map.set('values', {
|
|
323
|
+
value: v.nativeFn('js/values', (obj: CljValue) => {
|
|
324
|
+
const raw = extractJsTarget(obj, 'js/values') as Record<string, unknown>
|
|
325
|
+
return v.vector(Object.values(raw).map(jsToClj))
|
|
326
|
+
}),
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
// (js/entries obj) — Object.entries equivalent → vector of [key value] pairs
|
|
330
|
+
map.set('entries', {
|
|
331
|
+
value: v.nativeFn('js/entries', (obj: CljValue) => {
|
|
332
|
+
const raw = extractJsTarget(obj, 'js/entries') as Record<string, unknown>
|
|
333
|
+
return v.vector(
|
|
334
|
+
Object.entries(raw).map(([k, val]) => v.vector([v.string(k), jsToClj(val)]))
|
|
335
|
+
)
|
|
336
|
+
}),
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
return map
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
}
|
|
344
|
+
}
|