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.
Files changed (80) hide show
  1. package/dist-cli/conjure-js.mjs +9336 -5028
  2. package/dist-vite-plugin/index.mjs +10455 -0
  3. package/package.json +9 -2
  4. package/src/bin/cli.ts +2 -2
  5. package/src/bin/nrepl-symbol.ts +150 -0
  6. package/src/bin/nrepl.ts +301 -157
  7. package/src/bin/version.ts +1 -1
  8. package/src/clojure/core.clj +764 -29
  9. package/src/clojure/core.clj.d.ts +76 -4
  10. package/src/clojure/demo/math.clj +5 -1
  11. package/src/clojure/generated/builtin-namespace-registry.ts +4 -0
  12. package/src/clojure/generated/clojure-core-source.ts +765 -29
  13. package/src/clojure/generated/clojure-set-source.ts +136 -0
  14. package/src/clojure/generated/clojure-walk-source.ts +72 -0
  15. package/src/clojure/set.clj +132 -0
  16. package/src/clojure/set.clj.d.ts +20 -0
  17. package/src/clojure/string.clj.d.ts +14 -0
  18. package/src/clojure/walk.clj +68 -0
  19. package/src/clojure/walk.clj.d.ts +7 -0
  20. package/src/core/assertions.ts +114 -6
  21. package/src/core/bootstrap.ts +337 -0
  22. package/src/core/conversions.ts +48 -31
  23. package/src/core/core-module.ts +303 -0
  24. package/src/core/env.ts +42 -7
  25. package/src/core/errors.ts +8 -0
  26. package/src/core/evaluator/apply.ts +40 -25
  27. package/src/core/evaluator/arity.ts +8 -8
  28. package/src/core/evaluator/async-evaluator.ts +565 -0
  29. package/src/core/evaluator/collections.ts +30 -4
  30. package/src/core/evaluator/destructure.ts +180 -69
  31. package/src/core/evaluator/dispatch.ts +24 -14
  32. package/src/core/evaluator/evaluate.ts +22 -20
  33. package/src/core/evaluator/expand.ts +45 -15
  34. package/src/core/evaluator/form-parsers.ts +178 -0
  35. package/src/core/evaluator/index.ts +7 -9
  36. package/src/core/evaluator/js-interop.ts +189 -0
  37. package/src/core/evaluator/quasiquote.ts +14 -8
  38. package/src/core/evaluator/recur-check.ts +6 -6
  39. package/src/core/evaluator/special-forms.ts +380 -173
  40. package/src/core/factories.ts +182 -3
  41. package/src/core/index.ts +55 -5
  42. package/src/core/module.ts +136 -0
  43. package/src/core/ns-forms.ts +107 -0
  44. package/src/core/positions.ts +9 -2
  45. package/src/core/printer.ts +371 -11
  46. package/src/core/reader.ts +127 -29
  47. package/src/core/registry.ts +209 -0
  48. package/src/core/runtime.ts +376 -0
  49. package/src/core/session.ts +263 -478
  50. package/src/core/stdlib/arithmetic.ts +516 -215
  51. package/src/core/stdlib/async-fns.ts +132 -0
  52. package/src/core/stdlib/atoms.ts +286 -63
  53. package/src/core/stdlib/errors.ts +54 -50
  54. package/src/core/stdlib/hof.ts +74 -173
  55. package/src/core/stdlib/js-namespace.ts +344 -0
  56. package/src/core/stdlib/lazy.ts +34 -0
  57. package/src/core/stdlib/maps-sets.ts +322 -0
  58. package/src/core/stdlib/meta.ts +109 -28
  59. package/src/core/stdlib/predicates.ts +322 -196
  60. package/src/core/stdlib/regex.ts +126 -98
  61. package/src/core/stdlib/seq.ts +564 -0
  62. package/src/core/stdlib/strings.ts +164 -135
  63. package/src/core/stdlib/transducers.ts +95 -100
  64. package/src/core/stdlib/utils.ts +283 -147
  65. package/src/core/stdlib/vars.ts +27 -27
  66. package/src/core/stdlib/vectors.ts +122 -0
  67. package/src/core/tokenizer.ts +13 -3
  68. package/src/core/transformations.ts +117 -9
  69. package/src/core/types.ts +118 -6
  70. package/src/host/node-host-module.ts +74 -0
  71. package/src/nrepl/relay.ts +432 -0
  72. package/src/vite-plugin-clj/codegen.ts +87 -95
  73. package/src/vite-plugin-clj/index.ts +242 -18
  74. package/src/vite-plugin-clj/namespace-utils.ts +39 -0
  75. package/src/vite-plugin-clj/static-analysis.ts +211 -0
  76. package/src/clojure/demo.clj +0 -63
  77. package/src/clojure/demo.clj.d.ts +0 -0
  78. package/src/core/core-env.ts +0 -60
  79. package/src/core/stdlib/collections.ts +0 -784
  80. package/src/host/node.ts +0 -55
