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.
Files changed (77) hide show
  1. package/dist-cli/conjure-js.mjs +9360 -5298
  2. package/dist-vite-plugin/index.mjs +9463 -5185
  3. package/package.json +3 -1
  4. package/src/bin/cli.ts +2 -2
  5. package/src/bin/nrepl-symbol.ts +150 -0
  6. package/src/bin/nrepl.ts +289 -167
  7. package/src/bin/version.ts +1 -1
  8. package/src/clojure/core.clj +757 -29
  9. package/src/clojure/core.clj.d.ts +75 -131
  10. package/src/clojure/generated/builtin-namespace-registry.ts +4 -0
  11. package/src/clojure/generated/clojure-core-source.ts +758 -29
  12. package/src/clojure/generated/clojure-set-source.ts +136 -0
  13. package/src/clojure/generated/clojure-walk-source.ts +72 -0
  14. package/src/clojure/set.clj +132 -0
  15. package/src/clojure/set.clj.d.ts +20 -0
  16. package/src/clojure/string.clj.d.ts +14 -0
  17. package/src/clojure/walk.clj +68 -0
  18. package/src/clojure/walk.clj.d.ts +7 -0
  19. package/src/core/assertions.ts +114 -6
  20. package/src/core/bootstrap.ts +337 -0
  21. package/src/core/conversions.ts +48 -31
  22. package/src/core/core-module.ts +303 -0
  23. package/src/core/env.ts +20 -6
  24. package/src/core/evaluator/apply.ts +40 -25
  25. package/src/core/evaluator/arity.ts +8 -8
  26. package/src/core/evaluator/async-evaluator.ts +565 -0
  27. package/src/core/evaluator/collections.ts +28 -5
  28. package/src/core/evaluator/destructure.ts +180 -69
  29. package/src/core/evaluator/dispatch.ts +12 -14
  30. package/src/core/evaluator/evaluate.ts +22 -20
  31. package/src/core/evaluator/expand.ts +45 -15
  32. package/src/core/evaluator/form-parsers.ts +178 -0
  33. package/src/core/evaluator/index.ts +7 -9
  34. package/src/core/evaluator/js-interop.ts +189 -0
  35. package/src/core/evaluator/quasiquote.ts +14 -8
  36. package/src/core/evaluator/recur-check.ts +6 -6
  37. package/src/core/evaluator/special-forms.ts +234 -191
  38. package/src/core/factories.ts +182 -3
  39. package/src/core/index.ts +54 -4
  40. package/src/core/module.ts +136 -0
  41. package/src/core/ns-forms.ts +107 -0
  42. package/src/core/printer.ts +371 -11
  43. package/src/core/reader.ts +84 -33
  44. package/src/core/registry.ts +209 -0
  45. package/src/core/runtime.ts +376 -0
  46. package/src/core/session.ts +253 -487
  47. package/src/core/stdlib/arithmetic.ts +528 -194
  48. package/src/core/stdlib/async-fns.ts +132 -0
  49. package/src/core/stdlib/atoms.ts +291 -56
  50. package/src/core/stdlib/errors.ts +54 -50
  51. package/src/core/stdlib/hof.ts +82 -166
  52. package/src/core/stdlib/js-namespace.ts +344 -0
  53. package/src/core/stdlib/lazy.ts +34 -0
  54. package/src/core/stdlib/maps-sets.ts +322 -0
  55. package/src/core/stdlib/meta.ts +61 -30
  56. package/src/core/stdlib/predicates.ts +325 -187
  57. package/src/core/stdlib/regex.ts +126 -98
  58. package/src/core/stdlib/seq.ts +564 -0
  59. package/src/core/stdlib/strings.ts +164 -135
  60. package/src/core/stdlib/transducers.ts +95 -100
  61. package/src/core/stdlib/utils.ts +292 -130
  62. package/src/core/stdlib/vars.ts +27 -27
  63. package/src/core/stdlib/vectors.ts +122 -0
  64. package/src/core/tokenizer.ts +2 -2
  65. package/src/core/transformations.ts +117 -9
  66. package/src/core/types.ts +98 -2
  67. package/src/host/node-host-module.ts +74 -0
  68. package/src/{vite-plugin-clj/nrepl-relay.ts → nrepl/relay.ts} +72 -11
  69. package/src/vite-plugin-clj/codegen.ts +87 -95
  70. package/src/vite-plugin-clj/index.ts +178 -23
  71. package/src/vite-plugin-clj/namespace-utils.ts +39 -0
  72. package/src/vite-plugin-clj/static-analysis.ts +211 -0
  73. package/src/clojure/demo.clj +0 -72
  74. package/src/clojure/demo.clj.d.ts +0 -0
  75. package/src/core/core-env.ts +0 -61
  76. package/src/core/stdlib/collections.ts +0 -739
  77. 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
+ }
@@ -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
- // TODO: set literals — #{1 2 3}
298
- throw new TokenizerError('Set literals are not yet supported', start)
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
- return `(${value.value.map(valueToString).join(' ')})`
21
- case valueKeywords.vector:
22
- return `[${value.value.map(valueToString).join(' ')}]`
23
- case valueKeywords.map:
24
- return `{${value.entries.map(([key, value]) => `${valueToString(key)} ${valueToString(value)}`).join(' ')}}`
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 = { kind: 'atom'; value: CljValue; meta?: CljMap }
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
- send(encoder, {
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
- send(encoder, {
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)