conjure-js 0.0.13 → 0.0.14

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 (58) hide show
  1. package/dist-cli/conjure-js.mjs +2328 -2089
  2. package/dist-vite-plugin/index.mjs +2327 -2088
  3. package/package.json +1 -1
  4. package/src/bin/version.ts +1 -1
  5. package/src/core/assertions.ts +10 -3
  6. package/src/core/bootstrap.ts +7 -23
  7. package/src/core/compiler/binding.ts +164 -0
  8. package/src/core/compiler/callable.ts +41 -0
  9. package/src/core/compiler/compile-env.ts +40 -0
  10. package/src/core/compiler/control-flow.ts +79 -0
  11. package/src/core/compiler/index.ts +121 -0
  12. package/src/core/env.ts +4 -4
  13. package/src/core/errors.ts +1 -0
  14. package/src/core/evaluator/apply.ts +7 -3
  15. package/src/core/evaluator/arity.ts +16 -6
  16. package/src/core/evaluator/async-evaluator.ts +68 -89
  17. package/src/core/evaluator/collections.ts +9 -4
  18. package/src/core/evaluator/destructure.ts +45 -55
  19. package/src/core/evaluator/dispatch.ts +21 -24
  20. package/src/core/evaluator/evaluate.ts +14 -2
  21. package/src/core/evaluator/expand.ts +5 -7
  22. package/src/core/evaluator/js-interop.ts +46 -33
  23. package/src/core/evaluator/quasiquote.ts +7 -11
  24. package/src/core/evaluator/recur-check.ts +1 -1
  25. package/src/core/evaluator/special-forms.ts +18 -38
  26. package/src/core/index.ts +1 -1
  27. package/src/core/keywords.ts +105 -0
  28. package/src/core/modules/core/index.ts +131 -0
  29. package/src/core/{stdlib → modules/core/stdlib}/arithmetic.ts +6 -6
  30. package/src/core/{stdlib → modules/core/stdlib}/async-fns.ts +6 -6
  31. package/src/core/{stdlib → modules/core/stdlib}/atoms.ts +7 -7
  32. package/src/core/{stdlib → modules/core/stdlib}/errors.ts +4 -4
  33. package/src/core/{stdlib → modules/core/stdlib}/hof.ts +6 -6
  34. package/src/core/{stdlib → modules/core/stdlib}/lazy.ts +4 -4
  35. package/src/core/{stdlib → modules/core/stdlib}/maps-sets.ts +6 -6
  36. package/src/core/{stdlib → modules/core/stdlib}/meta.ts +5 -5
  37. package/src/core/{stdlib → modules/core/stdlib}/predicates.ts +7 -7
  38. package/src/core/modules/core/stdlib/print.ts +108 -0
  39. package/src/core/{stdlib → modules/core/stdlib}/regex.ts +5 -5
  40. package/src/core/{stdlib → modules/core/stdlib}/seq.ts +7 -7
  41. package/src/core/{stdlib → modules/core/stdlib}/strings.ts +6 -6
  42. package/src/core/{stdlib → modules/core/stdlib}/transducers.ts +6 -6
  43. package/src/core/{stdlib → modules/core/stdlib}/utils.ts +10 -10
  44. package/src/core/{stdlib → modules/core/stdlib}/vars.ts +4 -4
  45. package/src/core/{stdlib → modules/core/stdlib}/vectors.ts +6 -6
  46. package/src/core/modules/js/index.ts +402 -0
  47. package/src/core/ns-forms.ts +25 -17
  48. package/src/core/positions.ts +22 -2
  49. package/src/core/printer.ts +162 -53
  50. package/src/core/reader.ts +25 -22
  51. package/src/core/registry.ts +10 -10
  52. package/src/core/runtime.ts +23 -23
  53. package/src/core/session.ts +17 -7
  54. package/src/core/tokenizer.ts +14 -4
  55. package/src/core/transformations.ts +48 -29
  56. package/src/core/types.ts +57 -81
  57. package/src/core/core-module.ts +0 -303
  58. package/src/core/stdlib/js-namespace.ts +0 -344