@@ -1,108 +1,27 @@
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
- // map: cljNativeFunctionWithContext(
22
- // 'map',
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 || !isAFunction(fn)) {
103
- throw new EvaluationError(
19
+ ) {
20
+ if (fn === undefined || !is.aFunction(fn)) {
21
+ throw EvaluationError.atArg(
104
22
  `reduce expects a function as first argument${fn !== undefined ? `, got ${printString(fn)}` : ''}`,
105
- { fn }
23
+ { fn },
24
+ 0
106
25
  )
107
26
  }
108
27
  if (rest.length === 0 || rest.length > 2) {
@@ -127,10 +46,12 @@ export const hofFunctions: Record<string, CljValue> = {
127
46
  return init!
128
47
  }
129
48
 
130
- if (!isSeqable(collection)) {
131
- throw new EvaluationError(
49
+ if (!is.seqable(collection)) {
50
+ // collection is at args[rest.length]: 1 for (reduce f coll), 2 for (reduce f init coll)
51
+ throw EvaluationError.atArg(
132
52
  `reduce expects a collection or string, got ${printString(collection)}`,
133
- { collection }
53
+ { collection },
54
+ rest.length
134
55
  )
135
56
  }
136
57
 
@@ -147,7 +68,7 @@ export const hofFunctions: Record<string, CljValue> = {
147
68
  let acc = items[0]
148
69
  for (let i = 1; i < items.length; i++) {
149
70
  const result = ctx.applyFunction(fn, [acc, items[i]], callEnv)
150
- if (isReduced(result)) return result.value
71
+ if (is.reduced(result)) return result.value
151
72
  acc = result
152
73
  }
153
74
  return acc
@@ -156,21 +77,22 @@ export const hofFunctions: Record<string, CljValue> = {
156
77
  let acc = init!
157
78
  for (const item of items) {
158
79
  const result = ctx.applyFunction(fn, [acc, item], callEnv)
159
- if (isReduced(result)) return result.value
80
+ if (is.reduced(result)) return result.value
160
81
  acc = result
161
82
  }
162
83
  return acc
163
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
+ ]
164
92
  ),
165
- 'Reduces a collection to a single value by iteratively applying f. (reduce f coll) or (reduce f init coll).',
166
- [
167
- ['f', 'coll'],
168
- ['f', 'val', 'coll'],
169
- ]
170
- ),
171
93
 
172
- apply: withDoc(
173
- cljNativeFunctionWithContext(
94
+ apply: v
95
+ .nativeFnCtx(
174
96
  'apply',
175
97
  (
176
98
  ctx: EvaluationContext,
@@ -178,10 +100,11 @@ export const hofFunctions: Record<string, CljValue> = {
178
100
  fn: CljValue | undefined,
179
101
  ...rest: CljValue[]
180
102
  ) => {
181
- if (fn === undefined || !isCallable(fn)) {
182
- throw new EvaluationError(
103
+ if (fn === undefined || !is.callable(fn)) {
104
+ throw EvaluationError.atArg(
183
105
  `apply expects a callable as first argument${fn !== undefined ? `, got ${printString(fn)}` : ''}`,
184
- { fn }
106
+ { fn },
107
+ 0
185
108
  )
186
109
  }
187
110
  if (rest.length === 0) {
@@ -190,37 +113,41 @@ export const hofFunctions: Record<string, CljValue> = {
190
113
  })
191
114
  }
192
115
  const lastArg = rest[rest.length - 1]
193
- if (!isNil(lastArg) && !isSeqable(lastArg)) {
194
- throw new EvaluationError(
116
+ if (!is.nil(lastArg) && !is.seqable(lastArg)) {
117
+ // last arg is at index rest.length (fn=0, rest[0]=1, ..., rest[n-1]=n)
118
+ throw EvaluationError.atArg(
195
119
  `apply expects a collection or string as last argument, got ${printString(lastArg)}`,
196
- { lastArg }
120
+ { lastArg },
121
+ rest.length
197
122
  )
198
123
  }
199
124
 
200
125
  const args = [
201
126
  ...rest.slice(0, -1),
202
- ...(isNil(lastArg) ? [] : toSeq(lastArg)),
127
+ ...(is.nil(lastArg) ? [] : toSeq(lastArg)),
203
128
  ]
204
129
  return ctx.applyCallable(fn, args, callEnv)
205
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
+ ]
206
138
  ),
207
- 'Calls f with the elements of the last argument (a collection) as its arguments, optionally prepended by fixed args.',
208
- [
209
- ['f', 'args'],
210
- ['f', '&', 'args'],
211
- ]
212
- ),
213
139
 
214
- partial: withDoc(
215
- cljNativeFunction('partial', (fn: CljValue, ...preArgs: CljValue[]) => {
216
- if (fn === undefined || !isCallable(fn)) {
217
- throw new EvaluationError(
140
+ partial: v
141
+ .nativeFn('partial', (fn: CljValue, ...preArgs: CljValue[]) => {
142
+ if (fn === undefined || !is.callable(fn)) {
143
+ throw EvaluationError.atArg(
218
144
  `partial expects a callable as first argument${fn !== undefined ? `, got ${printString(fn)}` : ''}`,
219
- { fn }
145
+ { fn },
146
+ 0
220
147
  )
221
148
  }
222
149
  const capturedFn = fn
223
- return cljNativeFunctionWithContext(
150
+ return v.nativeFnCtx(
224
151
  'partial',
225
152
  (ctx: EvaluationContext, callEnv: Env, ...moreArgs: CljValue[]) => {
226
153
  return ctx.applyCallable(
@@ -230,24 +157,27 @@ export const hofFunctions: Record<string, CljValue> = {
230
157
  )
231
158
  }
232
159
  )
233
- }),
234
- 'Returns a function that calls f with pre-applied args prepended to any additional arguments.',
235
- [['f', '&', 'args']]
236
- ),
160
+ })
161
+ .doc(
162
+ 'Returns a function that calls f with pre-applied args prepended to any additional arguments.',
163
+ [['f', '&', 'args']]
164
+ ),
237
165
 
238
- comp: withDoc(
239
- cljNativeFunction('comp', (...fns: CljValue[]) => {
166
+ comp: v
167
+ .nativeFn('comp', (...fns: CljValue[]) => {
240
168
  if (fns.length === 0) {
241
- return cljNativeFunction('identity', (x: CljValue) => x)
169
+ return v.nativeFn('identity', (x: CljValue) => x)
242
170
  }
243
- if (fns.some((f) => !isCallable(f))) {
244
- throw new EvaluationError(
171
+ const badIdx = fns.findIndex((f) => !is.callable(f))
172
+ if (badIdx !== -1) {
173
+ throw EvaluationError.atArg(
245
174
  'comp expects functions or other callable values (keywords, maps)',
246
- { fns }
175
+ { fns },
176
+ badIdx
247
177
  )
248
178
  }
249
179
  const capturedFns = fns
250
- return cljNativeFunctionWithContext(
180
+ return v.nativeFnCtx(
251
181
  'composed',
252
182
  (ctx: EvaluationContext, callEnv: Env, ...args: CljValue[]) => {
253
183
  let result = ctx.applyCallable(
@@ -261,47 +191,18 @@ export const hofFunctions: Record<string, CljValue> = {
261
191
  return result
262
192
  }
263
193
  )
264
- }),
265
- '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.',
266
- [[], ['f'], ['f', 'g'], ['f', 'g', '&', 'fns']]
267
- ),
268
-
269
- // 'map-indexed': cljNativeFunctionWithContext(
270
- // 'map-indexed',
271
- // (ctx: EvaluationContext, fn: CljValue, coll: CljValue): CljValue => {
272
- // if (fn === undefined || !isAFunction(fn)) {
273
- // throw new EvaluationError(
274
- // `map-indexed expects a function as first argument${fn !== undefined ? `, got ${printString(fn)}` : ''}`,
275
- // { fn }
276
- // )
277
- // }
278
- // if (coll === undefined || !isCollection(coll)) {
279
- // throw new EvaluationError(
280
- // `map-indexed expects a collection as second argument${coll !== undefined ? `, got ${printString(coll)}` : ''}`,
281
- // { coll }
282
- // )
283
- // }
284
- // const items = toSeq(coll)
285
- // const wrap = isVector(coll) ? cljVector : cljList
286
- // return wrap(
287
- // items.map((item, idx) =>
288
- // ctx.applyFunction(fn as CljFunction | CljNativeFunction, [
289
- // cljNumber(idx),
290
- // item,
291
- // ])
292
- // )
293
- // )
294
- // }
295
- // ),
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
+ ),
296
199
 
297
- identity: withDoc(
298
- cljNativeFunction('identity', (x: CljValue) => {
200
+ identity: v
201
+ .nativeFn('identity', (x: CljValue) => {
299
202
  if (x === undefined) {
300
- throw new EvaluationError('identity expects one argument', {})
203
+ throw EvaluationError.atArg('identity expects one argument', {}, 0)
301
204
  }
302
205
  return x
303
- }),
304
- 'Returns its single argument unchanged.',
305
- [['x']]
306
- ),
206
+ })
207
+ .doc('Returns its single argument unchanged.', [['x']]),
307
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
+ }