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,122 @@
|
|
|
1
|
+
// Vector-specific operations: vector, vec, subvec, peek, pop
|
|
2
|
+
//
|
|
3
|
+
// These functions are exclusively concerned with vectors (or lists treated as
|
|
4
|
+
// a stack for peek/pop). Pure vector construction and stack operations.
|
|
5
|
+
|
|
6
|
+
import { is } from '../assertions'
|
|
7
|
+
import { EvaluationError } from '../errors'
|
|
8
|
+
import { v } from '../factories'
|
|
9
|
+
import { printString } from '../printer'
|
|
10
|
+
import { toSeq } from '../transformations'
|
|
11
|
+
import { type CljValue } from '../types'
|
|
12
|
+
|
|
13
|
+
export const vectorFunctions: Record<string, CljValue> = {
|
|
14
|
+
vector: v
|
|
15
|
+
.nativeFn('vector', function vectorImpl(...args: CljValue[]) {
|
|
16
|
+
if (args.length === 0) {
|
|
17
|
+
return v.vector([])
|
|
18
|
+
}
|
|
19
|
+
return v.vector(args)
|
|
20
|
+
})
|
|
21
|
+
.doc('Returns a new vector containing the given values.', [['&', 'args']]),
|
|
22
|
+
|
|
23
|
+
vec: v
|
|
24
|
+
.nativeFn('vec', function vecImpl(coll: CljValue) {
|
|
25
|
+
if (coll === undefined || coll.kind === 'nil') return v.vector([])
|
|
26
|
+
if (is.vector(coll)) return coll
|
|
27
|
+
if (!is.seqable(coll)) {
|
|
28
|
+
throw EvaluationError.atArg(
|
|
29
|
+
`vec expects a collection or string, got ${printString(coll)}`,
|
|
30
|
+
{ coll },
|
|
31
|
+
0
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
return v.vector(toSeq(coll))
|
|
35
|
+
})
|
|
36
|
+
.doc('Creates a new vector containing the contents of coll.', [['coll']]),
|
|
37
|
+
|
|
38
|
+
subvec: v
|
|
39
|
+
.nativeFn(
|
|
40
|
+
'subvec',
|
|
41
|
+
function subvecImpl(vector: CljValue, start: CljValue, end?: CljValue) {
|
|
42
|
+
if (vector === undefined || !is.vector(vector)) {
|
|
43
|
+
throw EvaluationError.atArg(
|
|
44
|
+
`subvec expects a vector, got ${printString(vector)}`,
|
|
45
|
+
{ v: vector },
|
|
46
|
+
0
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
if (start === undefined || start.kind !== 'number') {
|
|
50
|
+
throw EvaluationError.atArg(
|
|
51
|
+
`subvec expects a number start index`,
|
|
52
|
+
{ start },
|
|
53
|
+
1
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
const s = start.value
|
|
57
|
+
const e =
|
|
58
|
+
end !== undefined && end.kind === 'number'
|
|
59
|
+
? end.value
|
|
60
|
+
: vector.value.length
|
|
61
|
+
if (s < 0 || e > vector.value.length || s > e) {
|
|
62
|
+
throw new EvaluationError(
|
|
63
|
+
`subvec index out of bounds: start=${s}, end=${e}, length=${vector.value.length}`,
|
|
64
|
+
{ v: vector, start, end }
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
return v.vector(vector.value.slice(s, e))
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
.doc(
|
|
71
|
+
'Returns a persistent vector of the items in vector from start (inclusive) to end (exclusive).',
|
|
72
|
+
[
|
|
73
|
+
['v', 'start'],
|
|
74
|
+
['v', 'start', 'end'],
|
|
75
|
+
]
|
|
76
|
+
),
|
|
77
|
+
|
|
78
|
+
peek: v
|
|
79
|
+
.nativeFn('peek', function peekImpl(coll: CljValue) {
|
|
80
|
+
if (coll === undefined || coll.kind === 'nil') return v.nil()
|
|
81
|
+
if (is.vector(coll)) {
|
|
82
|
+
return coll.value.length === 0
|
|
83
|
+
? v.nil()
|
|
84
|
+
: coll.value[coll.value.length - 1]
|
|
85
|
+
}
|
|
86
|
+
if (is.list(coll)) {
|
|
87
|
+
return coll.value.length === 0 ? v.nil() : coll.value[0]
|
|
88
|
+
}
|
|
89
|
+
throw EvaluationError.atArg(
|
|
90
|
+
`peek expects a list or vector, got ${printString(coll)}`,
|
|
91
|
+
{ coll },
|
|
92
|
+
0
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
.doc('For a list, same as first. For a vector, same as last.', [['coll']]),
|
|
96
|
+
|
|
97
|
+
pop: v
|
|
98
|
+
.nativeFn('pop', function popImpl(coll: CljValue) {
|
|
99
|
+
if (coll === undefined || coll.kind === 'nil') {
|
|
100
|
+
throw EvaluationError.atArg("Can't pop empty list", { coll }, 0)
|
|
101
|
+
}
|
|
102
|
+
if (is.vector(coll)) {
|
|
103
|
+
if (coll.value.length === 0)
|
|
104
|
+
throw new EvaluationError("Can't pop empty vector", { coll })
|
|
105
|
+
return v.vector(coll.value.slice(0, -1))
|
|
106
|
+
}
|
|
107
|
+
if (is.list(coll)) {
|
|
108
|
+
if (coll.value.length === 0)
|
|
109
|
+
throw new EvaluationError("Can't pop empty list", { coll })
|
|
110
|
+
return v.list(coll.value.slice(1))
|
|
111
|
+
}
|
|
112
|
+
throw EvaluationError.atArg(
|
|
113
|
+
`pop expects a list or vector, got ${printString(coll)}`,
|
|
114
|
+
{ coll },
|
|
115
|
+
0
|
|
116
|
+
)
|
|
117
|
+
})
|
|
118
|
+
.doc(
|
|
119
|
+
'For a list, returns a new list without the first item. For a vector, returns a new vector without the last item.',
|
|
120
|
+
[['coll']]
|
|
121
|
+
),
|
|
122
|
+
}
|
package/src/core/tokenizer.ts
CHANGED
|
@@ -294,8 +294,8 @@ function parseDispatch(ctx: TokenizationContext): Token {
|
|
|
294
294
|
return { kind: tokenKeywords.VarQuote, start, end: scanner.position() }
|
|
295
295
|
}
|
|
296
296
|
if (next === '{') {
|
|
297
|
-
//
|
|
298
|
-
|
|
297
|
+
scanner.advance() // consume '{'
|
|
298
|
+
return { kind: tokenKeywords.SetStart, start, end: scanner.position() }
|
|
299
299
|
}
|
|
300
300
|
throw new TokenizerError(
|
|
301
301
|
`Unknown dispatch character: #${next ?? 'EOF'}`,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { isList, isMap, isVector } from './assertions'
|
|
1
|
+
import { isCons, isLazySeq, isList, isMap, isNil, isSet, isVector } from './assertions'
|
|
2
2
|
import { EvaluationError } from './errors'
|
|
3
3
|
import { cljString, cljVector } from './factories'
|
|
4
|
-
import { printString } from './printer'
|
|
5
|
-
import { type CljValue, valueKeywords } from './types'
|
|
4
|
+
import { printString, getPrintContext } from './printer'
|
|
5
|
+
import { type CljCons, type CljDelay, type CljLazySeq, type CljValue, valueKeywords } from './types'
|
|
6
6
|
|
|
7
7
|
export function valueToString(value: CljValue): string {
|
|
8
8
|
switch (value.kind) {
|
|
@@ -16,12 +16,30 @@ export function valueToString(value: CljValue): string {
|
|
|
16
16
|
return value.name
|
|
17
17
|
case valueKeywords.symbol:
|
|
18
18
|
return value.name
|
|
19
|
-
case valueKeywords.list:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
case valueKeywords.list: {
|
|
20
|
+
const { printLength } = getPrintContext()
|
|
21
|
+
const items = printLength !== null ? value.value.slice(0, printLength) : value.value
|
|
22
|
+
const suffix = printLength !== null && value.value.length > printLength ? ' ...' : ''
|
|
23
|
+
return `(${items.map(valueToString).join(' ')}${suffix})`
|
|
24
|
+
}
|
|
25
|
+
case valueKeywords.vector: {
|
|
26
|
+
const { printLength } = getPrintContext()
|
|
27
|
+
const items = printLength !== null ? value.value.slice(0, printLength) : value.value
|
|
28
|
+
const suffix = printLength !== null && value.value.length > printLength ? ' ...' : ''
|
|
29
|
+
return `[${items.map(valueToString).join(' ')}${suffix}]`
|
|
30
|
+
}
|
|
31
|
+
case valueKeywords.map: {
|
|
32
|
+
const { printLength } = getPrintContext()
|
|
33
|
+
const entries = printLength !== null ? value.entries.slice(0, printLength) : value.entries
|
|
34
|
+
const suffix = printLength !== null && value.entries.length > printLength ? ' ...' : ''
|
|
35
|
+
return `{${entries.map(([key, v]) => `${valueToString(key)} ${valueToString(v)}`).join(' ')}${suffix}}`
|
|
36
|
+
}
|
|
37
|
+
case valueKeywords.set: {
|
|
38
|
+
const { printLength } = getPrintContext()
|
|
39
|
+
const items = printLength !== null ? value.values.slice(0, printLength) : value.values
|
|
40
|
+
const suffix = printLength !== null && value.values.length > printLength ? ' ...' : ''
|
|
41
|
+
return `#{${items.map(valueToString).join(' ')}${suffix}}`
|
|
42
|
+
}
|
|
25
43
|
case valueKeywords.function: {
|
|
26
44
|
if (value.arities.length === 1) {
|
|
27
45
|
const a = value.arities[0]
|
|
@@ -48,6 +66,26 @@ export function valueToString(value: CljValue): string {
|
|
|
48
66
|
const prefix = value.flags ? `(?${value.flags})` : ''
|
|
49
67
|
return `${prefix}${value.pattern}`
|
|
50
68
|
}
|
|
69
|
+
case valueKeywords.delay:
|
|
70
|
+
return value.realized ? `#<Delay @${valueToString(value.value!)}>` : '#<Delay pending>'
|
|
71
|
+
case valueKeywords.lazySeq: {
|
|
72
|
+
const realized = realizeLazySeq(value)
|
|
73
|
+
if (isNil(realized)) return '()'
|
|
74
|
+
return valueToString(realized)
|
|
75
|
+
}
|
|
76
|
+
case valueKeywords.cons: {
|
|
77
|
+
const items = consToArray(value)
|
|
78
|
+
const { printLength } = getPrintContext()
|
|
79
|
+
const visible = printLength !== null ? items.slice(0, printLength) : items
|
|
80
|
+
const suffix = printLength !== null && items.length > printLength ? ' ...' : ''
|
|
81
|
+
return `(${visible.map(valueToString).join(' ')}${suffix})`
|
|
82
|
+
}
|
|
83
|
+
case valueKeywords.namespace:
|
|
84
|
+
return `#namespace[${value.name}]`
|
|
85
|
+
case 'pending':
|
|
86
|
+
if (value.resolved && value.resolvedValue !== undefined)
|
|
87
|
+
return `#<Pending @${valueToString(value.resolvedValue)}>`
|
|
88
|
+
return '#<Pending>'
|
|
51
89
|
default:
|
|
52
90
|
throw new EvaluationError(`unhandled value type: ${value.kind}`, {
|
|
53
91
|
value,
|
|
@@ -55,6 +93,35 @@ export function valueToString(value: CljValue): string {
|
|
|
55
93
|
}
|
|
56
94
|
}
|
|
57
95
|
|
|
96
|
+
/** Realize a delay: evaluate thunk once, cache result. */
|
|
97
|
+
export function realizeDelay(d: CljDelay): CljValue {
|
|
98
|
+
if (d.realized) return d.value!
|
|
99
|
+
d.value = d.thunk()
|
|
100
|
+
d.realized = true
|
|
101
|
+
return d.value!
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Realize a lazy-seq: evaluate thunk once, cache result. Trampolines through chained lazy-seqs. */
|
|
105
|
+
export function realizeLazySeq(ls: CljLazySeq): CljValue {
|
|
106
|
+
let current: CljValue = ls
|
|
107
|
+
while (current.kind === 'lazy-seq') {
|
|
108
|
+
const lazy = current as CljLazySeq
|
|
109
|
+
if (lazy.realized) {
|
|
110
|
+
current = lazy.value!
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
if (lazy.thunk) {
|
|
114
|
+
lazy.value = lazy.thunk()
|
|
115
|
+
lazy.thunk = null
|
|
116
|
+
lazy.realized = true
|
|
117
|
+
current = lazy.value!
|
|
118
|
+
} else {
|
|
119
|
+
return { kind: 'nil', value: null }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return current
|
|
123
|
+
}
|
|
124
|
+
|
|
58
125
|
export const toSeq = (collection: CljValue): CljValue[] => {
|
|
59
126
|
if (isList(collection)) {
|
|
60
127
|
return collection.value
|
|
@@ -65,11 +132,52 @@ export const toSeq = (collection: CljValue): CljValue[] => {
|
|
|
65
132
|
if (isMap(collection)) {
|
|
66
133
|
return collection.entries.map(([k, v]) => cljVector([k, v]))
|
|
67
134
|
}
|
|
135
|
+
if (isSet(collection)) {
|
|
136
|
+
return collection.values
|
|
137
|
+
}
|
|
68
138
|
if (collection.kind === 'string') {
|
|
69
139
|
return [...collection.value].map(cljString)
|
|
70
140
|
}
|
|
141
|
+
if (isLazySeq(collection)) {
|
|
142
|
+
const realized = realizeLazySeq(collection)
|
|
143
|
+
if (isNil(realized)) return []
|
|
144
|
+
return toSeq(realized)
|
|
145
|
+
}
|
|
146
|
+
if (isCons(collection)) {
|
|
147
|
+
return consToArray(collection)
|
|
148
|
+
}
|
|
71
149
|
throw new EvaluationError(
|
|
72
150
|
`toSeq expects a collection or string, got ${printString(collection)}`,
|
|
73
151
|
{ collection }
|
|
74
152
|
)
|
|
75
153
|
}
|
|
154
|
+
|
|
155
|
+
/** Walk a cons/lazy-seq chain into a flat array (trampoline, no recursion). */
|
|
156
|
+
export function consToArray(c: CljCons): CljValue[] {
|
|
157
|
+
const result: CljValue[] = [c.head]
|
|
158
|
+
let tail: CljValue = c.tail
|
|
159
|
+
while (true) {
|
|
160
|
+
if (isNil(tail)) break
|
|
161
|
+
if (isCons(tail)) {
|
|
162
|
+
result.push(tail.head)
|
|
163
|
+
tail = tail.tail
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
if (isLazySeq(tail)) {
|
|
167
|
+
tail = realizeLazySeq(tail)
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
if (isList(tail)) {
|
|
171
|
+
result.push(...tail.value)
|
|
172
|
+
break
|
|
173
|
+
}
|
|
174
|
+
if (isVector(tail)) {
|
|
175
|
+
result.push(...tail.value)
|
|
176
|
+
break
|
|
177
|
+
}
|
|
178
|
+
// Other seqable types — fall through to toSeq
|
|
179
|
+
result.push(...toSeq(tail))
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
return result
|
|
183
|
+
}
|
package/src/core/types.ts
CHANGED
|
@@ -17,6 +17,12 @@ export const valueKeywords = {
|
|
|
17
17
|
volatile: 'volatile',
|
|
18
18
|
regex: 'regex',
|
|
19
19
|
var: 'var',
|
|
20
|
+
set: 'set',
|
|
21
|
+
delay: 'delay',
|
|
22
|
+
lazySeq: 'lazy-seq',
|
|
23
|
+
cons: 'cons',
|
|
24
|
+
namespace: 'namespace',
|
|
25
|
+
jsValue: 'js-value',
|
|
20
26
|
} as const
|
|
21
27
|
export type ValueKeywords = (typeof valueKeywords)[keyof typeof valueKeywords]
|
|
22
28
|
|
|
@@ -30,6 +36,7 @@ export type CljList = { kind: 'list'; value: CljValue[]; meta?: CljMap }
|
|
|
30
36
|
export type CljVector = { kind: 'vector'; value: CljValue[]; meta?: CljMap }
|
|
31
37
|
export type CljMap = { kind: 'map'; entries: [CljValue, CljValue][]; meta?: CljMap }
|
|
32
38
|
export type CljNamespace = {
|
|
39
|
+
kind: 'namespace'
|
|
33
40
|
name: string
|
|
34
41
|
vars: Map<string, CljVar> // user defs from (def ...)
|
|
35
42
|
aliases: Map<string, CljNamespace> // :as namespace aliases
|
|
@@ -40,7 +47,6 @@ export type Env = {
|
|
|
40
47
|
bindings: Map<string, CljValue> // native fns, macros, multimethods, local values
|
|
41
48
|
outer: Env | null
|
|
42
49
|
ns?: CljNamespace // set on namespace-root envs only
|
|
43
|
-
resolveNs?: (name: string) => Env | null // set on coreEnv only
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
export type DestructurePattern = CljSymbol | CljVector | CljMap
|
|
@@ -55,6 +61,7 @@ export type CljFunction = {
|
|
|
55
61
|
kind: 'function'
|
|
56
62
|
arities: Arity[]
|
|
57
63
|
env: Env
|
|
64
|
+
name?: string // set for named fn: (fn my-name [x] x)
|
|
58
65
|
meta?: CljMap
|
|
59
66
|
}
|
|
60
67
|
|
|
@@ -62,13 +69,43 @@ export type CljMacro = {
|
|
|
62
69
|
kind: 'macro'
|
|
63
70
|
arities: Arity[]
|
|
64
71
|
env: Env
|
|
72
|
+
name?: string // set for named defmacro
|
|
65
73
|
}
|
|
66
74
|
|
|
67
|
-
export type CljAtom = {
|
|
75
|
+
export type CljAtom = {
|
|
76
|
+
kind: 'atom'
|
|
77
|
+
value: CljValue
|
|
78
|
+
meta?: CljMap
|
|
79
|
+
watches?: Map<string, { key: CljValue; fn: CljValue; ctx: EvaluationContext; callEnv: Env }>
|
|
80
|
+
validator?: CljValue
|
|
81
|
+
}
|
|
68
82
|
export type CljReduced = { kind: 'reduced'; value: CljValue }
|
|
69
83
|
export type CljVolatile = { kind: 'volatile'; value: CljValue }
|
|
70
84
|
export type CljRegex = { kind: 'regex'; pattern: string; flags: string }
|
|
71
85
|
|
|
86
|
+
export type CljSet = { kind: 'set'; values: CljValue[] }
|
|
87
|
+
|
|
88
|
+
export type CljDelay = {
|
|
89
|
+
kind: 'delay'
|
|
90
|
+
thunk: () => CljValue
|
|
91
|
+
realized: boolean
|
|
92
|
+
value?: CljValue
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type CljLazySeq = {
|
|
96
|
+
kind: 'lazy-seq'
|
|
97
|
+
thunk: (() => CljValue) | null
|
|
98
|
+
realized: boolean
|
|
99
|
+
value?: CljValue // nil, list, cons, or another lazy-seq after realization
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type CljCons = {
|
|
103
|
+
kind: 'cons'
|
|
104
|
+
head: CljValue
|
|
105
|
+
tail: CljValue // can be list, vector, lazy-seq, cons, or nil
|
|
106
|
+
meta?: CljMap
|
|
107
|
+
}
|
|
108
|
+
|
|
72
109
|
export type CljVar = {
|
|
73
110
|
kind: 'var'
|
|
74
111
|
ns: string
|
|
@@ -87,6 +124,17 @@ export type CljMultiMethod = {
|
|
|
87
124
|
defaultMethod?: CljFunction | CljNativeFunction
|
|
88
125
|
}
|
|
89
126
|
|
|
127
|
+
/**
|
|
128
|
+
* IO channels for a session. stdout is the primary output channel (println,
|
|
129
|
+
* print, pr, prn, pprint, newline). stderr is available for error output.
|
|
130
|
+
* Both are set by the session on context creation and read at call time by
|
|
131
|
+
* IO native functions — no closure capture, no reinstallation on restore.
|
|
132
|
+
*/
|
|
133
|
+
export type IOContext = {
|
|
134
|
+
stdout: (text: string) => void
|
|
135
|
+
stderr: (text: string) => void
|
|
136
|
+
}
|
|
137
|
+
|
|
90
138
|
export type EvaluationContext = {
|
|
91
139
|
evaluate: (expr: CljValue, env: Env) => CljValue
|
|
92
140
|
evaluateForms: (forms: CljValue[], env: Env) => CljValue
|
|
@@ -99,6 +147,19 @@ export type EvaluationContext = {
|
|
|
99
147
|
applyCallable: (fn: CljValue, args: CljValue[], callEnv: Env) => CljValue
|
|
100
148
|
applyMacro: (macro: CljMacro, rawArgs: CljValue[]) => CljValue
|
|
101
149
|
expandAll: (form: CljValue, env: Env) => CljValue
|
|
150
|
+
/**
|
|
151
|
+
* Resolves a namespace name (or alias) to its CljNamespace record.
|
|
152
|
+
* Wired by the session/runtime after context creation; defaults to no-op null.
|
|
153
|
+
*/
|
|
154
|
+
resolveNs: (name: string) => CljNamespace | null
|
|
155
|
+
/**
|
|
156
|
+
* IO channels — set by the session in buildSessionFacade.
|
|
157
|
+
* IO native functions (println, print, pr, prn, pprint, newline) read
|
|
158
|
+
* ctx.io.stdout at call time instead of closing over an emit callback.
|
|
159
|
+
* This means snapshot clones automatically use the correct output without
|
|
160
|
+
* any reinstallation of IO vars.
|
|
161
|
+
*/
|
|
162
|
+
io: IOContext
|
|
102
163
|
/**
|
|
103
164
|
* Mutable per-call fields set by session.evaluate / loadFile before
|
|
104
165
|
* executing forms. Used by evaluateDef to stamp :line/:column/:file onto
|
|
@@ -108,6 +169,17 @@ export type EvaluationContext = {
|
|
|
108
169
|
currentFile?: string
|
|
109
170
|
currentLineOffset?: number
|
|
110
171
|
currentColOffset?: number
|
|
172
|
+
/**
|
|
173
|
+
* Optional module loader for string `:require` specs.
|
|
174
|
+
* Called by processNsRequiresAsync when it encounters ["specifier" :as Alias].
|
|
175
|
+
* Wired from SessionOptions.importModule in buildSessionFacade.
|
|
176
|
+
*/
|
|
177
|
+
importModule?: (specifier: string) => unknown | Promise<unknown>
|
|
178
|
+
/**
|
|
179
|
+
* Switches the session's current namespace. Wired by buildSessionFacade.
|
|
180
|
+
* Called by `in-ns` at runtime. Without this hook, `in-ns` is a no-op.
|
|
181
|
+
*/
|
|
182
|
+
setCurrentNs?: (name: string) => void
|
|
111
183
|
}
|
|
112
184
|
|
|
113
185
|
export type CljNativeFunction = {
|
|
@@ -123,6 +195,18 @@ export type CljNativeFunction = {
|
|
|
123
195
|
meta?: CljMap
|
|
124
196
|
}
|
|
125
197
|
|
|
198
|
+
export type CljJsValue = { kind: 'js-value'; value: unknown }
|
|
199
|
+
|
|
200
|
+
// --- ASYNC (experimental, see evaluator/async-evaluator.ts) ---
|
|
201
|
+
export type CljPending = {
|
|
202
|
+
kind: 'pending'
|
|
203
|
+
promise: Promise<CljValue>
|
|
204
|
+
/** Set to true once the promise settles (fulfilled only). */
|
|
205
|
+
resolved?: boolean
|
|
206
|
+
resolvedValue?: CljValue
|
|
207
|
+
}
|
|
208
|
+
// --- END ASYNC ---
|
|
209
|
+
|
|
126
210
|
export type CljValue =
|
|
127
211
|
| CljNumber
|
|
128
212
|
| CljString
|
|
@@ -142,6 +226,13 @@ export type CljValue =
|
|
|
142
226
|
| CljVolatile
|
|
143
227
|
| CljRegex
|
|
144
228
|
| CljVar
|
|
229
|
+
| CljSet
|
|
230
|
+
| CljDelay
|
|
231
|
+
| CljLazySeq
|
|
232
|
+
| CljCons
|
|
233
|
+
| CljNamespace
|
|
234
|
+
| CljPending
|
|
235
|
+
| CljJsValue
|
|
145
236
|
|
|
146
237
|
/** Tokens */
|
|
147
238
|
export const tokenKeywords = {
|
|
@@ -166,6 +257,7 @@ export const tokenKeywords = {
|
|
|
166
257
|
Regex: 'Regex',
|
|
167
258
|
VarQuote: 'VarQuote',
|
|
168
259
|
Meta: 'Meta',
|
|
260
|
+
SetStart: 'SetStart',
|
|
169
261
|
} as const
|
|
170
262
|
export const tokenSymbols = {
|
|
171
263
|
Quote: 'quote',
|
|
@@ -269,6 +361,9 @@ export type TokenVarQuote = {
|
|
|
269
361
|
export type TokenMeta = {
|
|
270
362
|
kind: 'Meta'
|
|
271
363
|
}
|
|
364
|
+
export type TokenSetStart = {
|
|
365
|
+
kind: 'SetStart'
|
|
366
|
+
}
|
|
272
367
|
export type Token = (
|
|
273
368
|
| TokenLParen
|
|
274
369
|
| TokenRParen
|
|
@@ -291,4 +386,5 @@ export type Token = (
|
|
|
291
386
|
| TokenRegex
|
|
292
387
|
| TokenVarQuote
|
|
293
388
|
| TokenMeta
|
|
389
|
+
| TokenSetStart
|
|
294
390
|
) & { start: Cursor; end: Cursor }
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import {
|
|
4
|
+
cljNativeFunction,
|
|
5
|
+
cljNil,
|
|
6
|
+
cljString,
|
|
7
|
+
valueToString,
|
|
8
|
+
type Session,
|
|
9
|
+
type CljValue,
|
|
10
|
+
} from '../core'
|
|
11
|
+
import type { RuntimeModule } from '../core/module'
|
|
12
|
+
import { inferSourceRoot } from '../bin/nrepl-utils'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns a RuntimeModule that installs Node.js host functions into clojure.core.
|
|
16
|
+
* Closes over the session for functions (like `load`) that need to drive evaluation.
|
|
17
|
+
*/
|
|
18
|
+
export function makeNodeHostModule(session: Session): RuntimeModule {
|
|
19
|
+
return {
|
|
20
|
+
id: 'conjure/host-node',
|
|
21
|
+
dependsOn: ['clojure.core'],
|
|
22
|
+
declareNs: [
|
|
23
|
+
{
|
|
24
|
+
name: 'clojure.core',
|
|
25
|
+
vars(_ctx) {
|
|
26
|
+
return new Map([
|
|
27
|
+
[
|
|
28
|
+
'slurp',
|
|
29
|
+
{
|
|
30
|
+
value: cljNativeFunction('slurp', (pathVal: CljValue) => {
|
|
31
|
+
const filePath = resolve(valueToString(pathVal))
|
|
32
|
+
if (!existsSync(filePath)) {
|
|
33
|
+
throw new Error(`slurp: file not found: ${filePath}`)
|
|
34
|
+
}
|
|
35
|
+
return cljString(readFileSync(filePath, 'utf8'))
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
[
|
|
40
|
+
'spit',
|
|
41
|
+
{
|
|
42
|
+
value: cljNativeFunction(
|
|
43
|
+
'spit',
|
|
44
|
+
(pathVal: CljValue, content: CljValue) => {
|
|
45
|
+
const filePath = resolve(valueToString(pathVal))
|
|
46
|
+
writeFileSync(filePath, valueToString(content), 'utf8')
|
|
47
|
+
return cljNil()
|
|
48
|
+
}
|
|
49
|
+
),
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
[
|
|
53
|
+
'load',
|
|
54
|
+
{
|
|
55
|
+
value: cljNativeFunction('load', (pathVal: CljValue) => {
|
|
56
|
+
const filePath = resolve(valueToString(pathVal))
|
|
57
|
+
if (!existsSync(filePath)) {
|
|
58
|
+
throw new Error(`load: file not found: ${filePath}`)
|
|
59
|
+
}
|
|
60
|
+
const source = readFileSync(filePath, 'utf8')
|
|
61
|
+
const inferred = inferSourceRoot(filePath, source)
|
|
62
|
+
if (inferred) session.addSourceRoot(inferred)
|
|
63
|
+
const loadedNs = session.loadFile(source)
|
|
64
|
+
session.setNs(loadedNs)
|
|
65
|
+
return cljNil()
|
|
66
|
+
}),
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
])
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -5,6 +5,7 @@ import { BDecoderStream, BEncoderStream } from '../bin/bencode'
|
|
|
5
5
|
import type { Session } from '../core'
|
|
6
6
|
import type { WebSocketServer } from 'vite'
|
|
7
7
|
import { VERSION } from '../bin/version'
|
|
8
|
+
import { resolveSymbol, extractMeta } from '../bin/nrepl-symbol'
|
|
8
9
|
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
10
11
|
// Types
|
|
@@ -23,6 +24,7 @@ type BrowserResult = {
|
|
|
23
24
|
value?: string
|
|
24
25
|
error?: string
|
|
25
26
|
ns?: string
|
|
27
|
+
out?: string
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
type PendingResolver = (result: BrowserResult) => void
|
|
@@ -158,6 +160,72 @@ function handleClose(
|
|
|
158
160
|
send(encoder, { id, session: sessionId, status: ['done'] })
|
|
159
161
|
}
|
|
160
162
|
|
|
163
|
+
function handleInfo(
|
|
164
|
+
msg: NreplMessage,
|
|
165
|
+
session: RelaySession,
|
|
166
|
+
encoder: BEncoderStream,
|
|
167
|
+
serverSession: Session
|
|
168
|
+
) {
|
|
169
|
+
const id = (msg['id'] as string) ?? ''
|
|
170
|
+
const sym = msg['sym'] as string | undefined
|
|
171
|
+
const nsOverride = msg['ns'] as string | undefined
|
|
172
|
+
|
|
173
|
+
if (!sym) {
|
|
174
|
+
done(encoder, id, session.id, { status: ['no-info', 'done'] })
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const resolved = resolveSymbol(sym, serverSession, nsOverride ?? session.currentNs)
|
|
179
|
+
if (!resolved) {
|
|
180
|
+
done(encoder, id, session.id, { status: ['no-info', 'done'] })
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const meta = extractMeta(resolved.value, resolved.varObj?.meta)
|
|
185
|
+
done(encoder, id, session.id, {
|
|
186
|
+
ns: resolved.resolvedNs,
|
|
187
|
+
name: resolved.localName,
|
|
188
|
+
doc: meta.doc,
|
|
189
|
+
'arglists-str': meta.arglistsStr,
|
|
190
|
+
type: meta.type,
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function handleEldoc(
|
|
195
|
+
msg: NreplMessage,
|
|
196
|
+
session: RelaySession,
|
|
197
|
+
encoder: BEncoderStream,
|
|
198
|
+
serverSession: Session
|
|
199
|
+
) {
|
|
200
|
+
const id = (msg['id'] as string) ?? ''
|
|
201
|
+
const sym = msg['sym'] as string | undefined
|
|
202
|
+
const nsOverride = msg['ns'] as string | undefined
|
|
203
|
+
|
|
204
|
+
if (!sym) {
|
|
205
|
+
done(encoder, id, session.id, { status: ['no-eldoc', 'done'] })
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const resolved = resolveSymbol(sym, serverSession, nsOverride ?? session.currentNs)
|
|
210
|
+
if (!resolved) {
|
|
211
|
+
done(encoder, id, session.id, { status: ['no-eldoc', 'done'] })
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const meta = extractMeta(resolved.value, resolved.varObj?.meta)
|
|
216
|
+
if (!meta.eldocArgs) {
|
|
217
|
+
done(encoder, id, session.id, { status: ['no-eldoc', 'done'] })
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
done(encoder, id, session.id, {
|
|
222
|
+
name: resolved.localName,
|
|
223
|
+
ns: resolved.resolvedNs,
|
|
224
|
+
type: meta.type,
|
|
225
|
+
eldoc: meta.eldocArgs,
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
161
229
|
function handleUnknown(msg: NreplMessage, encoder: BEncoderStream) {
|
|
162
230
|
const id = (msg['id'] as string) ?? ''
|
|
163
231
|
send(encoder, { id, status: ['unknown-op', 'done'] })
|
|
@@ -181,6 +249,7 @@ async function handleEval(
|
|
|
181
249
|
)
|
|
182
250
|
|
|
183
251
|
if (result.ns) session.currentNs = result.ns
|
|
252
|
+
if (result.out) send(encoder, { id, session: session.id, out: result.out })
|
|
184
253
|
|
|
185
254
|
if (result.error) {
|
|
186
255
|
done(encoder, id, session.id, {
|
|
@@ -215,6 +284,7 @@ async function handleLoadFile(
|
|
|
215
284
|
)
|
|
216
285
|
|
|
217
286
|
if (result.ns) session.currentNs = result.ns
|
|
287
|
+
if (result.out) send(encoder, { id, session: session.id, out: result.out })
|
|
218
288
|
|
|
219
289
|
if (result.error) {
|
|
220
290
|
done(encoder, id, session.id, {
|
|
@@ -264,21 +334,12 @@ async function handleMessage(
|
|
|
264
334
|
case 'close':
|
|
265
335
|
handleClose(msg, sessions, encoder)
|
|
266
336
|
break
|
|
267
|
-
// info/lookup/eldoc: return no-info — static analysis not critical for browser REPL
|
|
268
337
|
case 'info':
|
|
269
338
|
case 'lookup':
|
|
270
|
-
|
|
271
|
-
id: (msg['id'] as string) ?? '',
|
|
272
|
-
session: session.id,
|
|
273
|
-
status: ['no-info', 'done'],
|
|
274
|
-
})
|
|
339
|
+
handleInfo(msg, session, encoder, serverSession)
|
|
275
340
|
break
|
|
276
341
|
case 'eldoc':
|
|
277
|
-
|
|
278
|
-
id: (msg['id'] as string) ?? '',
|
|
279
|
-
session: session.id,
|
|
280
|
-
status: ['no-eldoc', 'done'],
|
|
281
|
-
})
|
|
342
|
+
handleEldoc(msg, session, encoder, serverSession)
|
|
282
343
|
break
|
|
283
344
|
default:
|
|
284
345
|
handleUnknown(msg, encoder)
|