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
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared structural parsers for special forms.
|
|
3
|
+
*
|
|
4
|
+
* These helpers are pure data transformations — no evaluation, no side effects.
|
|
5
|
+
* Both the sync evaluator (special-forms.ts) and the async evaluator
|
|
6
|
+
* (async-evaluator.ts) import from here so the logic lives in exactly one place.
|
|
7
|
+
*
|
|
8
|
+
* matchesDiscriminator also lives here because it is the same algorithm in both
|
|
9
|
+
* paths — only the EvaluationContext it receives differs (the real ctx in sync,
|
|
10
|
+
* asyncCtx.syncCtx in async, which is correct: discriminator matching is
|
|
11
|
+
* inherently synchronous).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { is } from '../assertions'
|
|
15
|
+
import { EvaluationError } from '../errors'
|
|
16
|
+
import type {
|
|
17
|
+
CljFunction,
|
|
18
|
+
CljList,
|
|
19
|
+
CljNativeFunction,
|
|
20
|
+
CljValue,
|
|
21
|
+
CljVector,
|
|
22
|
+
Env,
|
|
23
|
+
EvaluationContext,
|
|
24
|
+
} from '../types'
|
|
25
|
+
|
|
26
|
+
// ---- let / loop bindings ----
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validates that a binding vector is well-formed: must be a vector with an
|
|
30
|
+
* even number of forms. Used by let and loop handlers in both the sync and
|
|
31
|
+
* async evaluators so the error messages are consistent.
|
|
32
|
+
*
|
|
33
|
+
* @param formName Display name for the error message, e.g. 'let' or 'loop'.
|
|
34
|
+
*/
|
|
35
|
+
export function validateBindingVector(
|
|
36
|
+
vec: CljValue,
|
|
37
|
+
formName: string,
|
|
38
|
+
env: Env
|
|
39
|
+
): asserts vec is CljVector {
|
|
40
|
+
if (!is.vector(vec)) {
|
|
41
|
+
throw new EvaluationError(`${formName} bindings must be a vector`, {
|
|
42
|
+
bindings: vec,
|
|
43
|
+
env,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
if (vec.value.length % 2 !== 0) {
|
|
47
|
+
throw new EvaluationError(
|
|
48
|
+
`${formName} bindings must have an even number of forms`,
|
|
49
|
+
{ bindings: vec, env }
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---- try ----
|
|
55
|
+
|
|
56
|
+
export type CatchClause = {
|
|
57
|
+
discriminator: CljValue
|
|
58
|
+
binding: string
|
|
59
|
+
body: CljValue[]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type TryStructure = {
|
|
63
|
+
bodyForms: CljValue[]
|
|
64
|
+
catchClauses: CatchClause[]
|
|
65
|
+
finallyForms: CljValue[] | null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Splits a (try ...) list into body forms, catch clauses, and an optional
|
|
70
|
+
* finally clause. Validates that catch has a discriminator + binding symbol and
|
|
71
|
+
* that finally (if present) is the last form.
|
|
72
|
+
*/
|
|
73
|
+
export function parseTryStructure(list: CljList, env: Env): TryStructure {
|
|
74
|
+
const forms = list.value.slice(1)
|
|
75
|
+
const bodyForms: CljValue[] = []
|
|
76
|
+
const catchClauses: CatchClause[] = []
|
|
77
|
+
let finallyForms: CljValue[] | null = null
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < forms.length; i++) {
|
|
80
|
+
const form = forms[i]
|
|
81
|
+
if (is.list(form) && form.value.length > 0 && is.symbol(form.value[0])) {
|
|
82
|
+
const head = form.value[0].name
|
|
83
|
+
|
|
84
|
+
if (head === 'catch') {
|
|
85
|
+
if (form.value.length < 3) {
|
|
86
|
+
throw new EvaluationError(
|
|
87
|
+
'catch requires a discriminator and a binding symbol',
|
|
88
|
+
{ form, env }
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
const discriminator = form.value[1]
|
|
92
|
+
const bindingSym = form.value[2]
|
|
93
|
+
if (!is.symbol(bindingSym)) {
|
|
94
|
+
throw new EvaluationError('catch binding must be a symbol', {
|
|
95
|
+
form,
|
|
96
|
+
env,
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
catchClauses.push({
|
|
100
|
+
discriminator,
|
|
101
|
+
binding: bindingSym.name,
|
|
102
|
+
body: form.value.slice(3),
|
|
103
|
+
})
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (head === 'finally') {
|
|
108
|
+
if (i !== forms.length - 1) {
|
|
109
|
+
throw new EvaluationError(
|
|
110
|
+
'finally clause must be the last in try expression',
|
|
111
|
+
{ form, env }
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
finallyForms = form.value.slice(1)
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
bodyForms.push(form)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { bodyForms, catchClauses, finallyForms }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Determines whether a catch clause's discriminator matches a thrown value.
|
|
126
|
+
*
|
|
127
|
+
* Rules (same as Clojure-on-JVM, adapted for JS runtime):
|
|
128
|
+
* - Discriminator evaluates to a symbol → catch-all (JVM class names fall here)
|
|
129
|
+
* - `:default` keyword → catch-all
|
|
130
|
+
* - Any other keyword → matches if thrown is a map with a :type entry equal to
|
|
131
|
+
* the discriminator keyword
|
|
132
|
+
* - A callable function → call it with the thrown value; truthy = match
|
|
133
|
+
* - Anything else → error
|
|
134
|
+
*
|
|
135
|
+
* @param ctx Use the real EvaluationContext in sync paths; use asyncCtx.syncCtx
|
|
136
|
+
* in async paths. Discriminator matching is always synchronous.
|
|
137
|
+
*/
|
|
138
|
+
export function matchesDiscriminator(
|
|
139
|
+
discriminator: CljValue,
|
|
140
|
+
thrown: CljValue,
|
|
141
|
+
env: Env,
|
|
142
|
+
ctx: EvaluationContext
|
|
143
|
+
): boolean {
|
|
144
|
+
let disc: CljValue
|
|
145
|
+
try {
|
|
146
|
+
disc = ctx.evaluate(discriminator, env)
|
|
147
|
+
} catch {
|
|
148
|
+
// Discriminator failed to evaluate (e.g. unresolvable Java class name like
|
|
149
|
+
// java.lang.Throwable). Treat as catch-all — we're not on the JVM.
|
|
150
|
+
return true
|
|
151
|
+
}
|
|
152
|
+
// A symbol that evaluated to itself (shouldn't happen, but guard anyway)
|
|
153
|
+
if (disc.kind === 'symbol') return true
|
|
154
|
+
|
|
155
|
+
if (is.keyword(disc)) {
|
|
156
|
+
if (disc.name === ':default') return true
|
|
157
|
+
if (!is.map(thrown)) return false
|
|
158
|
+
const typeEntry = thrown.entries.find(
|
|
159
|
+
([k]) => is.keyword(k) && k.name === ':type'
|
|
160
|
+
)
|
|
161
|
+
if (!typeEntry) return false
|
|
162
|
+
return is.equal(typeEntry[1], disc)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (is.aFunction(disc)) {
|
|
166
|
+
const result = ctx.applyFunction(
|
|
167
|
+
disc as CljFunction | CljNativeFunction,
|
|
168
|
+
[thrown],
|
|
169
|
+
env
|
|
170
|
+
)
|
|
171
|
+
return is.truthy(result)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
throw new EvaluationError(
|
|
175
|
+
'catch discriminator must be a keyword or a predicate function',
|
|
176
|
+
{ discriminator: disc, env }
|
|
177
|
+
)
|
|
178
|
+
}
|
|
@@ -39,12 +39,19 @@ export function createEvaluationContext(): EvaluationContext {
|
|
|
39
39
|
applyMacroWithContext(macro, rawArgs, ctx),
|
|
40
40
|
expandAll: (form: CljValue, env: Env) =>
|
|
41
41
|
macroExpandAllWithContext(form, env, ctx),
|
|
42
|
+
resolveNs: (_name: string) => null as null,
|
|
43
|
+
// IO defaults — overwritten by buildSessionFacade with session-specific channels.
|
|
44
|
+
io: {
|
|
45
|
+
stdout: (text: string) => console.log(text),
|
|
46
|
+
stderr: (text: string) => console.error(text),
|
|
47
|
+
},
|
|
42
48
|
}
|
|
43
49
|
return ctx
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
/** Public API, this is the only place where we create a new evaluation context
|
|
47
53
|
* All inner evaluations will use the same context
|
|
54
|
+
* @deprecated use session.applyFunction instead
|
|
48
55
|
*/
|
|
49
56
|
export function applyFunction(
|
|
50
57
|
fn: CljFunction | CljNativeFunction,
|
|
@@ -53,15 +60,6 @@ export function applyFunction(
|
|
|
53
60
|
): CljValue {
|
|
54
61
|
return createEvaluationContext().applyFunction(fn, args, callEnv)
|
|
55
62
|
}
|
|
56
|
-
export function applyMacro(macro: CljMacro, rawArgs: CljValue[]): CljValue {
|
|
57
|
-
return createEvaluationContext().applyMacro(macro, rawArgs)
|
|
58
|
-
}
|
|
59
|
-
export function evaluate(expr: CljValue, env: Env): CljValue {
|
|
60
|
-
return createEvaluationContext().evaluate(expr, env)
|
|
61
|
-
}
|
|
62
|
-
export function evaluateForms(forms: CljValue[], env: Env): CljValue {
|
|
63
|
-
return createEvaluationContext().evaluateForms(forms, env)
|
|
64
|
-
}
|
|
65
63
|
|
|
66
64
|
export function evaluateWithMeasurements(
|
|
67
65
|
expr: CljValue,
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { is } from '../assertions'
|
|
2
|
+
import { EvaluationError } from '../errors'
|
|
3
|
+
import { cljBoolean, cljJsValue, cljNil, cljNumber, cljString } from '../factories'
|
|
4
|
+
import type { CljList, CljValue, Env, EvaluationContext } from '../types'
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// JS ↔ Clojure conversion
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Convert a raw JS value to a CljValue.
|
|
12
|
+
* - null → CljNil (intentional absence)
|
|
13
|
+
* - undefined → CljJsValue(undefined) (property does not exist / unset — distinct from null)
|
|
14
|
+
* - primitives convert; everything else boxes.
|
|
15
|
+
*/
|
|
16
|
+
export function jsToClj(raw: unknown): CljValue {
|
|
17
|
+
if (raw === null) return cljNil()
|
|
18
|
+
if (raw === undefined) return cljJsValue(undefined)
|
|
19
|
+
if (typeof raw === 'number') return cljNumber(raw)
|
|
20
|
+
if (typeof raw === 'string') return cljString(raw)
|
|
21
|
+
if (typeof raw === 'boolean') return cljBoolean(raw)
|
|
22
|
+
return cljJsValue(raw)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Convert a CljValue map key to a JS object key string.
|
|
27
|
+
* Only primitive keys are allowed. Rich keys (vectors, maps, sets, etc.)
|
|
28
|
+
* have no meaningful JS representation and must be reduced to a primitive first.
|
|
29
|
+
*/
|
|
30
|
+
function mapKeyToString(key: CljValue): string {
|
|
31
|
+
if (key.kind === 'string') return key.value
|
|
32
|
+
if (key.kind === 'keyword') return key.name.slice(1) // strip leading ':'
|
|
33
|
+
if (key.kind === 'number') return String(key.value)
|
|
34
|
+
if (key.kind === 'boolean') return String(key.value)
|
|
35
|
+
throw new EvaluationError(
|
|
36
|
+
`cljToJs: map key must be a string, keyword, number, or boolean — ` +
|
|
37
|
+
`got ${key.kind} (rich keys are not allowed as JS object keys; reduce to a primitive first)`,
|
|
38
|
+
{ key }
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Convert a CljValue to a raw JS value for crossing the interop boundary.
|
|
44
|
+
* Called on each argument passed to `.` and `js/new`.
|
|
45
|
+
*/
|
|
46
|
+
export function cljToJs(val: CljValue, ctx: EvaluationContext, callEnv: Env): unknown {
|
|
47
|
+
switch (val.kind) {
|
|
48
|
+
case 'js-value': return val.value
|
|
49
|
+
case 'number': return val.value
|
|
50
|
+
case 'string': return val.value
|
|
51
|
+
case 'boolean': return val.value
|
|
52
|
+
case 'nil': return null
|
|
53
|
+
case 'keyword': return val.name.slice(1) // strip leading ':'
|
|
54
|
+
case 'function':
|
|
55
|
+
case 'native-function': {
|
|
56
|
+
const fn = val
|
|
57
|
+
// Wrap so JS can call it: converts args JS→Clj on entry, result Clj→JS on exit.
|
|
58
|
+
return (...jsArgs: unknown[]) => {
|
|
59
|
+
const cljArgs = jsArgs.map(jsToClj)
|
|
60
|
+
const result = ctx.applyCallable(fn, cljArgs, callEnv)
|
|
61
|
+
return cljToJs(result, ctx, callEnv)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
case 'list':
|
|
65
|
+
case 'vector':
|
|
66
|
+
return val.value.map((v) => cljToJs(v, ctx, callEnv))
|
|
67
|
+
case 'map': {
|
|
68
|
+
const obj: Record<string, unknown> = {}
|
|
69
|
+
for (const [key, value] of val.entries) {
|
|
70
|
+
obj[mapKeyToString(key)] = cljToJs(value, ctx, callEnv)
|
|
71
|
+
}
|
|
72
|
+
return obj
|
|
73
|
+
}
|
|
74
|
+
default:
|
|
75
|
+
throw new EvaluationError(
|
|
76
|
+
`cannot convert ${val.kind} to JS value — no coercion defined`,
|
|
77
|
+
{ val }
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// (. obj prop) / (. obj method arg1 arg2 ...)
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extract the raw JS value from a target CljValue for use in `.`.
|
|
88
|
+
* Strings, numbers, and booleans are auto-boxed (JS auto-promotes them for
|
|
89
|
+
* property/method access). Nil and all other Clojure types are rejected.
|
|
90
|
+
*/
|
|
91
|
+
function extractRawTarget(target: CljValue): unknown {
|
|
92
|
+
switch (target.kind) {
|
|
93
|
+
case 'js-value': return target.value
|
|
94
|
+
case 'string':
|
|
95
|
+
case 'number':
|
|
96
|
+
case 'boolean': return target.value
|
|
97
|
+
default:
|
|
98
|
+
throw new EvaluationError(
|
|
99
|
+
`cannot use . on ${target.kind}`,
|
|
100
|
+
{ target }
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function evaluateDot(
|
|
106
|
+
list: CljList,
|
|
107
|
+
env: Env,
|
|
108
|
+
ctx: EvaluationContext
|
|
109
|
+
): CljValue {
|
|
110
|
+
if (list.value.length < 3) {
|
|
111
|
+
throw new EvaluationError(
|
|
112
|
+
'. requires at least 2 arguments: (. obj prop)',
|
|
113
|
+
{ list }
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const target = ctx.evaluate(list.value[1], env)
|
|
118
|
+
const rawTarget = extractRawTarget(target)
|
|
119
|
+
|
|
120
|
+
if (rawTarget === null || rawTarget === undefined) {
|
|
121
|
+
const label = rawTarget === null ? 'null' : 'undefined'
|
|
122
|
+
throw new EvaluationError(
|
|
123
|
+
`cannot use . on ${label} js value — check for nil/undefined before accessing properties`,
|
|
124
|
+
{ target }
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const propForm = list.value[2]
|
|
129
|
+
if (!is.symbol(propForm)) {
|
|
130
|
+
throw new EvaluationError(
|
|
131
|
+
`. expects a symbol for property name, got: ${propForm.kind}`,
|
|
132
|
+
{ propForm }
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const propName = propForm.name
|
|
137
|
+
const rawObj = rawTarget as Record<string, unknown>
|
|
138
|
+
|
|
139
|
+
if (list.value.length === 3) {
|
|
140
|
+
// Property access — zero extra args.
|
|
141
|
+
// Functions are bound to their object so that ((. obj method)) works correctly.
|
|
142
|
+
const rawProp = rawObj[propName]
|
|
143
|
+
if (typeof rawProp === 'function') {
|
|
144
|
+
return cljJsValue((rawProp as (...a: unknown[]) => unknown).bind(rawObj))
|
|
145
|
+
}
|
|
146
|
+
return jsToClj(rawProp)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Method call — one or more extra args
|
|
150
|
+
const method = rawObj[propName]
|
|
151
|
+
if (typeof method !== 'function') {
|
|
152
|
+
throw new EvaluationError(
|
|
153
|
+
`method '${propName}' is not callable on ${String(rawObj)}`,
|
|
154
|
+
{ propName, rawObj }
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const cljArgs = list.value.slice(3).map((a) => ctx.evaluate(a, env))
|
|
159
|
+
const jsArgs = cljArgs.map((a) => cljToJs(a, ctx, env))
|
|
160
|
+
const rawResult = (method as (...args: unknown[]) => unknown).apply(rawObj, jsArgs)
|
|
161
|
+
return jsToClj(rawResult)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// (js/new ClassName arg1 arg2 ...)
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
export function evaluateNew(
|
|
169
|
+
list: CljList,
|
|
170
|
+
env: Env,
|
|
171
|
+
ctx: EvaluationContext
|
|
172
|
+
): CljValue {
|
|
173
|
+
if (list.value.length < 2) {
|
|
174
|
+
throw new EvaluationError('js/new requires a constructor argument', { list })
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const cls = ctx.evaluate(list.value[1], env)
|
|
178
|
+
if (!is.jsValue(cls) || typeof cls.value !== 'function') {
|
|
179
|
+
throw new EvaluationError(
|
|
180
|
+
`js/new: expected js-value constructor, got ${cls.kind}`,
|
|
181
|
+
{ cls }
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const cljArgs = list.value.slice(2).map((a) => ctx.evaluate(a, env))
|
|
186
|
+
const jsArgs = cljArgs.map((a) => cljToJs(a, ctx, env))
|
|
187
|
+
const ctor = cls.value as new (...args: unknown[]) => unknown
|
|
188
|
+
return cljJsValue(new ctor(...jsArgs))
|
|
189
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { is } from '../assertions'
|
|
2
2
|
import { EvaluationError } from '../errors'
|
|
3
3
|
import { cljList, cljMap, cljVector } from '../factories'
|
|
4
|
+
import { toSeq } from '../transformations'
|
|
4
5
|
import { makeGensym } from '../gensym'
|
|
5
6
|
import {
|
|
6
7
|
type CljValue,
|
|
@@ -19,11 +20,11 @@ export function evaluateQuasiquote(
|
|
|
19
20
|
case valueKeywords.vector:
|
|
20
21
|
case valueKeywords.list: {
|
|
21
22
|
// Handle unquote
|
|
22
|
-
const isAList =
|
|
23
|
+
const isAList = is.list(form)
|
|
23
24
|
if (
|
|
24
25
|
isAList &&
|
|
25
26
|
form.value.length === 2 &&
|
|
26
|
-
|
|
27
|
+
is.symbol(form.value[0]) &&
|
|
27
28
|
form.value[0].name === 'unquote'
|
|
28
29
|
) {
|
|
29
30
|
return ctx.evaluate(form.value[1], env)
|
|
@@ -34,19 +35,24 @@ export function evaluateQuasiquote(
|
|
|
34
35
|
for (const elem of form.value) {
|
|
35
36
|
// Handle unquote splicing
|
|
36
37
|
if (
|
|
37
|
-
|
|
38
|
+
is.list(elem) &&
|
|
38
39
|
elem.value.length === 2 &&
|
|
39
|
-
|
|
40
|
+
is.symbol(elem.value[0]) &&
|
|
40
41
|
elem.value[0].name === 'unquote-splicing'
|
|
41
42
|
) {
|
|
42
43
|
const toSplice = ctx.evaluate(elem.value[1], env)
|
|
43
|
-
if (
|
|
44
|
+
if (is.list(toSplice) || is.vector(toSplice)) {
|
|
45
|
+
elements.push(...toSplice.value)
|
|
46
|
+
} else if (is.lazySeq(toSplice) || is.cons(toSplice)) {
|
|
47
|
+
elements.push(...toSeq(toSplice))
|
|
48
|
+
} else if (is.nil(toSplice)) {
|
|
49
|
+
// nil splices as empty — nothing to push
|
|
50
|
+
} else {
|
|
44
51
|
throw new EvaluationError(
|
|
45
|
-
'Unquote-splicing must evaluate to a
|
|
52
|
+
'Unquote-splicing must evaluate to a seqable',
|
|
46
53
|
{ elem, env }
|
|
47
54
|
)
|
|
48
55
|
}
|
|
49
|
-
elements.push(...toSplice.value)
|
|
50
56
|
continue
|
|
51
57
|
}
|
|
52
58
|
// Otherwise, recursively evaluate the quasiquote
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { is } from '../assertions'
|
|
2
2
|
import { EvaluationError } from '../errors'
|
|
3
3
|
import type { CljValue } from '../types'
|
|
4
4
|
import { specialFormKeywords } from './special-forms'
|
|
@@ -28,9 +28,9 @@ export function assertRecurInTailPosition(body: CljValue[]): void {
|
|
|
28
28
|
|
|
29
29
|
function isRecurForm(form: CljValue): boolean {
|
|
30
30
|
return (
|
|
31
|
-
|
|
31
|
+
is.list(form) &&
|
|
32
32
|
form.value.length >= 1 &&
|
|
33
|
-
|
|
33
|
+
is.symbol(form.value[0]) &&
|
|
34
34
|
form.value[0].name === specialFormKeywords.recur
|
|
35
35
|
)
|
|
36
36
|
}
|
|
@@ -44,7 +44,7 @@ function validateForms(forms: CljValue[], inTail: boolean): void {
|
|
|
44
44
|
|
|
45
45
|
/** Walk a single form; `inTail` = whether a direct recur here is valid. */
|
|
46
46
|
function validateForm(form: CljValue, inTail: boolean): void {
|
|
47
|
-
if (!
|
|
47
|
+
if (!is.list(form)) return
|
|
48
48
|
|
|
49
49
|
if (isRecurForm(form)) {
|
|
50
50
|
if (!inTail) {
|
|
@@ -57,7 +57,7 @@ function validateForm(form: CljValue, inTail: boolean): void {
|
|
|
57
57
|
|
|
58
58
|
const first = form.value[0]
|
|
59
59
|
|
|
60
|
-
if (!
|
|
60
|
+
if (!is.symbol(first)) {
|
|
61
61
|
// Anonymous fn call etc. — all sub-forms are non-tail
|
|
62
62
|
for (const sub of form.value) validateForm(sub, false)
|
|
63
63
|
return
|
|
@@ -92,7 +92,7 @@ function validateForm(form: CljValue, inTail: boolean): void {
|
|
|
92
92
|
// `let`: binding values are non-tail; last body form inherits inTail
|
|
93
93
|
if (name === specialFormKeywords.let) {
|
|
94
94
|
const bindings = form.value[1]
|
|
95
|
-
if (
|
|
95
|
+
if (is.vector(bindings)) {
|
|
96
96
|
for (let i = 1; i < bindings.value.length; i += 2) {
|
|
97
97
|
validateForm(bindings.value[i], false)
|
|
98
98
|
}
|