@@ -1,303 +0,0 @@
1
- import type {
2
- RuntimeModule,
3
- VarDeclaration,
4
- VarMap,
5
- ModuleContext,
6
- } from './module'
7
- import { buildPrintContext, prettyPrintString, printString, withPrintContext } from './printer'
8
- import { derefValue } from './env'
9
- import { valueToString } from './transformations'
10
- import type { CljMap, CljValue, Env, EvaluationContext } from './types'
11
- import { arithmeticFunctions } from './stdlib/arithmetic'
12
- import { atomFunctions } from './stdlib/atoms'
13
- import { mapsSetsFunctions } from './stdlib/maps-sets'
14
- import { seqFunctions } from './stdlib/seq'
15
- import { vectorFunctions } from './stdlib/vectors'
16
- import { errorFunctions } from './stdlib/errors'
17
- import { hofFunctions } from './stdlib/hof'
18
- import { metaFunctions } from './stdlib/meta'
19
- import { predicateFunctions } from './stdlib/predicates'
20
- import { regexFunctions } from './stdlib/regex'
21
- import { stringFunctions } from './stdlib/strings'
22
- import { transducerFunctions } from './stdlib/transducers'
23
- import { utilFunctions } from './stdlib/utils'
24
- import { lazyFunctions } from './stdlib/lazy'
25
- import { varFunctions } from './stdlib/vars'
26
- // --- ASYNC (experimental) ---
27
- import { asyncFunctions } from './stdlib/async-fns'
28
- import { v } from './factories'
29
- import { is } from './assertions'
30
- import { EvaluationError } from './errors'
31
- import { cljToJs as cljToJsDeep, jsToClj as jsToCljDeep, type FunctionApplier } from './conversions'
32
- // --- END ASYNC ---
33
-
34
- // ---------------------------------------------------------------------------
35
- // Native function registry — installed as the initial clojure.core binding
36
- // set by createRuntime.
37
- //
38
- // INTENTIONAL BOOTSTRAP OVERRIDES:
39
- // Several functions below are redefined by clojure.core.clj after bootstrap.
40
- // They serve as scaffolding during the bootstrap phase only. The Clojure
41
- // source versions (lazy / transducer-aware) become authoritative once loaded.
42
- // Overridden by clojure.core.clj:
43
- // concat → lazy recursive version (shadows eager native)
44
- // map → lazy + transducer 2-arity
45
- // filter → lazy + transducer 2-arity
46
- // take → lazy + stateful transducer
47
- // drop → lazy + stateful transducer
48
- // take-while → lazy + stateless transducer
49
- // drop-while → lazy + stateful transducer
50
- // map-indexed → lazy + stateful transducer
51
- // keep → lazy + transducer
52
- // keep-indexed→ lazy + stateful transducer
53
- // mapcat → lazy + transducer
54
- // partition-by→ lazy + stateful transducer
55
- // iterate → lazy infinite sequence
56
- // repeatedly → lazy infinite sequence
57
- // cycle → lazy infinite sequence
58
- // repeat → lazy infinite (delegates to repeat* for finite arity)
59
- // range → lazy infinite (delegates to range* for finite arity)
60
- // into → 2-arity uses reduce+conj; 3-arity uses transduce
61
- // sequence → materialise via into
62
- // completing → 0-arity init + 1-arity completion wrapper
63
- // newline → redefined as (defn newline [] (println ""))
64
- // not → redefined as pure Clojure (if x false true)
65
- // dorun → redefined in Clojure
66
- // doall → redefined in Clojure
67
- //
68
- // Dynamic vars declared here (NOT overridden by clojure.core.clj):
69
- // *out* → nil by default; bound by with-out-str to capture stdout
70
- // *err* → nil by default; bound by with-err-str to capture stderr
71
- // *print-length*, *print-level* → print control
72
- //
73
- // range* and repeat* are intentionally kept — clojure.core.clj calls them
74
- // explicitly as private native helpers for finite-arity range/repeat.
75
- // ---------------------------------------------------------------------------
76
-
77
- const nativeFunctions = {
78
- ...arithmeticFunctions,
79
- ...atomFunctions,
80
- ...seqFunctions,
81
- ...vectorFunctions,
82
- ...mapsSetsFunctions,
83
- ...errorFunctions,
84
- ...predicateFunctions,
85
- ...hofFunctions,
86
- ...metaFunctions,
87
- ...transducerFunctions,
88
- ...regexFunctions,
89
- ...stringFunctions,
90
- ...utilFunctions,
91
- ...varFunctions,
92
- ...lazyFunctions,
93
- // --- ASYNC (experimental) ---
94
- ...asyncFunctions,
95
- // --- END ASYNC ---
96
- }
97
-
98
-
99
- /**
100
- * Emit text to the current output channel.
101
- * If *out* is dynamically bound to a callable (e.g. inside with-out-str),
102
- * invoke it. Otherwise fall back to ctx.io.stdout.
103
- *
104
- * NOTE: We look up *out* via ctx.resolveNs('clojure.core') rather than
105
- * tryLookup(callEnv) because Clojure functions close over the original
106
- * snapshot env. After a snapshot restore, those closure envs point to the
107
- * old CljVar objects, not the session's freshly cloned ones. resolveNs goes
108
- * through the runtime registry and always returns the session's own var.
109
- */
110
- function emitToOut(ctx: EvaluationContext, callEnv: Env, text: string): void {
111
- const outVar = ctx.resolveNs('clojure.core')?.vars.get('*out*')
112
- const out = outVar ? derefValue(outVar) : undefined
113
- if (out && (out.kind === 'function' || out.kind === 'native-function')) {
114
- ctx.applyCallable(out, [v.string(text)], callEnv)
115
- } else {
116
- ctx.io.stdout(text)
117
- }
118
- }
119
-
120
- /**
121
- * Emit text to the current error channel.
122
- * If *err* is dynamically bound to a callable (e.g. inside with-err-str),
123
- * invoke it. Otherwise fall back to ctx.io.stderr.
124
- * Same snapshot-env rationale as emitToOut.
125
- */
126
- function emitToErr(ctx: EvaluationContext, callEnv: Env, text: string): void {
127
- const errVar = ctx.resolveNs('clojure.core')?.vars.get('*err*')
128
- const err = errVar ? derefValue(errVar) : undefined
129
- if (err && (err.kind === 'function' || err.kind === 'native-function')) {
130
- ctx.applyCallable(err, [v.string(text)], callEnv)
131
- } else {
132
- ctx.io.stderr(text)
133
- }
134
- }
135
-
136
- /**
137
- * Returns the full clojure.core RuntimeModule.
138
- *
139
- * IO functions (println, print, newline, pr, prn, pprint) read ctx.io.stdout
140
- * at call time instead of closing over an emit callback. This means:
141
- * - No output parameter needed here
142
- * - Snapshot clones automatically use the correct output without reinstalling
143
- * any IO vars (restoreRuntime no longer needs makeIOModule)
144
- */
145
- export function makeCoreModule(): RuntimeModule {
146
- return {
147
- id: 'clojure/core',
148
- declareNs: [
149
- {
150
- name: 'clojure.core',
151
- vars(_ctx: ModuleContext): VarMap {
152
- const map = new Map<string, VarDeclaration>()
153
-
154
- // Pure stdlib functions (all have .meta via .doc())
155
- for (const [name, fn] of Object.entries(nativeFunctions)) {
156
- const meta = (fn as { meta?: CljMap }).meta
157
- map.set(name, { value: fn, ...(meta ? { meta } : {}) })
158
- }
159
-
160
- // IO functions — route through emitToOut/emitToErr so that
161
- // *out*/*err* dynamic bindings (e.g. inside with-out-str) are honoured.
162
- map.set('println', {
163
- value: v.nativeFnCtx(
164
- 'println',
165
- (ctx, callEnv, ...args: CljValue[]) => {
166
- withPrintContext(buildPrintContext(ctx), () => {
167
- emitToOut(
168
- ctx,
169
- callEnv,
170
- args.map(valueToString).join(' ') + '\n'
171
- )
172
- })
173
- return v.nil()
174
- }
175
- ),
176
- })
177
- map.set('print', {
178
- value: v.nativeFnCtx(
179
- 'print',
180
- (ctx, callEnv, ...args: CljValue[]) => {
181
- withPrintContext(buildPrintContext(ctx), () => {
182
- emitToOut(ctx, callEnv, args.map(valueToString).join(' '))
183
- })
184
- return v.nil()
185
- }
186
- ),
187
- })
188
- map.set('newline', {
189
- value: v.nativeFnCtx('newline', (ctx, callEnv) => {
190
- emitToOut(ctx, callEnv, '\n')
191
- return v.nil()
192
- }),
193
- })
194
- map.set('pr', {
195
- value: v.nativeFnCtx('pr', (ctx, callEnv, ...args: CljValue[]) => {
196
- withPrintContext(buildPrintContext(ctx), () => {
197
- emitToOut(
198
- ctx,
199
- callEnv,
200
- args.map((v) => printString(v)).join(' ')
201
- )
202
- })
203
- return v.nil()
204
- }),
205
- })
206
- map.set('prn', {
207
- value: v.nativeFnCtx('prn', (ctx, callEnv, ...args: CljValue[]) => {
208
- withPrintContext(buildPrintContext(ctx), () => {
209
- emitToOut(
210
- ctx,
211
- callEnv,
212
- args.map((v) => printString(v)).join(' ') + '\n'
213
- )
214
- })
215
- return v.nil()
216
- }),
217
- })
218
- map.set('pprint', {
219
- value: v.nativeFnCtx(
220
- 'pprint',
221
- (ctx, callEnv, form: CljValue, widthArg?: CljValue) => {
222
- if (form === undefined) return v.nil()
223
- const maxWidth =
224
- widthArg?.kind === 'number' ? widthArg.value : 80
225
- withPrintContext(buildPrintContext(ctx), () => {
226
- emitToOut(
227
- ctx,
228
- callEnv,
229
- prettyPrintString(form, maxWidth) + '\n'
230
- )
231
- })
232
- return v.nil()
233
- }
234
- ),
235
- })
236
- map.set('warn', {
237
- value: v.nativeFnCtx(
238
- 'warn',
239
- (ctx, callEnv, ...args: CljValue[]) => {
240
- withPrintContext(buildPrintContext(ctx), () => {
241
- emitToErr(
242
- ctx,
243
- callEnv,
244
- args.map(valueToString).join(' ') + '\n'
245
- )
246
- })
247
- return v.nil()
248
- }
249
- ),
250
- })
251
-
252
- // Dynamic output-channel vars. IO functions check these first before
253
- // falling back to ctx.io.stdout / ctx.io.stderr. Bound by with-out-str
254
- // and with-err-str macros defined in clojure.core.
255
- map.set('*out*', { value: v.nil(), dynamic: true })
256
- map.set('*err*', { value: v.nil(), dynamic: true })
257
-
258
- // Dynamic print-control vars
259
- map.set('*print-length*', { value: v.nil(), dynamic: true })
260
- map.set('*print-level*', { value: v.nil(), dynamic: true })
261
-
262
- // Compatibility var for IDE tooling
263
- map.set('*compiler-options*', { value: v.map([]) })
264
-
265
- // JS interop — deep conversion functions
266
- map.set('clj->js', {
267
- value: v.nativeFnCtx('clj->js', (ctx: EvaluationContext, callEnv: Env, val: CljValue) => {
268
- if (is.jsValue(val)) return val
269
- const applier: FunctionApplier = {
270
- applyFunction: (fn, args) => ctx.applyCallable(fn, args, callEnv),
271
- }
272
- return v.jsValue(cljToJsDeep(val, applier))
273
- }),
274
- })
275
-
276
- map.set('js->clj', {
277
- value: v.nativeFn('js->clj', (val: CljValue, opts?: CljValue) => {
278
- if (val.kind === 'nil') return val
279
- if (!is.jsValue(val)) {
280
- throw new EvaluationError(
281
- `js->clj expects a js-value, got ${val.kind}`,
282
- { val }
283
- )
284
- }
285
- const keywordizeKeys = (() => {
286
- if (!opts || opts.kind !== 'map') return false
287
- for (const [k, flag] of opts.entries) {
288
- if (k.kind === 'keyword' && k.name === ':keywordize-keys') {
289
- return flag.kind !== 'boolean' || flag.value !== false
290
- }
291
- }
292
- return false
293
- })()
294
- return jsToCljDeep(val.value, { keywordizeKeys })
295
- }),
296
- })
297
-
298
- return map
299
- },
300
- },
301
- ],
302
- }
303
- }
@@ -1,344 +0,0 @@
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
- }