diff --git a/.gitignore b/.gitignore index 473c22d..10d3ce2 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,8 @@ calva-example/ # Build output conjure +conjure-js +packages/conjure-js/conjure-js # nREPL port file (written at server start, deleted on exit) .nrepl-port diff --git a/docs/core-language.md b/docs/core-language.md new file mode 100644 index 0000000..7fa0e65 --- /dev/null +++ b/docs/core-language.md @@ -0,0 +1,381 @@ +# Conjure Core Language + +> This document defines the **minimal post-expansion language** — the set of forms that survive macro expansion and reach the evaluator. This is the **compilation target**: what a compiler must handle to compile all valid Conjure programs. +> +> Everything above this level is syntax sugar, macros, or reader transforms that reduce to these forms before evaluation begins. + +--- + +## Overview + +The Conjure evaluator pipeline: + +``` +Source (.clj) + → Tokenizer + → Reader ← quasiquote handled here (reader transform) + → Expander ← all macros expand here; quasiquote completes here + → Core Language ← the forms defined in this document + → Evaluator / Compiler + → CljValue +``` + +After the expander runs, only the forms listed in this document remain. Macros are gone. `defmacro`, `when`, `->`, `cond`, `letfn`, `defmulti`, `defmethod`, `delay` — all expanded to core forms. The evaluator and any future compiler only need to handle what is listed below. + +--- + +## Tier 1 — True Primitives + +These forms cannot be implemented as macros. The evaluator and compiler handle them explicitly. + +--- + +### Literals + +Self-evaluating values. A literal compiles to a constant closure: `() => value`. + +| Form | Examples | +|------|---------| +| Nil | `nil` | +| Boolean | `true`, `false` | +| Integer | `42`, `-7` | +| Float | `3.14`, `-0.5` | +| String | `"hello"` | +| Keyword | `:foo`, `::bar`, `:ns/kw` | +| Character | `\a`, `\newline`, `\space` | +| Regex | `#"pattern"` | + +--- + +### Collection Literals + +``` +[expr*] — vector literal +{expr*} — map literal (must have even number of forms) +#{expr*} — set literal +``` + +Each element is evaluated left-to-right. Compiles to: evaluate all elements, construct the collection. + +--- + +### Symbol + +``` +sym +``` + +Look up `sym` in the lexical env chain; deref the var. Compiles to: `(env) => lookup("sym", env)`. With slot indexing: `(env) => env[slot]`. + +--- + +### `quote` + +```clojure +(quote x) +``` + +Returns `x` unevaluated. `x` is any value — symbol, list, map, etc. Compiles to a constant. + +--- + +### `if` + +```clojure +(if test then) +(if test then else) +``` + +Evaluates `test`. If truthy (not `nil` and not `false`), evaluates and returns `then`. Otherwise evaluates and returns `else` (or `nil` if absent). **Short-circuits** — only one branch is evaluated. + +Compiles to: `(env) => isTruthy(test(env)) ? then(env) : else(env)`. + +--- + +### `do` + +```clojure +(do expr*) +``` + +Evaluates each expression in order. Returns the value of the last expression. Returns `nil` if no expressions. + +Compiles to: evaluate all in sequence, return last. + +--- + +### `let*` + +```clojure +(let* [sym1 val1 sym2 val2 ...] body*) +``` + +Introduces lexical bindings. Each `sym` is bound to its `val` in a new scope frame. Bindings are sequential — `sym2` can reference `sym1`. Body expressions are evaluated in sequence; the last is returned. + +**Slot indexing opportunity:** At compile time, the slot position of each `sym` is known. The compiler assigns integer slots instead of name-based env lookup. + +Compiles to: +```javascript +(env) => { + const env1 = extend(env, slot0, val1(env)) + const env2 = extend(env1, slot1, val2(env1)) + return body(env2) +} +``` + +--- + +### `fn*` + +```clojure +(fn* name? [params] body*) +(fn* name? ([params] body*) ([params] body*) ...) +``` + +Creates a closure. Captures the lexical environment at creation time. Supports multi-arity, rest args (`& rest`), and destructuring in params. + +The optional `name` is for self-reference (recursive anonymous functions) — it is bound in the function's own body scope. + +Compiles to: `(env) => makeClosure(compiledBody, env)`. Params become slot-indexed entries in a new env frame. + +--- + +### `letfn*` + +```clojure +(letfn* [sym1 fn1 sym2 fn2 ...] body*) +``` + +Like `let*` but all bindings are in scope for all functions simultaneously. This is the mutual recursion primitive. `sym1` can call `sym2` and vice versa. + +Used by the `letfn` macro (defined in `clojure/core.clj`): +```clojure +(defmacro letfn [fnspecs & body] + `(letfn* ~(vec (interleave (map first fnspecs) + (map #(cons 'fn %) fnspecs))) + ~@body)) +``` + +--- + +### `loop*` and `recur` + +```clojure +(loop* [sym1 init1 sym2 init2 ...] body*) +(recur new-val1 new-val2 ...) +``` + +`loop*` establishes a recursion point with initial bindings. `recur` jumps back to the nearest `loop*` (or `fn*` for self-tail-calls), rebinding the loop variables to new values. `recur` must be in tail position. + +Compiles to a `while` loop — no stack growth: +```javascript +(env) => { + let [s0, s1] = [init1(env), init2(env)] + while (true) { + const result = body(envWithSlots(s0, s1)) + if (result instanceof RecurSignal) { [s0, s1] = result.values; continue } + return result + } +} +``` + +--- + +### `def` + +```clojure +(def sym) +(def sym val) +(def sym docstring val) +``` + +Interns a var in the current namespace. If the var already exists, updates its root binding. Returns the `CljVar`. + +--- + +### `set!` + +```clojure +(set! sym val) +``` + +Mutates the root binding of a var. Used for dynamic var binding stack mutation. In normal code, prefer `binding`. + +--- + +### `var` + +```clojure +(var sym) +``` + +Returns the `CljVar` object for `sym` without dereferencing it. Reader sugar: `#'sym`. + +--- + +### `binding` + +```clojure +(binding [^:dynamic sym1 val1 ^:dynamic sym2 val2 ...] body*) +``` + +Establishes dynamic var bindings for the duration of `body`. Thread-local in JVM Clojure; call-stack-scoped in Conjure. All vars must be declared `^:dynamic`. + +Used for `*out*`, `*err*`, `*print-length*`, and user-defined dynamic vars. + +--- + +### `throw` + +```clojure +(throw expr) +``` + +Evaluates `expr` and throws the result as an exception. Can throw any `CljValue`. + +--- + +### `try` + +```clojure +(try + body* + (catch ExType sym body*) + (finally body*)) +``` + +Executes `body`. On exception, finds a matching `catch` clause and executes its body with `sym` bound to the thrown value. `finally` body always runs (return value discarded). `catch` and `finally` are both optional. + +`ExType` may be `js/Error` or any string/keyword that the runtime uses to match exception types. + +--- + +### `.` (JS member access / method call) + +```clojure +(. obj field) — property access: obj.field +(. obj (method args*)) — method call: obj.method(args...) +``` + +The unified JS interop form. `obj` is evaluated; must hold a `CljJsValue`. Returns a `CljJsValue`. + +Reader sugar: `(.method obj args*)` and `(.-field obj)`. + +--- + +### `js/new` + +```clojure +(js/new Constructor arg*) +``` + +Calls a JS constructor. Equivalent to `new Constructor(args...)`. Returns a `CljJsValue`. + +--- + +### `async` + +```clojure +(async body*) +``` + +Creates an async evaluation boundary. Evaluates `body` in an async context where `@` can be used to await `CljPending` values. Returns a `CljPending`. + +The `async` form is Conjure-specific. It is **not** equivalent to a JS `async` function — it is an explicit opt-in to async evaluation. The sync evaluator stays sync; `async` is the boundary. + +--- + +### `lazy-seq` + +```clojure +(lazy-seq body) +``` + +Creates a lazy sequence. `body` is not evaluated until the sequence is realized. Body must return `nil` (empty) or a `CljSeq`. + +Used by the `lazy-seq` macro (in `clojure/core.clj`) which wraps body in a thunk automatically. The special form receives the pre-wrapped thunk. + +--- + +## Tier 2 — Bootstrap-Only Forms + +These forms appear during macro loading but should not appear in user programs after expansion. + +### `defmacro` + +```clojure +(defmacro name [params] body*) +``` + +Defines a macro. Sugar for `def` + `fn*` + `:macro true` metadata. Only meaningful during bootstrap. After bootstrap, macros are ordinary `CljFunction` values with `:macro true` metadata in their var. + +### `ns` + +```clojure +(ns name (:require ...) (:import ...) ...) +``` + +Declares and activates a namespace. Complex declaration form that sets the current namespace, processes requires and imports. Only meaningful at top-level. + +--- + +## What Macros Expand To + +For reference — the major macros and their expansion targets: + +| Macro | Expands to core forms | +|-------|-----------------------| +| `let` | `let*` (after destructuring transform) | +| `letfn` | `letfn*` | +| `fn` | `fn*` (after destructuring transform) | +| `defn` | `def` + `fn*` | +| `when` | `if` + `do` | +| `when-not` | `if` + `do` | +| `and` | nested `if` | +| `or` | nested `if` + `let*` | +| `cond` | nested `if` | +| `->`, `->>` | nested function calls | +| `for` | `lazy-seq` + `let*` + `if` + `recur` | +| `doseq` | `loop*` + `recur` + `if` | +| `delay` | `make-delay` + `fn*` | +| `defmulti` | `def` + `make-multimethod` (with re-eval guard) | +| `defmethod` | `add-method!` | +| `with-out-str` | `binding` + `*out*` + `StringBuilder` | + +--- + +## Forms Handled Before Evaluation + +### Quasiquote + +```clojure +`(foo ~x ~@ys) +``` + +Handled entirely by the **reader and expander**. Never reaches the evaluator. + +Expands to: +```clojure +(seq (concat (list 'foo) (list x) ys)) +``` + +The `quasiquote` symbol does not appear in the core language. It is a reader/expander concern only. + +--- + +## Compiler Strategy (Reference) + +See `docs/roadmap.md` Level 4 for the phased compilation plan. + +Each core form compiles to a `CompiledExpr`: +```typescript +type CompiledExpr = (ctx: EvaluationContext) => CljValue +``` + +Key properties of compiled expressions: +- **Literals** → constant closures `() => value` +- **Symbols** → slot-indexed env access `(ctx) => ctx.env[slot]` (for `let*`-bound vars) or `ctx.resolveNs(ns).vars.get(name).deref()` (for dynamic/global vars) +- **`if`** → short-circuit: `(ctx) => isTruthy(test(ctx)) ? then(ctx) : else(ctx)` +- **`fn*`** → captures lexical env at creation: `(ctx) => makeClosure(compiledBody, ctx.env)` +- **`loop*`/`recur`** → `while` loop, no stack growth +- **Dynamic vars** → always resolved through `ctx.resolveNs` at call time, never captured at compile time diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..20b37ea --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,127 @@ +# Conjure-JS — Roadmap + +> **This is the north star document.** It describes what Conjure is, where it stands today, and where it is going. Update it when decisions change. Read it at the start of every work session. + +--- + +## What Conjure Is + +A **runtime-embeddable, snapshot-capable, module-injectable Clojure interpreter that runs anywhere JavaScript runs.** + +This is unoccupied territory: +- ClojureScript requires AOT compilation and the full CLJS toolchain — not embeddable +- Babashka runs on the JVM — not JS-native +- Nobody else has done this + +**The interpreter overhead is a feature, not just a limitation.** The ability to evaluate code at runtime, inspect and evolve a live namespace, and snapshot/restore a session is what makes live programming, metaprogramming, and runtime composition possible. An AOT-compiled approach cannot do this. We optimize the interpreter — we do not replace it. + +--- + +## Target Use Cases + +Conjure shines wherever **business logic, flexibility, metaprogramming, and live programming** are valuable in combination. Concretely: + +- **Data processing scripts** — readable, composable, data-oriented pipelines +- **Business logic layers** — hot-swappable rules, live REPL iteration +- **MCP server runtimes** — give an AI a full live Clojure runtime to eval and evolve its own code during a session +- **Live/exploratory programming** — REPL-driven development in any JS environment +- **Custom embedded languages** — inject domain capabilities as namespaces via the RuntimeModule system + +The **RuntimeModule system** is the key extensibility primitive. Users inject host capabilities (IO, HTTP, databases, domain APIs) as namespaces — not just Clojure source. This makes Conjure run correctly in any environment with a custom capability surface. + +--- + +## Current State (as of Session 107, 2026-03-14) + +### What is fully working +- Complete interpreter pipeline: Tokenizer → Reader → Expander → Evaluator → Printer +- Full Clojure stdlib coverage including lazy sequences, transducers, atoms, multimethods +- Macro system: `defmacro`, quasiquote, `syntax-quote`, `gensym` +- Namespace model with `require`, namespace aliasing, qualified keywords +- Try/catch/finally, destructuring (vector + map, nested, lazy-aware) +- Dynamic vars (`*out*`, `*err*`), `with-out-str`/`with-err-str` +- Async foundation: `CljPending`, `(async ...)` special form, `@` unwrap, `then`/`catch*` +- Session API: `createSession`, `evaluateAsync`, `snapshotSession`, `createSessionFromSnapshot` +- RuntimeModule system with Kahn-sorted dependency resolution +- nREPL server (TCP, bencode) — Calva connects via Generic project type +- JS interop: `CljJsValue`, `js/` namespace, `.` member access, `js/new`, full conversion layer +- Vite plugin: static analysis, codegen, HMR, TypeScript binding generation, nREPL relay +- Node and browser host modules +- Distributed nREPL mesh experiment (Redis-backed, streaming stdout) +- ~2100 tests, 0 failures + +### Known technical debt +1. **`readPrintCtx` env aliasing** — `tryLookup('*print-length*', callEnv)` is a latent bug (same class as the Session 86 bug). Fix: use `ctx.resolveNs` instead of `tryLookup`. One line. +2. **`runtime.ts` at ~977 lines** — mixes registry management, bootstrap, and the snapshot invariant. The "never reinstall stdlib in restoreRuntime" rule is only in comments. +3. **Async/sync evaluator duplication** — `async-evaluator.ts` (~554 lines) duplicates special-form handling. The divergence at function application is intentional and correct; the rest is reducible. +4. **`collections.ts` at ~958 lines** — too large. Natural splits: `seq.ts`, `maps-sets.ts`, `vectors.ts`. +5. **`nrepl-relay.ts` in wrong package** — lives in `vite-plugin-clj/`, should be `src/nrepl/relay.ts`. +6. **`browser.ts` in wrong location** — lives in `vite-plugin-clj/`, should be `src/host/browser.ts`. +7. **Mode 2 factory uses positional args** — `(importMap, onOutput?)` should be `ConjureFactoryContext` object. + +--- + +## Priority Queue + +Work items in order. Do not skip ahead — each layer is the foundation for the next. + +### Level 1 — Immediate (fix before anything else) + +- [ ] **Fix `readPrintCtx` env aliasing** — one line in `printer.ts`, prevents a latent time bomb +- [ ] **Move `browser.ts` → `src/host/browser.ts`** — fixes asymmetry with node host module +- [ ] **Move `nrepl-relay.ts` → `src/nrepl/relay.ts`** — correct conceptual home +- [ ] **Split `collections.ts`** → `seq.ts` + `maps-sets.ts` + `vectors.ts` + +### Level 2 — Architecture (before adding new features) + +- [ ] **Move `quasiquote` to the expander** — purely syntactic transform, should never reach the evaluator. Remove from `specialFormKeywords`. +- [ ] **Rename `letfn` → `letfn*`** (the special form is the mutual-recursion primitive); add `letfn` as a macro in `clojure/core.clj` +- [ ] **Extract multimethod primitives** — `make-multimethod`, `add-method!`, `multimethod?` as native functions; rewrite `defmulti`/`defmethod` as macros in `core.clj` using these. Preserve the re-eval guard: if the var already holds a multimethod, don't reset it. +- [ ] **Make `delay` a macro** — `(defmacro delay [expr] \`(make-delay (fn* [] ~expr)))`; expose `make-delay` as a native function in the core module +- [ ] **Split `runtime.ts`** → `src/core/registry.ts` (namespace registry + clone), `src/core/bootstrap.ts` (buildRuntime + installModules + invariant documentation), thin `src/core/runtime.ts` (createRuntime / restoreRuntime orchestrators) + +### Level 3 — Medium Term + +- [ ] **Reduce async/sync evaluator duplication** — extract shared helpers (`evaluateBody`, `evaluateDestructure`, `evaluateArgs`) that both paths call. Divergence at function application is intentional; everything else should converge. +- [ ] **Library distribution strategy** — decide how `.clj` libraries ship via npm. Leading candidate: ship `.clj` source files + a manifest; the Vite static analysis pass follows requires across package boundaries. Needs a dedicated design session. +- [ ] **`ConjureFactoryContext` object** — replace `(importMap, onOutput?)` with `{ importMap, onOutput?, onError? }` for forward-compatibility + +### Level 4 — Long Term (Compiler) + +See `docs/core-language.md` for the compilation target. + +- [ ] **Phase 1: Compiler foundation** — `evaluator/compiler.ts`; compile literals, symbols, and plain function calls to JS closures `(ctx) => CljValue` +- [ ] **Phase 2: Control flow** — compile `if`, `do`, `throw`, `try` +- [ ] **Phase 3: Slot indexing** — compile `let*`, assign variable slots at compile time; `env[0]` replaces `tryLookup("x", env)`. This is the key performance gain. +- [ ] **Phase 4: Closure compilation** — compile `fn*`; slot-indexed params; var-object deref for globals +- [ ] **Phase 5: Tail calls** — compile `loop*`/`recur` to `while` loops; no stack growth + +--- + +## Architecture Principles + +These are the rules that must not be broken as the codebase evolves: + +1. **The core interpreter is host-agnostic.** No `fs`, `net`, or browser globals in `src/core/`. Host capabilities enter only through `RuntimeModule`. + +2. **Never reinstall stdlib in `restoreRuntime`.** `clojure.core.clj` overwrites many native functions with lazy Clojure versions. The snapshot contains these. Reinstalling native versions silently reverts them. + +3. **Dynamic vars use `ctx.resolveNs`, not `tryLookup`.** Functions defined during bootstrap close over the original snapshot env. `tryLookup` traverses that env and finds stale pre-clone vars. `ctx.resolveNs` goes through the current session's registry and is always correct. + +4. **`@` is the only explicit async unwrap.** No auto-awaiting in `applyCallableAsync`. The sync evaluator stays sync; `async` is the opt-in boundary. + +5. **IO routes through `emitToOut`/`emitToErr`.** Never call `ctx.io.stdout` directly. Always go through the IO routing layer so `*out*`/`*err*` dynamic binding and `with-out-str` work correctly. + +--- + +## Long-Term Vision + +Once the architecture cleanup is complete and the compiler phases are underway, the project's major effort shifts to: + +1. **Documentation and integration guides** — clear setup guides for Node, Bun, Deno, browser, Vite. The experiments folder becomes `examples/` with runnable, documented setups. +2. **Library distribution** — a path for sharing Clojure namespaces via npm. +3. **Self-hosting milestones** — gradually implement more of the evaluator in Clojure itself, using the compiler infrastructure. + +The dream: a project where you can write Clojure in any JavaScript environment, share libraries via npm, inject domain capabilities as namespaces, and have a live REPL anywhere — with performance good enough for production use cases. + +**We have already proven this is achievable. The architecture works. The path is clear.** diff --git a/experiments/node-js-interop/bun.lock b/experiments/node-js-interop/bun.lock new file mode 100644 index 0000000..9354b82 --- /dev/null +++ b/experiments/node-js-interop/bun.lock @@ -0,0 +1,261 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "node-js-interop", + "dependencies": { + "conjure-js": "file:../../packages/conjure-js", + }, + }, + }, + "packages": { + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.4", "", { "os": "android", "cpu": "arm" }, "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.4", "", { "os": "android", "cpu": "arm64" }, "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.4", "", { "os": "android", "cpu": "x64" }, "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.4", "", { "os": "linux", "cpu": "arm" }, "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.4", "", { "os": "linux", "cpu": "x64" }, "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.4", "", { "os": "none", "cpu": "x64" }, "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + + "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.0", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.0", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.1.0", "vitest": "4.1.0" }, "optionalPeers": ["@vitest/browser"] }, "sha512-nDWulKeik2bL2Va/Wl4x7DLuTKAXa906iRFooIRPR+huHkcvp9QDkPQ2RJdmjOFrqOqvNfoSQLF68deE3xC3CQ=="], + + "@vitest/expect": ["@vitest/expect@4.1.0", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.0", "@vitest/utils": "4.1.0", "chai": "^6.2.2", "tinyrainbow": "^3.0.3" } }, "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.0", "", { "dependencies": { "@vitest/spy": "4.1.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.0", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A=="], + + "@vitest/runner": ["@vitest/runner@4.1.0", "", { "dependencies": { "@vitest/utils": "4.1.0", "pathe": "^2.0.3" } }, "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.0", "", { "dependencies": { "@vitest/pretty-format": "4.1.0", "@vitest/utils": "4.1.0", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg=="], + + "@vitest/spy": ["@vitest/spy@4.1.0", "", {}, "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw=="], + + "@vitest/ui": ["@vitest/ui@4.1.0", "", { "dependencies": { "@vitest/utils": "4.1.0", "fflate": "^0.8.2", "flatted": "3.4.0", "pathe": "^2.0.3", "sirv": "^3.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "vitest": "4.1.0" } }, "sha512-sTSDtVM1GOevRGsCNhp1mBUHKo9Qlc55+HCreFT4fe99AHxl1QQNXSL3uj4Pkjh5yEuWZIx8E2tVC94nnBZECQ=="], + + "@vitest/utils": ["@vitest/utils@4.1.0", "", { "dependencies": { "@vitest/pretty-format": "4.1.0", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" } }, "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg=="], + + "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "conjure-js": ["conjure-js@file:../../packages/conjure-js", { "devDependencies": { "@types/bun": "^1.3.10", "@types/node": "^25.3.5", "@vitest/coverage-v8": "^4.0.18", "@vitest/ui": "^4.0.18", "prettier": "^3.8.1", "typescript": "~5.9.3", "vite": "^7.3.1", "vitest": "^4.0.18" }, "bin": { "conjure-js": "./dist-cli/conjure-js.mjs" } }], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], + + "esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + + "flatted": ["flatted@3.4.0", "", {}, "sha512-kC6Bb+ooptOIvWj5B63EQWkF0FEnNjV2ZNkLMLZRDDduIiWeFF4iKnslwhiWxjAdbg4NzTNo6h0qLuvFrcx+Sw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], + + "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], + + "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], + + "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="], + + "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + + "vitest": ["vitest@4.1.0", "", { "dependencies": { "@vitest/expect": "4.1.0", "@vitest/mocker": "4.1.0", "@vitest/pretty-format": "4.1.0", "@vitest/runner": "4.1.0", "@vitest/snapshot": "4.1.0", "@vitest/spy": "4.1.0", "@vitest/utils": "4.1.0", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.0", "@vitest/browser-preview": "4.1.0", "@vitest/browser-webdriverio": "4.1.0", "@vitest/ui": "4.1.0", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + } +} diff --git a/experiments/node-js-interop/main.ts b/experiments/node-js-interop/main.ts new file mode 100644 index 0000000..7d694bf --- /dev/null +++ b/experiments/node-js-interop/main.ts @@ -0,0 +1,180 @@ +/** + * Node/Bun JS Interop Experiment + * + * Experimenting with conjure-js + native ESM dynamic imports. + * No bundler — just Bun + * + * bun run main.ts + * + * Key design under test: + * importModule: (s) => import(s) + * + * At build time (Vite), a static import table replaces this. + * Here we use real dynamic import() to validate the runtime contract. + */ + +import { readFileSync } from 'node:fs' +import { createSession, printString } from 'conjure-js' +import { startNreplServer } from 'conjure-js/nrepl' + +const session = createSession({ + output: (text) => process.stdout.write(text), + importModule: (specifier) => import(specifier), + hostBindings: { Math, console }, + sourceRoots: ['src'], + readFile: (filePath) => readFileSync(filePath, 'utf-8'), +}) + +function section(title: string) { + console.log(`\n── ${title} ${'─'.repeat(50 - title.length)}`) +} + +// ── 1. Basic method call via the . special form ───────────────────────────── +// +// (:require ["node:path" :as path]) loads the module via importModule, +// boxes the whole namespace object as CljJsValue, and binds it to `path` +// in the current namespace (NOT in js/). +// +// (. path join "a" "b") → path["join"]("a", "b") → "a/b" +section('1. Basic method call') + +const result1 = await session.evaluateAsync(` + (ns demo.path + (:require ["node:path" :as path])) + ; call node path method + (. path join "src" "components" "App.tsx") +`) +console.log('(. path join "src" "components" "App.tsx") =>', printString(result1)) + +// ── 2. After the module is loaded, sync evaluate() works fine ─────────────── +// +// importModule runs during (:require ...) processing, which only happens +// once per ns declaration. Subsequent evaluations of the same namespace +// are fully synchronous. +section('2. Sync evaluate after require') + +const result2 = session.evaluate(` + (. path dirname "/usr/local/bin/conjure") +`) +console.log('(. path dirname "/usr/local/bin/conjure") =>', printString(result2)) + +// ── 3. Compose with Clojure higher-order functions ────────────────────────── +// +// path is just a CljJsValue — it composes naturally with core functions. +// The anon-fn #(. path basename %) is idiomatic Clojure style. +section('3. Compose with Clojure HOFs') + +const result3 = await session.evaluateAsync(` + (ns demo.hof + (:require ["node:path" :as path])) + (mapv #(. path basename %) ["/foo/bar/baz.txt" "/home/user/app.js" "main.clj"]) +`) +console.log('(mapv #(. path basename %) [...]) =>', printString(result3)) + +// ── 4. Nested calls — return value of . is a Clojure string ───────────────── +// +// (. path join ...) returns a CljString. We can pass it directly into +// another (. path dirname ...) call — jsToClj/cljToJs handle the round-trip. +section('4. Nested calls') + +const result4 = await session.evaluateAsync(` + (ns demo.nested + (:require ["node:path" :as path])) + (. path dirname (. path join "/usr/local" "bin/foo.sh")) +`) +console.log('(. path dirname (. path join ...)) =>', printString(result4)) + +// ── 5. js/get to access a module export as a first-class value ────────────── +// +// Sometimes you want the function itself, not a method call. +// (js/get path "extname") retrieves path.extname as a CljJsValue fn. +// (js/call ext-fn "index.ts") invokes it with no this binding. +section('5. js/get + js/call') + +const result5 = await session.evaluateAsync(` + (ns demo.jsget + (:require ["node:path" :as path])) + (let [ext-fn (js/get path "extname")] + (mapv #(js/call ext-fn %) ["index.ts" "core.clj" "styles.css"])) +`) +console.log('(mapv #(js/call ext-fn %) [...]) =>', printString(result5)) + +// ── 6. println output routes through the session output fn ────────────────── +// +// Output from (println ...) flows through the output: option on createSession. +// Here that's process.stdout.write, so println output appears inline. +section('6. println in Clojure code') + +console.log('Clojure println output:') +await session.evaluateAsync(` + (ns demo.output + (:require ["node:path" :as path])) + (println "resolve src =>" (. path resolve "src")) + (println "parse /foo/bar.ts =>") + (let [parsed (js/get path "parse")] + (println " base:" (js/get (js/call parsed "/foo/bar.ts") "base"))) +`) + +// ── 7. Accessing js/Math from hostBindings ─────────────────────────────────── +section('7. hostBindings — js/Math') + +const result7 = session.evaluate(` + (let [result (mapv #(. js/Math pow % 2) [1 2 3 4 5])] + (. js/console log result) + result) +`) +console.log('(mapv #(. js/Math pow % 2) [1..5]) =>', printString(result7)) + +// ── 8. Load a Clojure file from disk ──────────────────────────────────────── +// +// readFile: (filePath) => readFileSync(filePath, 'utf-8') lets the runtime +// resolve (:require [demo.utils]) by reading src/demo/utils.clj from disk. +// +// The runtime constructs the path as: {sourceRoot}/{ns/path}.clj +// e.g. sourceRoot "src" + ns "demo.utils" → "src/demo/utils.clj" +section('8. Load Clojure file from disk') + +// demo.utils has JS string requires inside it (node:path, node:http). +// We evaluate it directly via evaluateAsync so processNsRequiresAsync +// can await those imports before running the defns. +// After this call, demo.utils is in the runtime registry. +const utilsSource = readFileSync('src/demo/utils.clj', 'utf-8') +await session.evaluateAsync(utilsSource) + +// [demo.utils] is now a registry cache hit — sync require is safe. +const result8 = session.evaluate(` + (ns demo.app + (:require [demo.utils :as utils])) + (utils/group-by-ext + ["index.ts" "core.clj" "styles.css" "parser.clj" "main.ts" "repl.clj"]) +`) +console.log('(utils/group-by-ext [...]) =>', printString(result8)) + +const result9 = session.evaluate(` + (filter utils/clj-file? ["server.ts" "routes.clj" "db.ts" "handlers.clj"]) +`) +console.log('(filter utils/clj-file? [...]) =>', printString(result9)) + +console.log('\n✓ All demos complete\n') + +// ── 9. nREPL server ────────────────────────────────────────────────────────── +// +// Snapshot the session (post-demos) and serve it over nREPL. +// Every editor connection gets a fresh clone of this state: +// - js/Math, js/console (from hostBindings) +// - demo.utils namespace (loaded from disk in section 8) +// - demo.path, demo.hof, etc. (loaded during the demos above) +// - importModule wired up — string requires work in the REPL too +// +// Connect with Calva → "Connect to a running nREPL server" → port in .nrepl-port +section('9. nREPL server') + +startNreplServer({ + session, + importModule: (s) => import(s), + sourceRoots: ['src'], + onOutput: (text) => process.stdout.write(text), +}) + +// Process stays alive while the TCP server is running. +// Ctrl+C to stop. diff --git a/experiments/node-js-interop/package-lock.json b/experiments/node-js-interop/package-lock.json new file mode 100644 index 0000000..0d1d8f8 --- /dev/null +++ b/experiments/node-js-interop/package-lock.json @@ -0,0 +1,36 @@ +{ + "name": "node-js-interop", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "node-js-interop", + "version": "0.0.0", + "dependencies": { + "conjure-js": "file:../../packages/conjure-js" + } + }, + "../../packages/conjure-js": { + "version": "0.0.12", + "license": "MIT", + "bin": { + "conjure-js": "dist-cli/conjure-js.mjs" + }, + "devDependencies": { + "@types/bun": "^1.3.10", + "@types/node": "^25.3.5", + "@vitest/coverage-v8": "^4.0.18", + "@vitest/ui": "^4.0.18", + "prettier": "^3.8.1", + "typescript": "~5.9.3", + "vite": "^7.3.1", + "vitest": "^4.0.18" + } + }, + "node_modules/conjure-js": { + "resolved": "../../packages/conjure-js", + "link": true + } + } +} diff --git a/experiments/node-js-interop/package.json b/experiments/node-js-interop/package.json new file mode 100644 index 0000000..4991f25 --- /dev/null +++ b/experiments/node-js-interop/package.json @@ -0,0 +1,12 @@ +{ + "name": "node-js-interop", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "start": "bun run main.ts" + }, + "dependencies": { + "conjure-js": "file:../../packages/conjure-js" + } +} diff --git a/experiments/node-js-interop/src/demo/utils.clj b/experiments/node-js-interop/src/demo/utils.clj new file mode 100644 index 0000000..93e7f3a --- /dev/null +++ b/experiments/node-js-interop/src/demo/utils.clj @@ -0,0 +1,44 @@ +(ns demo.utils + (:require [clojure.string :as str] + ["node:path" :as path] + ["node:http" :as http])) + +(defn file-ext + "Returns the extension (without dot) for a filename, or nil if no dot." + [filename] + (let [parts (str/split filename #"\.")] + (when (> (count parts) 1) + (last parts)))) + +(defn clj-file? [filename] + (= (file-ext filename) "clj")) + +(defn group-by-ext + "Groups a collection of filenames by their extension." + [filenames] + (group-by file-ext filenames)) + + +(. js/console log "Hello through console.log!!") + +(map #(. js/Math pow % 2) [1 2 3 4 5]) + +(file-ext "foo.com/bar.cljss") + +*ns* + +(println "hi!") + +(defn deep [n] + (if (zero? n) + :done + (deep (dec n)))) + +(deep 3936) + +(loop [n 1000000] + (if (zero? n) + :done + (recur (dec n)))) + +(inc 2) \ No newline at end of file diff --git a/experiments/vite-js-interop/bun.lock b/experiments/vite-js-interop/bun.lock new file mode 100644 index 0000000..5ac5962 --- /dev/null +++ b/experiments/vite-js-interop/bun.lock @@ -0,0 +1,367 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "vite-js-interop", + "dependencies": { + "conjure-js": "file:../../packages/conjure-js", + "date-fns": "^4.1.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + }, + "devDependencies": { + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "typescript": "~5.9.3", + "vite": "^8.0.0-beta.13", + }, + }, + }, + "packages": { + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], + + "@emnapi/core": ["@emnapi/core@1.9.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.4", "", { "os": "android", "cpu": "arm" }, "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.4", "", { "os": "android", "cpu": "arm64" }, "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.4", "", { "os": "android", "cpu": "x64" }, "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.4", "", { "os": "linux", "cpu": "arm" }, "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.4", "", { "os": "linux", "cpu": "x64" }, "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.4", "", { "os": "none", "cpu": "x64" }, "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + + "@oxc-project/runtime": ["@oxc-project/runtime@0.115.0", "", {}, "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ=="], + + "@oxc-project/types": ["@oxc-project/types@0.115.0", "", {}, "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw=="], + + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.9", "", { "os": "android", "cpu": "arm64" }, "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9", "", { "os": "linux", "cpu": "arm" }, "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "x64" }, "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.9", "", { "os": "linux", "cpu": "x64" }, "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.9", "", { "os": "none", "cpu": "arm64" }, "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.9", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.9", "", { "os": "win32", "cpu": "x64" }, "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="], + + "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.0", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.0", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.1.0", "vitest": "4.1.0" }, "optionalPeers": ["@vitest/browser"] }, "sha512-nDWulKeik2bL2Va/Wl4x7DLuTKAXa906iRFooIRPR+huHkcvp9QDkPQ2RJdmjOFrqOqvNfoSQLF68deE3xC3CQ=="], + + "@vitest/expect": ["@vitest/expect@4.1.0", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.0", "@vitest/utils": "4.1.0", "chai": "^6.2.2", "tinyrainbow": "^3.0.3" } }, "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.0", "", { "dependencies": { "@vitest/spy": "4.1.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.0", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A=="], + + "@vitest/runner": ["@vitest/runner@4.1.0", "", { "dependencies": { "@vitest/utils": "4.1.0", "pathe": "^2.0.3" } }, "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.0", "", { "dependencies": { "@vitest/pretty-format": "4.1.0", "@vitest/utils": "4.1.0", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg=="], + + "@vitest/spy": ["@vitest/spy@4.1.0", "", {}, "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw=="], + + "@vitest/ui": ["@vitest/ui@4.1.0", "", { "dependencies": { "@vitest/utils": "4.1.0", "fflate": "^0.8.2", "flatted": "3.4.0", "pathe": "^2.0.3", "sirv": "^3.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "vitest": "4.1.0" } }, "sha512-sTSDtVM1GOevRGsCNhp1mBUHKo9Qlc55+HCreFT4fe99AHxl1QQNXSL3uj4Pkjh5yEuWZIx8E2tVC94nnBZECQ=="], + + "@vitest/utils": ["@vitest/utils@4.1.0", "", { "dependencies": { "@vitest/pretty-format": "4.1.0", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" } }, "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg=="], + + "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "conjure-js": ["conjure-js@file:../../packages/conjure-js", { "devDependencies": { "@types/bun": "^1.3.10", "@types/node": "^25.3.5", "@vitest/coverage-v8": "^4.0.18", "@vitest/ui": "^4.0.18", "prettier": "^3.8.1", "typescript": "~5.9.3", "vite": "^7.3.1", "vitest": "^4.0.18" }, "bin": { "conjure-js": "./dist-cli/conjure-js.mjs" } }], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], + + "esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + + "flatted": ["flatted@3.4.0", "", {}, "sha512-kC6Bb+ooptOIvWj5B63EQWkF0FEnNjV2ZNkLMLZRDDduIiWeFF4iKnslwhiWxjAdbg4NzTNo6h0qLuvFrcx+Sw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], + + "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], + + "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], + + "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], + + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="], + + "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + + "rolldown": ["rolldown@1.0.0-rc.9", "", { "dependencies": { "@oxc-project/types": "=0.115.0", "@rolldown/pluginutils": "1.0.0-rc.9" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.9", "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", "@rolldown/binding-darwin-x64": "1.0.0-rc.9", "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q=="], + + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "vite": ["vite@8.0.0", "", { "dependencies": { "@oxc-project/runtime": "0.115.0", "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.9", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.0.0-alpha.31", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q=="], + + "vitest": ["vitest@4.1.0", "", { "dependencies": { "@vitest/expect": "4.1.0", "@vitest/mocker": "4.1.0", "@vitest/pretty-format": "4.1.0", "@vitest/runner": "4.1.0", "@vitest/snapshot": "4.1.0", "@vitest/spy": "4.1.0", "@vitest/utils": "4.1.0", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.0", "@vitest/browser-preview": "4.1.0", "@vitest/browser-webdriverio": "4.1.0", "@vitest/ui": "4.1.0", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "conjure-js/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + + "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.9", "", {}, "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw=="], + } +} diff --git a/experiments/vite-js-interop/index.html b/experiments/vite-js-interop/index.html new file mode 100644 index 0000000..b45acbe --- /dev/null +++ b/experiments/vite-js-interop/index.html @@ -0,0 +1,34 @@ + + + + + + Conjure — Vite JS Interop Demo + + + +
+ + + diff --git a/experiments/vite-js-interop/package-lock.json b/experiments/vite-js-interop/package-lock.json new file mode 100644 index 0000000..99ab028 --- /dev/null +++ b/experiments/vite-js-interop/package-lock.json @@ -0,0 +1,523 @@ +{ + "name": "vite-js-interop", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vite-js-interop", + "version": "0.0.0", + "dependencies": { + "conjure-js": "file:../../packages/conjure-js", + "date-fns": "^4.1.0", + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "typescript": "~5.9.3", + "vite": "^8.0.0-beta.13" + } + }, + "../../packages/conjure-js": { + "version": "0.0.12", + "license": "MIT", + "bin": { + "conjure-js": "dist-cli/conjure-js.mjs" + }, + "devDependencies": { + "@types/bun": "^1.3.10", + "@types/node": "^25.3.5", + "@vitest/coverage-v8": "^4.0.18", + "@vitest/ui": "^4.0.18", + "prettier": "^3.8.1", + "typescript": "~5.9.3", + "vite": "^7.3.1", + "vitest": "^4.0.18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@oxc-project/runtime": { + "version": "0.115.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.115.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.9", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.0", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/conjure-js": { + "resolved": "../../packages/conjure-js", + "link": true + }, + "node_modules/csstype": { + "version": "3.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "4.1.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.4", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.115.0", + "@rolldown/pluginutils": "1.0.0-rc.9" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-x64": "1.0.0-rc.9", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.9", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/vite": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "0.115.0", + "lightningcss": "^1.32.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.9", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.0.0-alpha.31", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/experiments/vite-js-interop/package.json b/experiments/vite-js-interop/package.json new file mode 100644 index 0000000..e74a552 --- /dev/null +++ b/experiments/vite-js-interop/package.json @@ -0,0 +1,24 @@ +{ + "name": "vite-js-interop", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "test:build": "bun run build && bun test-build.ts" + }, + "dependencies": { + "conjure-js": "file:../../packages/conjure-js", + "date-fns": "^4.1.0", + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "typescript": "~5.9.3", + "vite": "^8.0.0-beta.13" + } +} diff --git a/experiments/vite-js-interop/src/app.tsx b/experiments/vite-js-interop/src/app.tsx new file mode 100644 index 0000000..86d3c93 --- /dev/null +++ b/experiments/vite-js-interop/src/app.tsx @@ -0,0 +1,200 @@ +import { StrictMode, type CSSProperties } from 'react' +import { createRoot } from 'react-dom/client' + +// Direct CLJ module imports — the Vite plugin compiles each .clj file to a JS +// module that exports each public var as a typed JS function. Top-level await in +// each generated module ensures the namespace is fully loaded before any import +// of it resolves, so all functions are synchronously callable at render time. +import { + multiply, + sum_of_squares, + greet, + log_text, + math_abs, + math_sqrt, +} from './clojure/demo/utils.clj' +import { format_iso, compare_dates } from './clojure/demo/format.clj' +import { process_numbers, pipeline_report } from './clojure/demo/pipeline.clj' + +// ── Types ───────────────────────────────────────────────────────────────────── + +interface DemoResult { + label: string + category: string + value?: string + error?: string +} + +// ── Demo runner ─────────────────────────────────────────────────────────────── + +function runSafely( + label: string, + category: string, + fn: () => unknown +): DemoResult { + try { + const raw = fn() + const value = + raw === null || raw === undefined + ? 'nil' + : typeof raw === 'object' + ? JSON.stringify(raw) + : String(raw) + return { label, category, value } + } catch (e) { + return { label, category, error: String(e) } + } +} + +// All CLJ functions are synchronous here — top-level await in each generated +// .clj module guarantees the namespace is ready before this module body runs. +const RESULTS: DemoResult[] = [ + runSafely('(multiply 6 7)', 'Pure Clojure (no JS deps)', () => multiply(6, 7)), + runSafely( + '(sum-of-squares [1 2 3 4 5])', + 'Pure Clojure (no JS deps)', + () => sum_of_squares([1, 2, 3, 4, 5]) + ), + runSafely('(greet "World")', 'Pure Clojure (no JS deps)', () => greet('World')), + runSafely( + '(math-abs -99)', + 'HostBindings (js/Math via conjure.ts)', + () => math_abs(-99) + ), + runSafely( + '(math-sqrt 144)', + 'HostBindings (js/Math via conjure.ts)', + () => math_sqrt(144) + ), + runSafely( + '(format-iso "2024-01-15" "yyyy-MM-dd")', + 'date-fns via static import table', + () => format_iso('2024-01-15', 'yyyy-MM-dd') + ), + runSafely( + '(compare-dates 1000 2000)', + 'date-fns via static import table', + () => compare_dates(1000, 2000) + ), + runSafely( + '(process-numbers [1 2 3 4 5])', + 'CLJ→CLJ chain (pipeline → utils + format)', + () => process_numbers([1, 2, 3, 4, 5]) + ), + runSafely( + '(pipeline-report [3 4] "2025-06-01")', + 'CLJ→CLJ chain (pipeline → utils + format)', + () => pipeline_report([3, 4], '2025-06-01') + ), + runSafely( + '(log-text "hello") via host.ts', + 'Local TS import via string require', + () => { + log_text('hello from CLJ') + return 'logged! (check console)' + } + ), +] + +// ── Styles ──────────────────────────────────────────────────────────────────── + +const s = { + header: { marginBottom: '2rem' } as CSSProperties, + title: { + fontSize: '1.1rem', + color: '#569cd6', + fontWeight: 700, + letterSpacing: '0.04em', + marginBottom: '0.25rem', + } as CSSProperties, + subtitle: { fontSize: '0.78rem', color: '#858585' } as CSSProperties, + section: { marginBottom: '1.5rem' } as CSSProperties, + sectionTitle: { + fontSize: '0.72rem', + textTransform: 'uppercase' as const, + letterSpacing: '0.08em', + color: '#858585', + marginBottom: '0.5rem', + paddingBottom: '0.25rem', + borderBottom: '1px solid #3e3e42', + } as CSSProperties, + row: { + display: 'flex', + alignItems: 'baseline', + gap: '1rem', + padding: '0.45rem 0.75rem', + background: '#252526', + borderRadius: '4px', + marginBottom: '0.3rem', + fontSize: '0.83rem', + } as CSSProperties, + label: { + color: '#dcdcaa', + minWidth: '280px', + flexShrink: 0, + fontFamily: 'monospace', + } as CSSProperties, + value: { color: '#4ec9b0', flex: 1, wordBreak: 'break-all' as const } as CSSProperties, + error: { color: '#f44747', flex: 1 } as CSSProperties, + dot: (ok: boolean): CSSProperties => ({ + width: '7px', + height: '7px', + borderRadius: '50%', + flexShrink: 0, + marginTop: '2px', + background: ok ? '#4ec9b0' : '#f44747', + }), + footer: { marginTop: '2rem', fontSize: '0.72rem', color: '#858585' } as CSSProperties, +} + +// ── Component ───────────────────────────────────────────────────────────────── + +function App() { + // Group results by category + const grouped = RESULTS.reduce>((acc, r) => { + if (!acc[r.category]) acc[r.category] = [] + acc[r.category].push(r) + return acc + }, {}) + + const totalPassed = RESULTS.filter((r) => !r.error).length + const totalFailed = RESULTS.filter((r) => r.error).length + + return ( +
+
+
conjure-js · vite-js-interop experiment
+
+ Direct CLJ imports · static import table · Mode 2 entrypoint · CLJ→CLJ + chains · local TS string require +
+
+ + {Object.entries(grouped).map(([category, rows]) => ( +
+
{category}
+ {rows.map((r) => ( +
+
+
{r.label}
+ {r.value !== undefined &&
{r.value}
} + {r.error !== undefined &&
{r.error}
} +
+ ))} +
+ ))} + +
+ {totalFailed === 0 + ? `✓ all ${totalPassed} demos passed` + : `✗ ${totalFailed} failed · ${totalPassed} passed`} +
+
+ ) +} + +createRoot(document.getElementById('root')!).render( + + + +) diff --git a/experiments/vite-js-interop/src/clojure/demo/format.clj b/experiments/vite-js-interop/src/clojure/demo/format.clj new file mode 100644 index 0000000..5730f7f --- /dev/null +++ b/experiments/vite-js-interop/src/clojure/demo/format.clj @@ -0,0 +1,19 @@ +(ns demo.format + (:require ["date-fns" :as date-fns])) + +;; This namespace proves the static import table is working: +;; date-fns is required as a string spec, which the plugin must scan at build time +;; and emit as a static import. At runtime, importModule("date-fns") is a synchronous +;; map lookup — no dynamic import() call in the bundle. + +(defn format-iso [iso-string pattern] + ;; parseISO("2024-01-15") → Date object; format(date, pattern) → "2024-01-15" + (let [parse-iso (js/get date-fns "parseISO") + fmt (js/get date-fns "format") + date (js/call parse-iso iso-string)] + (js/call fmt date pattern))) + +(defn compare-dates [ts-a ts-b] + ;; compareAsc works with numeric timestamps; returns -1, 0, or 1 + (let [compare-asc (js/get date-fns "compareAsc")] + (js/call compare-asc ts-a ts-b))) diff --git a/experiments/vite-js-interop/src/clojure/demo/pipeline.clj b/experiments/vite-js-interop/src/clojure/demo/pipeline.clj new file mode 100644 index 0000000..5e79729 --- /dev/null +++ b/experiments/vite-js-interop/src/clojure/demo/pipeline.clj @@ -0,0 +1,17 @@ +(ns demo.pipeline + (:require [demo.utils :as utils] + [demo.format :as fmt])) + +;; Multi-dependency namespace: CLJ→CLJ symbol requires chain. +;; Validates that a namespace depending on both a pure-CLJ ns and a string-require ns +;; can be loaded and evaluated correctly after build. + +(defn process-numbers [nums] + {:sum-of-squares (utils/sum-of-squares nums) + :multiplied (mapv (fn [n] (utils/multiply n 10)) nums)}) + +(defn pipeline-report [nums iso-date] + (let [result (process-numbers nums)] + (str "date=" (fmt/format-iso iso-date "yyyy-MM-dd") + " sum-sq=" (get result :sum-of-squares) + " multiplied=" (pr-str (get result :multiplied))))) diff --git a/experiments/vite-js-interop/src/clojure/demo/utils.clj b/experiments/vite-js-interop/src/clojure/demo/utils.clj new file mode 100644 index 0000000..b914c00 --- /dev/null +++ b/experiments/vite-js-interop/src/clojure/demo/utils.clj @@ -0,0 +1,47 @@ +(ns demo.utils + (:require ["../../utils/host" :as host])) + +;; Pure Clojure utilities — no JS dependencies. +;; Tests that CLJ→CLJ symbol requires work correctly across the chain. + +(defn multiply + "Multiplies two numbers together" + [a b] + (* a b)) + +(defn sum-of-squares [nums] + (reduce + (mapv (fn [n] (* n n)) nums))) + +(defn greet [name] + (str "Hello, " name "!")) + +(defn log-text [text] + (. host logThere text)) + +(defn math-abs [x] + (. js/Math abs x)) + +(defn math-sqrt [x] + (. js/Math sqrt x)) + +(comment + (log-text "Hi there!!") + (inc 2) + (println "Olá amandinha") + (sum-of-squares [4 8]) + (println "Sum of squares" (sum-of-squares [4 8])) + (multiply 2 3) + (doc +) + + (. js/Math pow 3 2) + + (def x 10) + x + + (log-text (str "x is " x)) + (log-text (str "(. js/Math abs -99) -> " (. js/Math abs -99))) + (def x 20) + + + + ) diff --git a/experiments/vite-js-interop/src/conjure.ts b/experiments/vite-js-interop/src/conjure.ts new file mode 100644 index 0000000..9c18c26 --- /dev/null +++ b/experiments/vite-js-interop/src/conjure.ts @@ -0,0 +1,24 @@ +import { createSession } from 'conjure-js' +import type { ImportMap } from 'conjure-js' + +// Mode 2 user-defined session factory. +// The plugin scanned all .clj files for string requires (finding "date-fns") +// and built a static import table. This factory receives it as importMap. +// +// Users control: importModule routing, hostBindings, output, stderr, etc. +export default function conjureFactory( + importMap: ImportMap, + onOutput?: (text: string) => void +) { + return createSession({ + importModule: (s) => importMap[s], + // Expose Math as js/Math — validates Mode 2 hostBindings work in Clojure + hostBindings: { Math }, + // onOutput is provided by the vite plugin to forward output to Calva (REPL :out). + // Falls back to console.log alone if not provided (e.g. standalone use). + output: (text) => { + onOutput?.(text) + console.log(text) + }, + }) +} diff --git a/experiments/vite-js-interop/src/main.ts b/experiments/vite-js-interop/src/main.ts new file mode 100644 index 0000000..f0da275 --- /dev/null +++ b/experiments/vite-js-interop/src/main.ts @@ -0,0 +1,54 @@ +import { getSession } from 'virtual:clj-session' +import { printString } from 'conjure-js' +// Side-effect imports: load the Clojure namespaces into the session. +// Dep order matters: utils and format must be loaded before pipeline. +import './clojure/demo/utils.clj' +import './clojure/demo/format.clj' // ← has (:require ["date-fns" :as date-fns]) +import './clojure/demo/pipeline.clj' // ← depends on demo.utils + demo.format + +const session = getSession() + +// ── Demo 1: Pure Clojure math (no JS deps) ────────────────────────────────── +const mul = printString(session.evaluate('(demo.utils/multiply 6 7)')) +console.log(`1. multiply 6×7 = ${mul}`) +// Expected: 42 + +// ── Demo 2: Mode 2 hostBindings — js/Math from src/conjure.ts ────────────── +// Use the dot special form: (. js/Math abs -99) calls Math.abs(-99) +const abs = printString(session.evaluate('(. js/Math abs -99)')) +console.log(`2. Math.abs(-99) = ${abs}`) +// Expected: 99 + +// ── Demo 3: String require — date-fns accessed via import map ─────────────── +const formatted = printString( + await session.evaluateAsync('(demo.format/format-iso "2024-01-15" "yyyy-MM-dd")') +) +console.log(`3. format-iso = ${formatted}`) +// Expected: "2024-01-15" + +// ── Demo 4: Compare dates using compareAsc ────────────────────────────────── +const cmp = printString( + await session.evaluateAsync('(demo.format/compare-dates 1000 2000)') +) +console.log(`4. compareAsc(1000, 2000) = ${cmp}`) +// Expected: -1 (1000 < 2000) + +// ── Demo 5: Full pipeline — CLJ→CLJ chain with JS dep transitively ────────── +const pipe = printString( + await session.evaluateAsync( + '(demo.pipeline/process-numbers [1 2 3 4 5])' + ) +) +console.log(`5. process-numbers = ${pipe}`) +// Expected: {:sum-of-squares 55, :multiplied [10 20 30 40 50]} + +// ── Demo 6: Pipeline report — composition of all layers ───────────────────── +const report = printString( + await session.evaluateAsync( + '(demo.pipeline/pipeline-report [3 4] "2025-06-01")' + ) +) +console.log(`6. pipeline-report = ${report}`) +// Expected string containing "date=2025-06-01" and "sum-sq=25" + +console.log('\n✓ All demos complete') diff --git a/experiments/vite-js-interop/src/utils/host.ts b/experiments/vite-js-interop/src/utils/host.ts new file mode 100644 index 0000000..1e5b179 --- /dev/null +++ b/experiments/vite-js-interop/src/utils/host.ts @@ -0,0 +1,3 @@ +export const logThere = (text: string) => { + console.log(text) +} diff --git a/experiments/vite-js-interop/src/vite-env.d.ts b/experiments/vite-js-interop/src/vite-env.d.ts new file mode 100644 index 0000000..4710f48 --- /dev/null +++ b/experiments/vite-js-interop/src/vite-env.d.ts @@ -0,0 +1,13 @@ +/// + +// Virtual module provided by vite-plugin-clj +declare module 'virtual:clj-session' { + import type { Session } from 'conjure-js' + export function getSession(): Session +} + +// Clojure source files — compiled to JS modules by vite-plugin-clj. +// Named exports are typed by co-located .clj.d.ts files generated by the plugin. +// This shorthand declaration (no body) is a fallback: TypeScript types all imports +// from .clj files without a generated .d.ts as `any`, suppressing unknown-module errors. +declare module '*.clj' diff --git a/experiments/vite-js-interop/test-build.ts b/experiments/vite-js-interop/test-build.ts new file mode 100644 index 0000000..4ed5905 --- /dev/null +++ b/experiments/vite-js-interop/test-build.ts @@ -0,0 +1,83 @@ +/** + * Build + runtime validation for the vite-js-interop experiment. + * + * Validates: + * 1. vite build completes without errors + * 2. The built bundle does NOT contain dynamic import("date-fns") calls + * (proves the static import table approach is working) + * 3. Running the built artifact produces correct outputs + * (proves the full JS/CLJ chain works end-to-end after build) + */ +import { execFileSync, execSync } from 'node:child_process' +import { readFileSync, existsSync } from 'node:fs' +import { resolve, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const distMain = resolve(__dirname, 'dist/main.js') + +// ─── Step 1: Run vite build ────────────────────────────────────────────────── +console.log('⟳ Running vite build...') +try { + execSync('bun run build', { cwd: __dirname, stdio: 'inherit' }) +} catch { + console.error('✗ vite build failed') + process.exit(1) +} + +if (!existsSync(distMain)) { + console.error(`✗ Expected dist/main.js not found at ${distMain}`) + process.exit(1) +} +console.log('✓ vite build succeeded') + +// ─── Step 2: Inspect bundle — no dynamic import of date-fns ───────────────── +console.log('\n⟳ Inspecting bundle for dynamic imports...') +const bundle = readFileSync(distMain, 'utf-8') + +// Dynamic import of date-fns would look like: import("date-fns") or import('date-fns') +const dynamicImportPattern = /import\s*\(\s*["']date-fns["']\s*\)/ +if (dynamicImportPattern.test(bundle)) { + console.error('✗ Bundle contains dynamic import("date-fns") — static import table not working!') + process.exit(1) +} +console.log('✓ No dynamic import("date-fns") found — static import table is correct') + +// ─── Step 3: Run the built artifact and validate output ────────────────────── +console.log('\n⟳ Running built artifact with Bun...') +let output: string +try { + output = execFileSync('bun', [distMain], { + encoding: 'utf-8', + env: { ...process.env, NODE_ENV: 'production' }, + }) +} catch (err) { + console.error('✗ Running dist/main.js failed:') + console.error(err) + process.exit(1) +} + +console.log('\n── Output ──────────────────────────────────────────────────────') +console.log(output) +console.log('────────────────────────────────────────────────────────────────\n') + +// Validate individual outputs +function assert(condition: boolean, message: string) { + if (!condition) { + console.error(`✗ FAIL: ${message}`) + process.exit(1) + } + console.log(`✓ ${message}`) +} + +assert(output.includes('multiply 6×7 = 42'), 'Demo 1: pure Clojure multiply works') +assert(output.includes('Math.abs(-99) = 99'), 'Demo 2: Mode 2 hostBindings — (. js/Math abs -99) = 99') +assert(output.includes('format-iso = "2024-01-15"'), 'Demo 3: date-fns string require resolves via import map') +assert(output.includes('compareAsc(1000, 2000) = -1'), 'Demo 4: compareAsc returns correct result') +assert(output.includes(':sum-of-squares 55'), 'Demo 5: pipeline sum-of-squares is correct') +assert(output.includes(':multiplied [10 20 30 40 50]'), 'Demo 5: pipeline multiplied values are correct') +assert(output.includes('date=2025-06-01'), 'Demo 6: pipeline-report date format is correct') +assert(output.includes('sum-sq=25'), 'Demo 6: pipeline-report sum-of-squares of [3 4] = 9+16 = 25') +assert(output.includes('✓ All demos complete'), 'All demos ran to completion') + +console.log('\n✓✓✓ All assertions passed — vite-js-interop experiment is working end-to-end!') diff --git a/experiments/vite-js-interop/tsconfig.json b/experiments/vite-js-interop/tsconfig.json new file mode 100644 index 0000000..47a5c44 --- /dev/null +++ b/experiments/vite-js-interop/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["src"] +} diff --git a/experiments/vite-js-interop/vite.config.ts b/experiments/vite-js-interop/vite.config.ts new file mode 100644 index 0000000..73fdb33 --- /dev/null +++ b/experiments/vite-js-interop/vite.config.ts @@ -0,0 +1,31 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { cljPlugin } from 'conjure-js/vite-plugin' + +// Vite JS Interop Experiment +// Validates: static import table, two-mode session entrypoint, complex CLJ dependency chains. +// Built as a library (ESM), testable with Bun after build. +export default defineConfig({ + plugins: [ + react(), + cljPlugin({ + sourceRoots: ['src/clojure'], + // Mode 2: user-defined factory (src/conjure.ts) receives the import map + // and can add custom hostBindings, output handlers, etc. + entrypoint: 'src/conjure.ts', + }), + ], + // build: { + // lib: { + // entry: 'src/main.ts', + // formats: ['es'], + // fileName: 'main', + // }, + // target: 'esnext', + // // Bundle everything including conjure-js and date-fns for a self-contained + // // runnable output — makes build artifact validation straightforward with Bun. + // rollupOptions: { + // external: (id) => id.startsWith('node:'), + // }, + // }, +}) diff --git a/packages/conjure-js/dist-vite-plugin/index.mjs b/packages/conjure-js/dist-vite-plugin/index.mjs index fd63e2b..cf58e87 100644 --- a/packages/conjure-js/dist-vite-plugin/index.mjs +++ b/packages/conjure-js/dist-vite-plugin/index.mjs @@ -1,5097 +1,8564 @@ // src/vite-plugin-clj/index.ts import { execFileSync } from "node:child_process"; import { readFileSync, writeFileSync as writeFileSync2, readdirSync, statSync } from "node:fs"; -import { resolve, relative, join as join2, dirname } from "node:path"; -import { fileURLToPath } from "node:url"; +import { resolve as resolve2, relative, join as join2, dirname as dirname2 } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { createRequire } from "node:module"; -// src/core/types.ts -var valueKeywords = { - number: "number", - string: "string", - boolean: "boolean", - keyword: "keyword", - nil: "nil", - symbol: "symbol", - list: "list", - vector: "vector", - map: "map", - function: "function", - nativeFunction: "native-function", - macro: "macro", - multiMethod: "multi-method", - atom: "atom", - reduced: "reduced", - volatile: "volatile", - regex: "regex", - var: "var" -}; -var tokenKeywords = { - LParen: "LParen", - RParen: "RParen", - LBracket: "LBracket", - RBracket: "RBracket", - LBrace: "LBrace", - RBrace: "RBrace", - String: "String", - Number: "Number", - Keyword: "Keyword", - Quote: "Quote", - Quasiquote: "Quasiquote", - Unquote: "Unquote", - UnquoteSplicing: "UnquoteSplicing", - Comment: "Comment", - Whitespace: "Whitespace", - Symbol: "Symbol", - AnonFnStart: "AnonFnStart", - Deref: "Deref", - Regex: "Regex", - VarQuote: "VarQuote", - Meta: "Meta" -}; -var tokenSymbols = { - Quote: "quote", - Quasiquote: "quasiquote", - Unquote: "unquote", - UnquoteSplicing: "unquote-splicing", - LParen: "(", - RParen: ")", - LBracket: "[", - RBracket: "]", - LBrace: "{", - RBrace: "}" -}; +// src/clojure/generated/clojure-core-source.ts +var clojure_coreSource = `(ns clojure.core) -// src/core/errors.ts -class TokenizerError extends Error { - context; - constructor(message, context) { - super(message); - this.name = "TokenizerError"; - this.context = context; - } -} +;; Host shims, for autocomplete only +(def all) +(def async) +(def catch*) +(def then) -class ReaderError extends Error { - context; - pos; - constructor(message, context, pos) { - super(message); - this.name = "ReaderError"; - this.context = context; - this.pos = pos; - } -} +(defmacro defn [name & fdecl] + (let [doc (if (string? (first fdecl)) (first fdecl) nil) + rest-decl (if doc (rest fdecl) fdecl) + arglists (if (vector? (first rest-decl)) + (vector (first rest-decl)) + (reduce (fn [acc arity] (conj acc (first arity))) [] rest-decl)) + meta-map (if doc {:doc doc :arglists arglists} {:arglists arglists})] + \`(def ~(with-meta name meta-map) (fn ~@rest-decl)))) -class EvaluationError extends Error { - context; - pos; - data; - constructor(message, context, pos) { - super(message); - this.name = "EvaluationError"; - this.context = context; - this.pos = pos; - } - static atArg(message, context, argIndex) { - const err = new EvaluationError(message, context); - err.data = { argIndex }; - return err; - } -} -class CljThrownSignal { - value; - constructor(value) { - this.value = value; - } -} +(defn vary-meta + "Returns an object of the same type and value as obj, with + (apply f (meta obj) args) as its metadata." + [obj f & args] + (with-meta obj (apply f (meta obj) args))) -// src/core/factories.ts -var cljNumber = (value) => ({ kind: "number", value }); -var cljString = (value) => ({ kind: "string", value }); -var cljBoolean = (value) => ({ kind: "boolean", value }); -var cljKeyword = (name) => ({ kind: "keyword", name }); -var cljNil = () => ({ kind: "nil", value: null }); -var cljSymbol = (name) => ({ kind: "symbol", name }); -var cljList = (value) => ({ kind: "list", value }); -var cljVector = (value) => ({ kind: "vector", value }); -var cljMap = (entries) => ({ kind: "map", entries }); -var cljMultiArityFunction = (arities, env) => ({ - kind: "function", - arities, - env -}); -var cljNativeFunction = (name, fn) => ({ kind: "native-function", name, fn }); -var cljNativeFunctionWithContext = (name, fn) => ({ - kind: "native-function", - name, - fn: () => { - throw new EvaluationError("Native function called without context", { - name - }); - }, - fnWithContext: fn -}); -var cljMultiArityMacro = (arities, env) => ({ - kind: "macro", - arities, - env -}); -var cljRegex = (pattern, flags = "") => ({ - kind: "regex", - pattern, - flags -}); -var cljVar = (ns, name, value, meta) => ({ kind: "var", ns, name, value, meta }); -var cljAtom = (value) => ({ kind: "atom", value }); -var cljReduced = (value) => ({ - kind: "reduced", - value -}); -var cljVolatile = (value) => ({ - kind: "volatile", - value -}); -var withDoc = (fn, doc, arglists) => ({ - ...fn, - meta: cljMap([ - [cljKeyword(":doc"), cljString(doc)], - ...arglists ? [ - [ - cljKeyword(":arglists"), - cljVector(arglists.map((args) => cljVector(args.map(cljSymbol)))) - ] - ] : [] - ]) -}); -var cljMultiMethod = (name, dispatchFn, methods, defaultMethod) => ({ - kind: "multi-method", - name, - dispatchFn, - methods, - defaultMethod -}); +(defn next + "Returns a seq of the items after the first. Calls seq on its + argument. If there are no more items, returns nil." + [coll] + (seq (rest coll))) -// src/core/env.ts -class EnvError extends Error { - context; - constructor(message, context) { - super(message); - this.context = context; - this.name = "EnvError"; - } -} -function derefValue(val) { - if (val.kind !== "var") - return val; - if (val.dynamic && val.bindingStack && val.bindingStack.length > 0) { - return val.bindingStack[val.bindingStack.length - 1]; - } - return val.value; -} -function makeNamespace(name) { - return { name, vars: new Map, aliases: new Map, readerAliases: new Map }; -} -function makeEnv(outer) { - return { - bindings: new Map, - outer: outer ?? null - }; -} -function lookup(name, env) { - let current = env; - while (current) { - const raw = current.bindings.get(name); - if (raw !== undefined) - return derefValue(raw); - const v = current.ns?.vars.get(name); - if (v !== undefined) - return derefValue(v); - current = current.outer; - } - throw new EvaluationError(`Symbol ${name} not found`, { name }); -} -function tryLookup(name, env) { - let current = env; - while (current) { - const raw = current.bindings.get(name); - if (raw !== undefined) - return derefValue(raw); - const v = current.ns?.vars.get(name); - if (v !== undefined) - return derefValue(v); - current = current.outer; - } - return; -} -function internVar(name, value, nsEnv, meta) { - const ns = nsEnv.ns; - const existing = ns.vars.get(name); - if (existing) { - existing.value = value; - if (meta) - existing.meta = meta; - } else { - ns.vars.set(name, cljVar(ns.name, name, value, meta)); - } -} -function lookupVar(name, env) { - let current = env; - while (current) { - const raw = current.bindings.get(name); - if (raw !== undefined && raw.kind === "var") - return raw; - const v = current.ns?.vars.get(name); - if (v !== undefined) - return v; - current = current.outer; - } - return; -} -function define(name, value, env) { - env.bindings.set(name, value); -} -function extend(params, args, outer) { - if (params.length !== args.length) { - throw new EnvError("Number of parameters and arguments must match", { - params, - args, - outer - }); - } - const env = makeEnv(outer); - for (let i = 0;i < params.length; i++) { - define(params[i], args[i], env); - } - return env; -} -function getRootEnv(env) { - let current = env; - while (current?.outer) { - current = current.outer; - } - return current; -} -function getNamespaceEnv(env) { - let current = env; - while (current) { - if (current.ns) - return current; - current = current.outer; - } - return getRootEnv(env); -} +(defn not + "Returns true if x is logical false, false otherwise." + [x] (if x false true)) -// src/core/positions.ts -function setPos(val, pos) { - Object.defineProperty(val, "_pos", { - value: pos, - enumerable: false, - writable: true, - configurable: true - }); -} -function getPos(val) { - return val._pos; -} -function getLineCol(source, offset) { - const lines = source.split(` -`); - let pos = 0; - for (let i = 0;i < lines.length; i++) { - const lineEnd = pos + lines[i].length; - if (offset <= lineEnd) { - return { line: i + 1, col: offset - pos, lineText: lines[i] }; - } - pos = lineEnd + 1; - } - const last = lines[lines.length - 1]; - return { line: lines.length, col: last.length, lineText: last }; -} -function formatErrorContext(source, pos, opts) { - const { line, col, lineText } = getLineCol(source, pos.start); - const absLine = line + (opts?.lineOffset ?? 0); - const absCol = line === 1 ? col + (opts?.colOffset ?? 0) : col; - const span = Math.max(1, pos.end - pos.start); - const caret = " ".repeat(col) + "^".repeat(span); - return ` - at line ${absLine}, col ${absCol + 1}: - ${lineText} - ${caret}`; -} +(defn second + "Same as (first (next x))" + [coll] + (first (next coll))) -// src/core/evaluator/destructure.ts -function toSeqSafe(value) { - if (value.kind === "nil") - return []; - if (isList(value)) - return value.value; - if (isVector(value)) - return value.value; - throw new EvaluationError(`Cannot destructure ${value.kind} as a sequential collection`, { value }); -} -function findMapEntry(map, key) { - const entry = map.entries.find(([k]) => isEqual(k, key)); - return entry ? entry[1] : undefined; -} -function mapContainsKey(map, key) { - return map.entries.some(([k]) => isEqual(k, key)); -} -function destructureVector(pattern, value, ctx, env) { - const pairs = []; - const elems = [...pattern]; - const asIdx = elems.findIndex((e) => isKeyword(e) && e.kind === "keyword" && e.name === ":as"); - if (asIdx !== -1) { - const asSym = elems[asIdx + 1]; - if (!asSym || !isSymbol(asSym)) { - throw new EvaluationError(":as must be followed by a symbol", { pattern }); - } - pairs.push([asSym.name, value]); - elems.splice(asIdx, 2); - } - const ampIdx = elems.findIndex((e) => isSymbol(e) && e.name === "&"); - let restPattern = null; - let positionalCount; - if (ampIdx !== -1) { - restPattern = elems[ampIdx + 1]; - if (!restPattern) { - throw new EvaluationError("& must be followed by a binding pattern", { pattern }); - } - positionalCount = ampIdx; - elems.splice(ampIdx); - } else { - positionalCount = elems.length; - } - const seq = toSeqSafe(value); - for (let i = 0;i < positionalCount; i++) { - pairs.push(...destructureBindings(elems[i], seq[i] ?? cljNil(), ctx, env)); - } - if (restPattern !== null) { - const restArgs = seq.slice(positionalCount); - let restValue; - if (isMap(restPattern) && restArgs.length > 0) { - const entries = []; - for (let i = 0;i < restArgs.length; i += 2) { - entries.push([restArgs[i], restArgs[i + 1] ?? cljNil()]); - } - restValue = { kind: "map", entries }; - } else { - restValue = restArgs.length > 0 ? cljList(restArgs) : cljNil(); - } - pairs.push(...destructureBindings(restPattern, restValue, ctx, env)); - } - return pairs; -} -function destructureMap(pattern, value, ctx, env) { - const pairs = []; - const orMapVal = findMapEntry(pattern, cljKeyword(":or")); - const orMap = orMapVal && isMap(orMapVal) ? orMapVal : null; - const asVal = findMapEntry(pattern, cljKeyword(":as")); - if (!isMap(value) && value.kind !== "nil") { - throw new EvaluationError(`Cannot destructure ${value.kind} as a map`, { value, pattern }); - } - const targetMap = value.kind === "nil" ? { kind: "map", entries: [] } : value; - for (const [k, v] of pattern.entries) { - if (isKeyword(k) && k.name === ":or") - continue; - if (isKeyword(k) && k.name === ":as") - continue; - if (isKeyword(k) && k.name === ":keys") { - if (!isVector(v)) { - throw new EvaluationError(":keys must be followed by a vector of symbols", { pattern }); - } - for (const sym of v.value) { - if (!isSymbol(sym)) { - throw new EvaluationError(":keys vector must contain symbols", { pattern, sym }); - } - const slashIdx = sym.name.indexOf("/"); - const localName = slashIdx !== -1 ? sym.name.slice(slashIdx + 1) : sym.name; - const lookupKey = cljKeyword(":" + sym.name); - const present2 = mapContainsKey(targetMap, lookupKey); - const entry2 = present2 ? findMapEntry(targetMap, lookupKey) : undefined; - let result; - if (present2) { - result = entry2; - } else if (orMap) { - const orDefault = findMapEntry(orMap, cljSymbol(localName)); - result = orDefault !== undefined ? ctx.evaluate(orDefault, env) : cljNil(); - } else { - result = cljNil(); - } - pairs.push([localName, result]); - } - continue; - } - if (isKeyword(k) && k.name === ":strs") { - if (!isVector(v)) { - throw new EvaluationError(":strs must be followed by a vector of symbols", { pattern }); - } - for (const sym of v.value) { - if (!isSymbol(sym)) { - throw new EvaluationError(":strs vector must contain symbols", { pattern, sym }); - } - const lookupKey = cljString(sym.name); - const present2 = mapContainsKey(targetMap, lookupKey); - const entry2 = present2 ? findMapEntry(targetMap, lookupKey) : undefined; - let result; - if (present2) { - result = entry2; - } else if (orMap) { - const orDefault = findMapEntry(orMap, cljSymbol(sym.name)); - result = orDefault !== undefined ? ctx.evaluate(orDefault, env) : cljNil(); - } else { - result = cljNil(); - } - pairs.push([sym.name, result]); - } - continue; - } - if (isKeyword(k) && k.name === ":syms") { - if (!isVector(v)) { - throw new EvaluationError(":syms must be followed by a vector of symbols", { pattern }); - } - for (const sym of v.value) { - if (!isSymbol(sym)) { - throw new EvaluationError(":syms vector must contain symbols", { pattern, sym }); - } - const lookupKey = cljSymbol(sym.name); - const present2 = mapContainsKey(targetMap, lookupKey); - const entry2 = present2 ? findMapEntry(targetMap, lookupKey) : undefined; - let result; - if (present2) { - result = entry2; - } else if (orMap) { - const orDefault = findMapEntry(orMap, cljSymbol(sym.name)); - result = orDefault !== undefined ? ctx.evaluate(orDefault, env) : cljNil(); - } else { - result = cljNil(); - } - pairs.push([sym.name, result]); - } - continue; - } - const entry = findMapEntry(targetMap, v); - const present = mapContainsKey(targetMap, v); - let boundVal; - if (present) { - boundVal = entry; - } else if (orMap && isSymbol(k)) { - const orDefault = findMapEntry(orMap, cljSymbol(k.name)); - boundVal = orDefault !== undefined ? ctx.evaluate(orDefault, env) : cljNil(); - } else { - boundVal = cljNil(); - } - pairs.push(...destructureBindings(k, boundVal, ctx, env)); - } - if (asVal && isSymbol(asVal)) { - pairs.push([asVal.name, value]); - } - return pairs; -} -function destructureBindings(pattern, value, ctx, env) { - if (isSymbol(pattern)) { - return [[pattern.name, value]]; - } - if (isVector(pattern)) { - return destructureVector(pattern.value, value, ctx, env); - } - if (isMap(pattern)) { - return destructureMap(pattern, value, ctx, env); - } - throw new EvaluationError(`Invalid destructuring pattern: expected symbol, vector, or map, got ${pattern.kind}`, { pattern }); -} -// src/core/evaluator/arity.ts -class RecurSignal { - args; - constructor(args) { - this.args = args; - } -} -function parseParamVector(args, env) { - const ampIdx = args.value.findIndex((a) => isSymbol(a) && a.name === "&"); - let params = []; - let restParam = null; - if (ampIdx === -1) { - params = args.value; - } else { - const ampsCount = args.value.filter((a) => isSymbol(a) && a.name === "&").length; - if (ampsCount > 1) { - throw new EvaluationError("& can only appear once", { args, env }); - } - if (ampIdx !== args.value.length - 2) { - throw new EvaluationError("& must be second-to-last argument", { - args, - env - }); - } - params = args.value.slice(0, ampIdx); - restParam = args.value[ampIdx + 1]; - } - return { params, restParam }; -} -function parseArities(forms, env) { - if (forms.length === 0) { - throw new EvaluationError("fn/defmacro requires at least a parameter vector", { - forms, - env - }); - } - if (isVector(forms[0])) { - const paramVec = forms[0]; - const { params, restParam } = parseParamVector(paramVec, env); - return [{ params, restParam, body: forms.slice(1) }]; - } - if (isList(forms[0])) { - const arities = []; - for (const form of forms) { - if (!isList(form) || form.value.length === 0) { - throw new EvaluationError("Multi-arity clause must be a list starting with a parameter vector", { form, env }); - } - const paramVec = form.value[0]; - if (!isVector(paramVec)) { - throw new EvaluationError("First element of arity clause must be a parameter vector", { paramVec, env }); - } - const { params, restParam } = parseParamVector(paramVec, env); - arities.push({ params, restParam, body: form.value.slice(1) }); - } - const variadicCount = arities.filter((a) => a.restParam !== null).length; - if (variadicCount > 1) { - throw new EvaluationError("At most one variadic arity is allowed per function", { forms, env }); - } - return arities; - } - throw new EvaluationError("fn/defmacro expects a parameter vector or arity clauses", { forms, env }); -} -function bindParams(params, restParam, args, outerEnv, ctx, bindEnv) { - if (restParam === null) { - if (args.length !== params.length) { - throw new EvaluationError(`Arguments length mismatch: fn accepts ${params.length} arguments, but ${args.length} were provided`, { params, args, outerEnv }); - } - } else { - if (args.length < params.length) { - throw new EvaluationError(`Arguments length mismatch: fn expects at least ${params.length} arguments, but ${args.length} were provided`, { params, args, outerEnv }); - } - } - const allPairs = []; - for (let i = 0;i < params.length; i++) { - allPairs.push(...destructureBindings(params[i], args[i], ctx, bindEnv)); - } - if (restParam !== null) { - const restArgs = args.slice(params.length); - let restValue; - if (isMap(restParam) && restArgs.length > 0) { - const entries = []; - for (let i = 0;i < restArgs.length; i += 2) { - entries.push([restArgs[i], restArgs[i + 1] ?? cljNil()]); - } - restValue = { kind: "map", entries }; - } else { - restValue = restArgs.length > 0 ? cljList(restArgs) : cljNil(); - } - allPairs.push(...destructureBindings(restParam, restValue, ctx, bindEnv)); - } - return extend(allPairs.map(([n]) => n), allPairs.map(([, v]) => v), outerEnv); -} -function resolveArity(arities, argCount) { - const exactMatch = arities.find((a) => a.restParam === null && a.params.length === argCount); - if (exactMatch) - return exactMatch; - const variadicMatch = arities.find((a) => a.restParam !== null && argCount >= a.params.length); - if (variadicMatch) - return variadicMatch; - const counts = arities.map((a) => a.restParam ? `${a.params.length}+` : `${a.params.length}`); - throw new EvaluationError(`No matching arity for ${argCount} arguments. Available arities: ${counts.join(", ")}`, { arities, argCount }); -} +(defmacro when + "Executes body when condition is true, otherwise returns nil." + [condition & body] + \`(if ~condition (do ~@body) nil)) -// src/core/gensym.ts -var _counter = 0; -function makeGensym(prefix = "G") { - return `${prefix}__${_counter++}`; -} +(defmacro when-not + "Executes body when condition is false, otherwise returns nil." + [condition & body] + \`(if ~condition nil (do ~@body))) -// src/core/evaluator/quasiquote.ts -function evaluateQuasiquote(form, env, autoGensyms = new Map, ctx) { - switch (form.kind) { - case valueKeywords.vector: - case valueKeywords.list: { - const isAList = isList(form); - if (isAList && form.value.length === 2 && isSymbol(form.value[0]) && form.value[0].name === "unquote") { - return ctx.evaluate(form.value[1], env); - } - const elements = []; - for (const elem of form.value) { - if (isList(elem) && elem.value.length === 2 && isSymbol(elem.value[0]) && elem.value[0].name === "unquote-splicing") { - const toSplice = ctx.evaluate(elem.value[1], env); - if (!isList(toSplice) && !isVector(toSplice)) { - throw new EvaluationError("Unquote-splicing must evaluate to a list or vector", { elem, env }); - } - elements.push(...toSplice.value); - continue; - } - elements.push(evaluateQuasiquote(elem, env, autoGensyms, ctx)); - } - return isAList ? cljList(elements) : cljVector(elements); - } - case valueKeywords.map: { - const entries = []; - for (const [key, value] of form.entries) { - const evaluatedKey = evaluateQuasiquote(key, env, autoGensyms, ctx); - const evaluatedValue = evaluateQuasiquote(value, env, autoGensyms, ctx); - entries.push([evaluatedKey, evaluatedValue]); - } - return cljMap(entries); - } - case valueKeywords.number: - case valueKeywords.string: - case valueKeywords.boolean: - case valueKeywords.keyword: - case valueKeywords.nil: - return form; - case valueKeywords.symbol: { - if (form.name.endsWith("#")) { - if (!autoGensyms.has(form.name)) { - autoGensyms.set(form.name, makeGensym(form.name.slice(0, -1))); - } - return { kind: "symbol", name: autoGensyms.get(form.name) }; - } - return form; - } - default: - throw new EvaluationError(`Unexpected form: ${form.kind}`, { form, env }); - } -} +(defmacro if-let + ([bindings then] \`(if-let ~bindings ~then nil)) + ([bindings then else] + (let [form (first bindings) + tst (second bindings)] + \`(let [~form ~tst] + (if ~form ~then ~else))))) -// src/core/evaluator/recur-check.ts -function assertRecurInTailPosition(body) { - validateForms(body, true); -} -function isRecurForm(form) { - return isList(form) && form.value.length >= 1 && isSymbol(form.value[0]) && form.value[0].name === specialFormKeywords.recur; -} -function validateForms(forms, inTail) { - for (let i = 0;i < forms.length; i++) { - validateForm(forms[i], inTail && i === forms.length - 1); - } -} -function validateForm(form, inTail) { - if (!isList(form)) - return; - if (isRecurForm(form)) { - if (!inTail) { - throw new EvaluationError("Can only recur from tail position", { form }); - } - return; - } - if (form.value.length === 0) - return; - const first = form.value[0]; - if (!isSymbol(first)) { - for (const sub of form.value) - validateForm(sub, false); - return; - } - const name = first.name; - if (name === specialFormKeywords.fn || name === specialFormKeywords.loop || name === specialFormKeywords.quote || name === specialFormKeywords.quasiquote) { - return; - } - if (name === specialFormKeywords.if) { - if (form.value[1]) - validateForm(form.value[1], false); - if (form.value[2]) - validateForm(form.value[2], inTail); - if (form.value[3]) - validateForm(form.value[3], inTail); - return; - } - if (name === specialFormKeywords.do) { - validateForms(form.value.slice(1), inTail); - return; - } - if (name === specialFormKeywords.let) { - const bindings = form.value[1]; - if (isVector(bindings)) { - for (let i = 1;i < bindings.value.length; i += 2) { - validateForm(bindings.value[i], false); - } - } - validateForms(form.value.slice(2), inTail); - return; - } - for (const sub of form.value.slice(1)) { - validateForm(sub, false); - } -} +(defmacro when-let [bindings & body] + (let [form (first bindings) + tst (second bindings)] + \`(let [~form ~tst] + (when ~form ~@body)))) -// src/core/evaluator/special-forms.ts -function hasDynamicMeta(meta) { - if (!meta) - return false; - for (const [k, v] of meta.entries) { - if (k.kind === "keyword" && k.name === ":dynamic" && v.kind === "boolean" && v.value === true) { - return true; - } - } - return false; -} -var specialFormKeywords = { - quote: "quote", - def: "def", - if: "if", - do: "do", - let: "let", - fn: "fn", - defmacro: "defmacro", - quasiquote: "quasiquote", - ns: "ns", - loop: "loop", - recur: "recur", - defmulti: "defmulti", - defmethod: "defmethod", - try: "try", - var: "var", - binding: "binding", - "set!": "set!" -}; -function keywordToDispatchFn(kw) { - return cljNativeFunction(`kw:${kw.name}`, (...args) => { - const target = args[0]; - if (!isMap(target)) - return cljNil(); - const entry = target.entries.find(([k]) => isEqual(k, kw)); - return entry ? entry[1] : cljNil(); - }); -} -function evaluateTry(list, env, ctx) { - const forms = list.value.slice(1); - const bodyForms = []; - const catchClauses = []; - let finallyForms = null; - for (let i = 0;i < forms.length; i++) { - const form = forms[i]; - if (isList(form) && form.value.length > 0 && isSymbol(form.value[0])) { - const head = form.value[0].name; - if (head === "catch") { - if (form.value.length < 3) { - throw new EvaluationError("catch requires a discriminator and a binding symbol", { form, env }); - } - const discriminator = form.value[1]; - const bindingSym = form.value[2]; - if (!isSymbol(bindingSym)) { - throw new EvaluationError("catch binding must be a symbol", { - form, - env - }); - } - catchClauses.push({ - discriminator, - binding: bindingSym.name, - body: form.value.slice(3) - }); - continue; - } - if (head === "finally") { - if (i !== forms.length - 1) { - throw new EvaluationError("finally clause must be the last in try expression", { - form, - env - }); - } - finallyForms = form.value.slice(1); - continue; - } - } - bodyForms.push(form); - } - function matchesDiscriminator(discriminator, thrown) { - const disc = ctx.evaluate(discriminator, env); - if (isKeyword(disc)) { - if (disc.name === ":default") - return true; - if (!isMap(thrown)) - return false; - const typeEntry = thrown.entries.find(([k]) => isKeyword(k) && k.name === ":type"); - if (!typeEntry) - return false; - return isEqual(typeEntry[1], disc); - } - if (isAFunction(disc)) { - const result2 = ctx.applyFunction(disc, [thrown], env); - return isTruthy(result2); - } - throw new EvaluationError("catch discriminator must be a keyword or a predicate function", { discriminator: disc, env }); - } - let result = cljNil(); - let pendingThrow = null; - try { - result = ctx.evaluateForms(bodyForms, env); - } catch (e) { - if (e instanceof RecurSignal) - throw e; - let thrownValue; - if (e instanceof CljThrownSignal) { - thrownValue = e.value; - } else if (e instanceof EvaluationError) { - thrownValue = cljMap([ - [cljKeyword(":type"), cljKeyword(":error/runtime")], - [cljKeyword(":message"), cljString(e.message)] - ]); - } else { - throw e; - } - let handled = false; - for (const clause of catchClauses) { - if (matchesDiscriminator(clause.discriminator, thrownValue)) { - const catchEnv = extend([clause.binding], [thrownValue], env); - result = ctx.evaluateForms(clause.body, catchEnv); - handled = true; - break; - } - } - if (!handled) { - pendingThrow = e; - } - } finally { - if (finallyForms) { - ctx.evaluateForms(finallyForms, env); - } - } - if (pendingThrow !== null) - throw pendingThrow; - return result; -} -function evaluateQuote(list, _env, _ctx) { - return list.value[1]; -} -function evalQuasiquote(list, env, ctx) { - return evaluateQuasiquote(list.value[1], env, new Map, ctx); -} -function buildVarMeta(symMeta, ctx, nameVal) { - const pos = nameVal ? getPos(nameVal) : undefined; - const hasPosInfo = pos && ctx.currentSource; - if (!symMeta && !hasPosInfo) - return; - const posEntries = []; - if (hasPosInfo) { - const { line, col } = getLineCol(ctx.currentSource, pos.start); - const lineOffset = ctx.currentLineOffset ?? 0; - const colOffset = ctx.currentColOffset ?? 0; - posEntries.push([cljKeyword(":line"), cljNumber(line + lineOffset)]); - posEntries.push([cljKeyword(":column"), cljNumber(line === 1 ? col + colOffset : col)]); - if (ctx.currentFile) { - posEntries.push([cljKeyword(":file"), cljString(ctx.currentFile)]); - } - } - const POS_KEYS = new Set([":line", ":column", ":file"]); - const baseEntries = (symMeta?.entries ?? []).filter(([k]) => !(k.kind === "keyword" && POS_KEYS.has(k.name))); - const allEntries = [...baseEntries, ...posEntries]; - return allEntries.length > 0 ? cljMap(allEntries) : undefined; -} -function evaluateDef(list, env, ctx) { - const name = list.value[1]; - if (name.kind !== "symbol") { - throw new EvaluationError("First element of list must be a symbol", { - name, - list, - env - }); - } - if (list.value[2] === undefined) - return cljNil(); - const nsEnv = getNamespaceEnv(env); - const cljNs = nsEnv.ns; - const newValue = ctx.evaluate(list.value[2], env); - const varMeta = buildVarMeta(name.meta, ctx, name); - const existing = cljNs.vars.get(name.name); - if (existing) { - existing.value = newValue; - if (varMeta) { - existing.meta = varMeta; - if (hasDynamicMeta(varMeta)) - existing.dynamic = true; - } - } else { - const v = cljVar(cljNs.name, name.name, newValue, varMeta); - if (hasDynamicMeta(varMeta)) - v.dynamic = true; - cljNs.vars.set(name.name, v); - } - return cljNil(); -} -var evaluateNs = (_list, _env, _ctx) => { - return cljNil(); -}; -function evaluateIf(list, env, ctx) { - const condition = ctx.evaluate(list.value[1], env); - if (!isFalsy(condition)) { - return ctx.evaluate(list.value[2], env); - } - if (!list.value[3]) { - return cljNil(); - } - return ctx.evaluate(list.value[3], env); -} -function evaluateDo(list, env, ctx) { - return ctx.evaluateForms(list.value.slice(1), env); -} -function evaluateLet(list, env, ctx) { - const bindings = list.value[1]; - if (!isVector(bindings)) { - throw new EvaluationError("Bindings must be a vector", { - bindings, - env - }); - } - if (bindings.value.length % 2 !== 0) { - throw new EvaluationError("Bindings must be a balanced pair of keys and values", { bindings, env }); - } - const body = list.value.slice(2); - let localEnv = env; - for (let i = 0;i < bindings.value.length; i += 2) { - const pattern = bindings.value[i]; - const value = ctx.evaluate(bindings.value[i + 1], localEnv); - const pairs = destructureBindings(pattern, value, ctx, localEnv); - localEnv = extend(pairs.map(([n]) => n), pairs.map(([, v]) => v), localEnv); - } - return ctx.evaluateForms(body, localEnv); -} -function evaluateFn(list, env, _ctx) { - const arities = parseArities(list.value.slice(1), env); - for (const arity of arities) { - assertRecurInTailPosition(arity.body); - } - return cljMultiArityFunction(arities, env); -} -function evaluateDefmacro(list, env, _ctx) { - const name = list.value[1]; - if (!isSymbol(name)) { - throw new EvaluationError("First element of defmacro must be a symbol", { - name, - list, - env - }); - } - const arities = parseArities(list.value.slice(2), env); - const macro = cljMultiArityMacro(arities, env); - internVar(name.name, macro, getNamespaceEnv(env)); - return cljNil(); -} -function evaluateLoop(list, env, ctx) { - const loopBindings = list.value[1]; - if (!isVector(loopBindings)) { - throw new EvaluationError("loop bindings must be a vector", { - loopBindings, - env - }); - } - if (loopBindings.value.length % 2 !== 0) { - throw new EvaluationError("loop bindings must be a balanced pair of keys and values", { loopBindings, env }); - } - const loopBody = list.value.slice(2); - assertRecurInTailPosition(loopBody); - const patterns = []; - const initValues = []; - let initEnv = env; - for (let i = 0;i < loopBindings.value.length; i += 2) { - const pattern = loopBindings.value[i]; - const value = ctx.evaluate(loopBindings.value[i + 1], initEnv); - patterns.push(pattern); - initValues.push(value); - const pairs = destructureBindings(pattern, value, ctx, initEnv); - initEnv = extend(pairs.map(([n]) => n), pairs.map(([, v]) => v), initEnv); - } - let currentValues = initValues; - while (true) { - let loopEnv = env; - for (let i = 0;i < patterns.length; i++) { - const pairs = destructureBindings(patterns[i], currentValues[i], ctx, loopEnv); - loopEnv = extend(pairs.map(([n]) => n), pairs.map(([, v]) => v), loopEnv); - } - try { - return ctx.evaluateForms(loopBody, loopEnv); - } catch (e) { - if (e instanceof RecurSignal) { - if (e.args.length !== patterns.length) { - throw new EvaluationError(`recur expects ${patterns.length} arguments but got ${e.args.length}`, { list, env }); - } - currentValues = e.args; - continue; - } - throw e; - } - } -} -function evaluateRecur(list, env, ctx) { - const args = list.value.slice(1).map((v) => ctx.evaluate(v, env)); - throw new RecurSignal(args); -} -function evaluateDefmulti(list, env, ctx) { - const mmName = list.value[1]; - if (!isSymbol(mmName)) { - throw new EvaluationError("defmulti: first argument must be a symbol", { - list, - env - }); - } - const dispatchFnExpr = list.value[2]; - let dispatchFn; - if (isKeyword(dispatchFnExpr)) { - dispatchFn = keywordToDispatchFn(dispatchFnExpr); - } else { - const evaluated = ctx.evaluate(dispatchFnExpr, env); - if (!isAFunction(evaluated)) { - throw new EvaluationError("defmulti: dispatch-fn must be a function or keyword", { list, env }); - } - dispatchFn = evaluated; - } - const mm = cljMultiMethod(mmName.name, dispatchFn, []); - internVar(mmName.name, mm, getNamespaceEnv(env)); - return cljNil(); -} -function evaluateDefmethod(list, env, ctx) { - const mmName = list.value[1]; - if (!isSymbol(mmName)) { - throw new EvaluationError("defmethod: first argument must be a symbol", { - list, - env - }); - } - const dispatchVal = ctx.evaluate(list.value[2], env); - const existing = lookup(mmName.name, env); - if (!isMultiMethod(existing)) { - throw new EvaluationError(`defmethod: ${mmName.name} is not a multimethod`, { list, env }); - } - const arities = parseArities([list.value[3], ...list.value.slice(4)], env); - const methodFn = cljMultiArityFunction(arities, env); - const isDefault = isKeyword(dispatchVal) && dispatchVal.name === ":default"; - let updated; - if (isDefault) { - updated = cljMultiMethod(existing.name, existing.dispatchFn, existing.methods, methodFn); - } else { - const filtered = existing.methods.filter((m) => !isEqual(m.dispatchVal, dispatchVal)); - updated = cljMultiMethod(existing.name, existing.dispatchFn, [ - ...filtered, - { dispatchVal, fn: methodFn } - ]); - } - const v = lookupVar(mmName.name, env); - if (v) { - v.value = updated; - } else { - define(mmName.name, updated, getNamespaceEnv(env)); - } - return cljNil(); -} -function evaluateVar(list, env, _ctx) { - const sym = list.value[1]; - if (!isSymbol(sym)) { - throw new EvaluationError("var expects a symbol", { list }); - } - const slashIdx = sym.name.indexOf("/"); - if (slashIdx > 0 && slashIdx < sym.name.length - 1) { - const alias = sym.name.slice(0, slashIdx); - const localName = sym.name.slice(slashIdx + 1); - const nsEnv = getNamespaceEnv(env); - const aliasCljNs = nsEnv.ns?.aliases.get(alias); - if (aliasCljNs) { - const v3 = aliasCljNs.vars.get(localName); - if (!v3) - throw new EvaluationError(`Var ${sym.name} not found`, { sym }); - return v3; - } - const targetEnv = getRootEnv(env).resolveNs?.(alias) ?? null; - if (!targetEnv) { - throw new EvaluationError(`No such namespace: ${alias}`, { sym }); - } - const v2 = lookupVar(localName, targetEnv); - if (!v2) - throw new EvaluationError(`Var ${sym.name} not found`, { sym }); - return v2; - } - const v = lookupVar(sym.name, env); - if (!v) { - throw new EvaluationError(`Unable to resolve var: ${sym.name} in this context`, { sym }); - } - return v; -} -function evaluateBinding(list, env, ctx) { - const bindings = list.value[1]; - if (!isVector(bindings)) { - throw new EvaluationError("binding requires a vector of bindings", { - list, - env - }); - } - if (bindings.value.length % 2 !== 0) { - throw new EvaluationError("binding vector must have an even number of forms", { list, env }); - } - const body = list.value.slice(2); - const boundVars = []; - for (let i = 0;i < bindings.value.length; i += 2) { - const sym = bindings.value[i]; - if (!isSymbol(sym)) { - throw new EvaluationError("binding left-hand side must be a symbol", { sym }); - } - const newVal = ctx.evaluate(bindings.value[i + 1], env); - const v = lookupVar(sym.name, env); - if (!v) { - throw new EvaluationError(`No var found for symbol '${sym.name}' in binding form`, { sym }); - } - if (!v.dynamic) { - throw new EvaluationError(`Cannot use binding with non-dynamic var ${v.ns}/${v.name}. Mark it dynamic with (def ^:dynamic ${sym.name} ...)`, { sym }); - } - v.bindingStack ??= []; - v.bindingStack.push(newVal); - boundVars.push(v); - } - try { - return ctx.evaluateForms(body, env); - } finally { - for (const v of boundVars) { - v.bindingStack.pop(); - } - } -} -function evaluateSet(list, env, ctx) { - if (list.value.length !== 3) { - throw new EvaluationError(`set! requires exactly 2 arguments, got ${list.value.length - 1}`, { list, env }); - } - const symForm = list.value[1]; - if (!isSymbol(symForm)) { - throw new EvaluationError(`set! first argument must be a symbol, got ${symForm.kind}`, { symForm, env }); - } - const v = lookupVar(symForm.name, env); - if (!v) { - throw new EvaluationError(`Unable to resolve var: ${symForm.name} in this context`, { symForm, env }); - } - if (!v.dynamic) { - throw new EvaluationError(`Cannot set! non-dynamic var ${v.ns}/${v.name}. Mark it with ^:dynamic.`, { symForm, env }); - } - if (!v.bindingStack || v.bindingStack.length === 0) { - throw new EvaluationError(`Cannot set! ${v.ns}/${v.name} — no active binding. Use set! only inside a (binding [...] ...) form.`, { symForm, env }); - } - const newVal = ctx.evaluate(list.value[2], env); - v.bindingStack[v.bindingStack.length - 1] = newVal; - return newVal; -} -var specialFormEvaluatorEntries = { - try: evaluateTry, - quote: evaluateQuote, - quasiquote: evalQuasiquote, - def: evaluateDef, - ns: evaluateNs, - if: evaluateIf, - do: evaluateDo, - let: evaluateLet, - fn: evaluateFn, - defmacro: evaluateDefmacro, - loop: evaluateLoop, - recur: evaluateRecur, - defmulti: evaluateDefmulti, - defmethod: evaluateDefmethod, - var: evaluateVar, - binding: evaluateBinding, - "set!": evaluateSet -}; -function evaluateSpecialForm(symbol, list, env, ctx) { - const evalFn = specialFormEvaluatorEntries[symbol]; - if (evalFn) { - return evalFn(list, env, ctx); - } - throw new EvaluationError(`Unknown special form: ${symbol}`, { - symbol, - list, - env - }); -} - -// src/core/assertions.ts -var isNil = (value) => value.kind === "nil"; -var isFalsy = (value) => { - if (value.kind === "nil") - return true; - if (value.kind === "boolean") - return !value.value; - return false; -}; -var isTruthy = (value) => { - return !isFalsy(value); -}; -var isSpecialForm = (value) => value.kind === "symbol" && (value.name in specialFormKeywords); -var isSymbol = (value) => value.kind === "symbol"; -var isVector = (value) => value.kind === "vector"; -var isList = (value) => value.kind === "list"; -var isFunction = (value) => value.kind === "function"; -var isNativeFunction = (value) => value.kind === "native-function"; -var isMacro = (value) => value.kind === "macro"; -var isMap = (value) => value.kind === "map"; -var isKeyword = (value) => value.kind === "keyword"; -var isAFunction = (value) => isFunction(value) || isNativeFunction(value); -var isCallable = (value) => isAFunction(value) || isKeyword(value) || isMap(value); -var isMultiMethod = (value) => value.kind === "multi-method"; -var isAtom = (value) => value.kind === "atom"; -var isReduced = (value) => value.kind === "reduced"; -var isVolatile = (value) => value.kind === "volatile"; -var isRegex = (value) => value.kind === "regex"; -var isVar = (value) => value.kind === "var"; -var isCollection = (value) => isVector(value) || isMap(value) || isList(value); -var isSeqable = (value) => isCollection(value) || value.kind === "string"; -var equalityHandlers = { - [valueKeywords.number]: (a, b) => a.value === b.value, - [valueKeywords.string]: (a, b) => a.value === b.value, - [valueKeywords.boolean]: (a, b) => a.value === b.value, - [valueKeywords.nil]: () => true, - [valueKeywords.symbol]: (a, b) => a.name === b.name, - [valueKeywords.keyword]: (a, b) => a.name === b.name, - [valueKeywords.vector]: (a, b) => { - if (a.value.length !== b.value.length) - return false; - return a.value.every((value, index) => isEqual(value, b.value[index])); - }, - [valueKeywords.map]: (a, b) => { - if (a.entries.length !== b.entries.length) - return false; - const uniqueKeys = new Set([ - ...a.entries.map(([key]) => key), - ...b.entries.map(([key]) => key) - ]); - for (const key of uniqueKeys) { - const aEntry = a.entries.find(([k]) => isEqual(k, key)); - if (!aEntry) - return false; - const bEntry = b.entries.find(([k]) => isEqual(k, key)); - if (!bEntry) - return false; - if (!isEqual(aEntry[1], bEntry[1])) - return false; - } - return true; - }, - [valueKeywords.list]: (a, b) => { - if (a.value.length !== b.value.length) - return false; - return a.value.every((value, index) => isEqual(value, b.value[index])); - }, - [valueKeywords.atom]: (a, b) => a === b, - [valueKeywords.reduced]: (a, b) => isEqual(a.value, b.value), - [valueKeywords.volatile]: (a, b) => a === b, - [valueKeywords.regex]: (a, b) => a === b, - [valueKeywords.var]: (a, b) => a === b -}; -var isEqual = (a, b) => { - if (a.kind !== b.kind) - return false; - const handler = equalityHandlers[a.kind]; - if (!handler) - return false; - return handler(a, b); -}; - -// src/core/printer.ts -function printString(value) { - switch (value.kind) { - case valueKeywords.number: - return value.value.toString(); - case valueKeywords.string: - let escapedBuffer = ""; - for (const char of value.value) { - switch (char) { - case '"': - escapedBuffer += "\\\""; - break; - case "\\": - escapedBuffer += "\\\\"; - break; - case ` -`: - escapedBuffer += "\\n"; - break; - case "\r": - escapedBuffer += "\\r"; - break; - case "\t": - escapedBuffer += "\\t"; - break; - default: - escapedBuffer += char; - } - } - return `"${escapedBuffer}"`; - case valueKeywords.boolean: - return value.value ? "true" : "false"; - case valueKeywords.nil: - return "nil"; - case valueKeywords.keyword: - return `${value.name}`; - case valueKeywords.symbol: - return `${value.name}`; - case valueKeywords.list: - return `(${value.value.map(printString).join(" ")})`; - case valueKeywords.vector: - return `[${value.value.map(printString).join(" ")}]`; - case valueKeywords.map: - return `{${value.entries.map(([key, value2]) => `${printString(key)} ${printString(value2)}`).join(" ")}}`; - case valueKeywords.function: { - if (value.arities.length === 1) { - const a = value.arities[0]; - const params = a.restParam ? [...a.params, { kind: "symbol", name: "&" }, a.restParam] : a.params; - return `(fn [${params.map(printString).join(" ")}] ${a.body.map(printString).join(" ")})`; - } - const clauses = value.arities.map((a) => { - const params = a.restParam ? [...a.params, { kind: "symbol", name: "&" }, a.restParam] : a.params; - return `([${params.map(printString).join(" ")}] ${a.body.map(printString).join(" ")})`; - }); - return `(fn ${clauses.join(" ")})`; - } - case valueKeywords.nativeFunction: - return `(native-fn ${value.name})`; - case valueKeywords.multiMethod: - return `(multi-method ${value.name})`; - case valueKeywords.atom: - return `#`; - case valueKeywords.reduced: - return `#`; - case valueKeywords.volatile: - return `#`; - case valueKeywords.regex: { - const escaped = value.pattern.replace(/"/g, "\\\""); - const prefix = value.flags ? `(?${value.flags})` : ""; - return `#"${prefix}${escaped}"`; - } - case valueKeywords.var: - return `#'${value.ns}/${value.name}`; - default: - throw new EvaluationError(`unhandled value type: ${value.kind}`, { - value - }); - } -} -function joinLines(lines) { - return lines.join(` -`); -} - -// src/core/stdlib/arithmetic.ts -var arithmeticFunctions = { - "+": withDoc(cljNativeFunction("+", function add(...nums) { - if (nums.length === 0) { - return cljNumber(0); - } - const badIdx = nums.findIndex(function isNotNumber(a) { - return a.kind !== "number"; - }); - if (badIdx !== -1) { - throw EvaluationError.atArg("+ expects all arguments to be numbers", { args: nums }, badIdx); - } - return nums.reduce(function sumNumbers(acc, arg) { - return cljNumber(acc.value + arg.value); - }, cljNumber(0)); - }), "Returns the sum of the arguments. Throws on non-number arguments.", [["&", "nums"]]), - "-": withDoc(cljNativeFunction("-", function subtract(...nums) { - if (nums.length === 0) { - throw new EvaluationError("- expects at least one argument", { - args: nums - }); - } - const badIdx = nums.findIndex(function isNotNumber(a) { - return a.kind !== "number"; - }); - if (badIdx !== -1) { - throw EvaluationError.atArg("- expects all arguments to be numbers", { args: nums }, badIdx); - } - return nums.slice(1).reduce(function subtractNumbers(acc, arg) { - return cljNumber(acc.value - arg.value); - }, nums[0]); - }), "Returns the difference of the arguments. Throws on non-number arguments.", [["&", "nums"]]), - "*": withDoc(cljNativeFunction("*", function multiply(...nums) { - if (nums.length === 0) { - return cljNumber(1); - } - const badIdx = nums.findIndex(function isNotNumber(a) { - return a.kind !== "number"; - }); - if (badIdx !== -1) { - throw EvaluationError.atArg("* expects all arguments to be numbers", { args: nums }, badIdx); - } - return nums.slice(1).reduce(function multiplyNumbers(acc, arg) { - return cljNumber(acc.value * arg.value); - }, nums[0]); - }), "Returns the product of the arguments. Throws on non-number arguments.", [["&", "nums"]]), - "/": withDoc(cljNativeFunction("/", function divide(...nums) { - if (nums.length === 0) { - throw new EvaluationError("/ expects at least one argument", { - args: nums - }); - } - const badIdx = nums.findIndex(function isNotNumber(a) { - return a.kind !== "number"; - }); - if (badIdx !== -1) { - throw EvaluationError.atArg("/ expects all arguments to be numbers", { args: nums }, badIdx); - } - return nums.slice(1).reduce(function divideNumbers(acc, arg, reduceIdx) { - if (arg.value === 0) { - const err = new EvaluationError("division by zero", { args: nums }); - err.data = { argIndex: reduceIdx + 1 }; - throw err; - } - return cljNumber(acc.value / arg.value); - }, nums[0]); - }), "Returns the quotient of the arguments. Throws on non-number arguments or division by zero.", [["&", "nums"]]), - ">": withDoc(cljNativeFunction(">", function greaterThan(...nums) { - if (nums.length < 2) { - throw new EvaluationError("> expects at least two arguments", { - args: nums - }); - } - const badIdx = nums.findIndex(function isNotNumber(a) { - return a.kind !== "number"; - }); - if (badIdx !== -1) { - throw EvaluationError.atArg("> expects all arguments to be numbers", { args: nums }, badIdx); - } - for (let i = 1;i < nums.length; i++) { - if (nums[i].value >= nums[i - 1].value) { - return cljBoolean(false); - } - } - return cljBoolean(true); - }), "Compares adjacent arguments left to right, returns true if all values are in ascending order, false otherwise.", [["&", "nums"]]), - "<": withDoc(cljNativeFunction("<", function lessThan(...nums) { - if (nums.length < 2) { - throw new EvaluationError("< expects at least two arguments", { - args: nums - }); - } - const badIdx = nums.findIndex(function isNotNumber(a) { - return a.kind !== "number"; - }); - if (badIdx !== -1) { - throw EvaluationError.atArg("< expects all arguments to be numbers", { args: nums }, badIdx); - } - for (let i = 1;i < nums.length; i++) { - if (nums[i].value <= nums[i - 1].value) { - return cljBoolean(false); - } - } - return cljBoolean(true); - }), "Compares adjacent arguments left to right, returns true if all values are in descending order, false otherwise.", [["&", "nums"]]), - ">=": withDoc(cljNativeFunction(">=", function greaterThanOrEqual(...nums) { - if (nums.length < 2) { - throw new EvaluationError(">= expects at least two arguments", { - args: nums - }); - } - const badIdx = nums.findIndex(function isNotNumber(a) { - return a.kind !== "number"; - }); - if (badIdx !== -1) { - throw EvaluationError.atArg(">= expects all arguments to be numbers", { args: nums }, badIdx); - } - for (let i = 1;i < nums.length; i++) { - if (nums[i].value > nums[i - 1].value) { - return cljBoolean(false); - } - } - return cljBoolean(true); - }), "Compares adjacent arguments left to right, returns true if all comparisons returns true for greater than or equal to checks, false otherwise.", [["&", "nums"]]), - "<=": withDoc(cljNativeFunction("<=", function lessThanOrEqual(...nums) { - if (nums.length < 2) { - throw new EvaluationError("<= expects at least two arguments", { - args: nums - }); - } - const badIdx = nums.findIndex(function isNotNumber(a) { - return a.kind !== "number"; - }); - if (badIdx !== -1) { - throw EvaluationError.atArg("<= expects all arguments to be numbers", { args: nums }, badIdx); - } - for (let i = 1;i < nums.length; i++) { - if (nums[i].value < nums[i - 1].value) { - return cljBoolean(false); - } - } - return cljBoolean(true); - }), "Compares adjacent arguments left to right, returns true if all comparisons returns true for less than or equal to checks, false otherwise.", [["&", "nums"]]), - "=": withDoc(cljNativeFunction("=", function equals(...vals) { - if (vals.length < 2) { - throw new EvaluationError("= expects at least two arguments", { - args: vals - }); - } - for (let i = 1;i < vals.length; i++) { - if (!isEqual(vals[i], vals[i - 1])) { - return cljBoolean(false); - } - } - return cljBoolean(true); - }), "Compares adjacent arguments left to right, returns true if all values are structurally equal, false otherwise.", [["&", "vals"]]), - inc: withDoc(cljNativeFunction("inc", function increment(x) { - if (x === undefined || x.kind !== "number") { - throw EvaluationError.atArg(`inc expects a number${x !== undefined ? `, got ${printString(x)}` : ""}`, { x }, 0); - } - return cljNumber(x.value + 1); - }), "Returns the argument incremented by 1. Throws on non-number arguments.", [["x"]]), - dec: withDoc(cljNativeFunction("dec", function decrement(x) { - if (x === undefined || x.kind !== "number") { - throw EvaluationError.atArg(`dec expects a number${x !== undefined ? `, got ${printString(x)}` : ""}`, { x }, 0); - } - return cljNumber(x.value - 1); - }), "Returns the argument decremented by 1. Throws on non-number arguments.", [["x"]]), - max: withDoc(cljNativeFunction("max", function maximum(...nums) { - if (nums.length === 0) { - throw new EvaluationError("max expects at least one argument", { - args: nums - }); - } - const badIdx = nums.findIndex(function isNotNumber(a) { - return a.kind !== "number"; - }); - if (badIdx !== -1) { - throw EvaluationError.atArg("max expects all arguments to be numbers", { args: nums }, badIdx); - } - return nums.reduce(function findMax(best, arg) { - return arg.value > best.value ? arg : best; - }); - }), "Returns the largest of the arguments. Throws on non-number arguments.", [["&", "nums"]]), - min: withDoc(cljNativeFunction("min", function minimum(...nums) { - if (nums.length === 0) { - throw new EvaluationError("min expects at least one argument", { - args: nums - }); - } - const badIdx = nums.findIndex(function isNotNumber(a) { - return a.kind !== "number"; - }); - if (badIdx !== -1) { - throw EvaluationError.atArg("min expects all arguments to be numbers", { args: nums }, badIdx); - } - return nums.reduce(function findMin(best, arg) { - return arg.value < best.value ? arg : best; - }); - }), "Returns the smallest of the arguments. Throws on non-number arguments.", [["&", "nums"]]), - mod: withDoc(cljNativeFunction("mod", function modulo(n, d) { - if (n === undefined || n.kind !== "number") { - throw EvaluationError.atArg(`mod expects a number as first argument${n !== undefined ? `, got ${printString(n)}` : ""}`, { n }, 0); - } - if (d === undefined || d.kind !== "number") { - throw EvaluationError.atArg(`mod expects a number as second argument${d !== undefined ? `, got ${printString(d)}` : ""}`, { d }, 1); - } - if (d.value === 0) { - const err = new EvaluationError("mod: division by zero", { n, d }); - err.data = { argIndex: 1 }; - throw err; - } - const result = n.value % d.value; - return cljNumber(result < 0 ? result + Math.abs(d.value) : result); - }), "Returns the remainder of the first argument divided by the second argument. Throws on non-number arguments or division by zero.", [["n", "d"]]), - "even?": withDoc(cljNativeFunction("even?", function isEven(n) { - if (n === undefined || n.kind !== "number") { - throw EvaluationError.atArg(`even? expects a number${n !== undefined ? `, got ${printString(n)}` : ""}`, { n }, 0); - } - return cljBoolean(n.value % 2 === 0); - }), "Returns true if the argument is an even number, false otherwise.", [["n"]]), - "odd?": withDoc(cljNativeFunction("odd?", function isOdd(n) { - if (n === undefined || n.kind !== "number") { - throw EvaluationError.atArg(`odd? expects a number${n !== undefined ? `, got ${printString(n)}` : ""}`, { n }, 0); - } - return cljBoolean(Math.abs(n.value) % 2 !== 0); - }), "Returns true if the argument is an odd number, false otherwise.", [["n"]]), - "pos?": withDoc(cljNativeFunction("pos?", function isPositive(n) { - if (n === undefined || n.kind !== "number") { - throw EvaluationError.atArg(`pos? expects a number${n !== undefined ? `, got ${printString(n)}` : ""}`, { n }, 0); - } - return cljBoolean(n.value > 0); - }), "Returns true if the argument is a positive number, false otherwise.", [["n"]]), - "neg?": withDoc(cljNativeFunction("neg?", function isNegative(n) { - if (n === undefined || n.kind !== "number") { - throw EvaluationError.atArg(`neg? expects a number${n !== undefined ? `, got ${printString(n)}` : ""}`, { n }, 0); - } - return cljBoolean(n.value < 0); - }), "Returns true if the argument is a negative number, false otherwise.", [["n"]]), - "zero?": withDoc(cljNativeFunction("zero?", function isZero(n) { - if (n === undefined || n.kind !== "number") { - throw EvaluationError.atArg(`zero? expects a number${n !== undefined ? `, got ${printString(n)}` : ""}`, { n }, 0); - } - return cljBoolean(n.value === 0); - }), "Returns true if the argument is zero, false otherwise.", [["n"]]) -}; - -// src/core/stdlib/atoms.ts -var atomFunctions = { - atom: withDoc(cljNativeFunction("atom", function atom(value) { - return cljAtom(value); - }), "Returns a new atom holding the given value.", [["value"]]), - deref: withDoc(cljNativeFunction("deref", function deref(value) { - if (isAtom(value)) - return value.value; - if (isVolatile(value)) - return value.value; - if (isReduced(value)) - return value.value; - throw EvaluationError.atArg(`deref expects an atom, volatile, or reduced value, got ${value.kind}`, { value }, 0); - }), "Returns the wrapped value from an atom, volatile, or reduced value.", [["value"]]), - "swap!": withDoc(cljNativeFunctionWithContext("swap!", function swap(ctx, callEnv, atomVal, fn, ...extraArgs) { - if (!isAtom(atomVal)) { - throw EvaluationError.atArg(`swap! expects an atom as its first argument, got ${atomVal.kind}`, { atomVal }, 0); - } - if (!isAFunction(fn)) { - throw EvaluationError.atArg(`swap! expects a function as its second argument, got ${fn.kind}`, { fn }, 1); - } - const newVal = ctx.applyFunction(fn, [atomVal.value, ...extraArgs], callEnv); - atomVal.value = newVal; - return newVal; - }), "Applies fn to the current value of the atom, replacing the current value with the result. Returns the new value.", [["atomVal", "fn", "&", "extraArgs"]]), - "reset!": withDoc(cljNativeFunction("reset!", function reset(atomVal, newVal) { - if (!isAtom(atomVal)) { - throw EvaluationError.atArg(`reset! expects an atom as its first argument, got ${atomVal.kind}`, { atomVal }, 0); - } - atomVal.value = newVal; - return newVal; - }), "Sets the value of the atom to newVal and returns the new value.", [["atomVal", "newVal"]]), - "atom?": withDoc(cljNativeFunction("atom?", function isAtomPredicate(value) { - return cljBoolean(isAtom(value)); - }), "Returns true if the value is an atom, false otherwise.", [["value"]]) -}; - -// src/core/transformations.ts -function valueToString(value) { - switch (value.kind) { - case valueKeywords.string: - return value.value; - case valueKeywords.number: - return value.value.toString(); - case valueKeywords.boolean: - return value.value ? "true" : "false"; - case valueKeywords.keyword: - return value.name; - case valueKeywords.symbol: - return value.name; - case valueKeywords.list: - return `(${value.value.map(valueToString).join(" ")})`; - case valueKeywords.vector: - return `[${value.value.map(valueToString).join(" ")}]`; - case valueKeywords.map: - return `{${value.entries.map(([key, value2]) => `${valueToString(key)} ${valueToString(value2)}`).join(" ")}}`; - case valueKeywords.function: { - if (value.arities.length === 1) { - const a = value.arities[0]; - const params = a.restParam ? [...a.params, { kind: "symbol", name: "&" }, a.restParam] : a.params; - return `(fn [${params.map(valueToString).join(" ")}] ${a.body.map(valueToString).join(" ")})`; - } - const clauses = value.arities.map((a) => { - const params = a.restParam ? [...a.params, { kind: "symbol", name: "&" }, a.restParam] : a.params; - return `([${params.map(valueToString).join(" ")}] ${a.body.map(valueToString).join(" ")})`; - }); - return `(fn ${clauses.join(" ")})`; - } - case valueKeywords.nativeFunction: - return `(native-fn ${value.name})`; - case valueKeywords.nil: - return "nil"; - case valueKeywords.regex: { - const prefix = value.flags ? `(?${value.flags})` : ""; - return `${prefix}${value.pattern}`; - } - default: - throw new EvaluationError(`unhandled value type: ${value.kind}`, { - value - }); - } -} -var toSeq = (collection) => { - if (isList(collection)) { - return collection.value; - } - if (isVector(collection)) { - return collection.value; - } - if (isMap(collection)) { - return collection.entries.map(([k, v]) => cljVector([k, v])); - } - if (collection.kind === "string") { - return [...collection.value].map(cljString); - } - throw new EvaluationError(`toSeq expects a collection or string, got ${printString(collection)}`, { collection }); -}; - -// src/core/stdlib/collections.ts -var collectionFunctions = { - list: withDoc(cljNativeFunction("list", function listImpl(...args) { - if (args.length === 0) { - return cljList([]); - } - return cljList(args); - }), "Returns a new list containing the given values.", [["&", "args"]]), - vector: withDoc(cljNativeFunction("vector", function vectorImpl(...args) { - if (args.length === 0) { - return cljVector([]); - } - return cljVector(args); - }), "Returns a new vector containing the given values.", [["&", "args"]]), - "hash-map": withDoc(cljNativeFunction("hash-map", function hashMapImpl(...kvals) { - if (kvals.length === 0) { - return cljMap([]); - } - if (kvals.length % 2 !== 0) { - throw new EvaluationError(`hash-map expects an even number of arguments, got ${kvals.length}`, { args: kvals }); - } - const entries = []; - for (let i = 0;i < kvals.length; i += 2) { - const key = kvals[i]; - const value = kvals[i + 1]; - entries.push([key, value]); - } - return cljMap(entries); - }), "Returns a new hash-map containing the given key-value pairs.", [["&", "kvals"]]), - seq: withDoc(cljNativeFunction("seq", function seqImpl(coll) { - if (coll.kind === "nil") - return cljNil(); - if (!isSeqable(coll)) { - throw EvaluationError.atArg(`seq expects a collection, string, or nil, got ${printString(coll)}`, { collection: coll }, 0); - } - const items = toSeq(coll); - return items.length === 0 ? cljNil() : cljList(items); - }), "Returns a sequence of the given collection or string. Strings yield a sequence of single-character strings.", [["coll"]]), - first: withDoc(cljNativeFunction("first", function firstImpl(collection) { - if (collection.kind === "nil") - return cljNil(); - if (!isSeqable(collection)) { - throw EvaluationError.atArg("first expects a collection or string", { collection }, 0); - } - const entries = toSeq(collection); - return entries.length === 0 ? cljNil() : entries[0]; - }), "Returns the first element of the given collection or string.", [["coll"]]), - rest: withDoc(cljNativeFunction("rest", function restImpl(collection) { - if (collection.kind === "nil") - return cljList([]); - if (!isSeqable(collection)) { - throw EvaluationError.atArg("rest expects a collection or string", { collection }, 0); - } - if (isList(collection)) { - if (collection.value.length === 0) { - return collection; - } - return cljList(collection.value.slice(1)); - } - if (isVector(collection)) { - return cljVector(collection.value.slice(1)); - } - if (isMap(collection)) { - if (collection.entries.length === 0) { - return collection; - } - return cljMap(collection.entries.slice(1)); - } - if (collection.kind === "string") { - const chars = toSeq(collection); - return cljList(chars.slice(1)); - } - throw EvaluationError.atArg(`rest expects a collection or string, got ${printString(collection)}`, { collection }, 0); - }), "Returns a sequence of the given collection or string excluding the first element.", [["coll"]]), - conj: withDoc(cljNativeFunction("conj", function conjImpl(collection, ...args) { - if (!collection) { - throw new EvaluationError("conj expects a collection as first argument", { collection }); - } - if (args.length === 0) { - return collection; - } - if (!isCollection(collection)) { - throw EvaluationError.atArg(`conj expects a collection, got ${printString(collection)}`, { collection }, 0); - } - if (isList(collection)) { - const newItems = []; - for (let i = args.length - 1;i >= 0; i--) { - newItems.push(args[i]); - } - return cljList([...newItems, ...collection.value]); - } - if (isVector(collection)) { - return cljVector([...collection.value, ...args]); - } - if (isMap(collection)) { - const newEntries = [...collection.entries]; - for (let i = 0;i < args.length; i += 1) { - const pair = args[i]; - const pairArgIndex = i + 1; - if (pair.kind !== "vector") { - throw EvaluationError.atArg(`conj on maps expects each argument to be a vector key-pair for maps, got ${printString(pair)}`, { pair }, pairArgIndex); - } - if (pair.value.length !== 2) { - throw EvaluationError.atArg(`conj on maps expects each argument to be a vector key-pair for maps, got ${printString(pair)}`, { pair }, pairArgIndex); - } - const key = pair.value[0]; - const keyIdx = newEntries.findIndex(function findKeyEntry(entry) { - return isEqual(entry[0], key); - }); - if (keyIdx === -1) { - newEntries.push([key, pair.value[1]]); - } else { - newEntries[keyIdx] = [key, pair.value[1]]; - } - } - return cljMap([...newEntries]); - } - throw new EvaluationError(`unhandled collection type, got ${printString(collection)}`, { collection }); - }), "Appends args to the given collection. Lists append in reverse order to the head, vectors append to the tail.", [["collection", "&", "args"]]), - cons: withDoc(cljNativeFunction("cons", function consImpl(x, xs) { - if (!isCollection(xs)) { - throw EvaluationError.atArg(`cons expects a collection as second argument, got ${printString(xs)}`, { xs }, 1); - } - if (isMap(xs)) { - throw EvaluationError.atArg("cons on maps is not supported, use vectors instead", { xs }, 1); - } - const wrap = isList(xs) ? cljList : cljVector; - const newItems = [x, ...xs.value]; - return wrap(newItems); - }), "Returns a new collection with x prepended to the head of xs.", [["x", "xs"]]), - assoc: withDoc(cljNativeFunction("assoc", function assocImpl(collection, ...args) { - if (!collection) { - throw new EvaluationError("assoc expects a collection as first argument", { collection }); - } - if (isNil(collection)) { - collection = cljMap([]); - } - if (isList(collection)) { - throw new EvaluationError("assoc on lists is not supported, use vectors instead", { collection }); - } - if (!isCollection(collection)) { - throw EvaluationError.atArg(`assoc expects a collection, got ${printString(collection)}`, { collection }, 0); - } - if (args.length < 2) { - throw new EvaluationError("assoc expects at least two arguments", { - args - }); - } - if (args.length % 2 !== 0) { - throw new EvaluationError("assoc expects an even number of binding arguments", { - args - }); - } - if (isVector(collection)) { - const newValues = [...collection.value]; - for (let i = 0;i < args.length; i += 2) { - const index = args[i]; - if (index.kind !== "number") { - throw EvaluationError.atArg(`assoc on vectors expects each key argument to be a index (number), got ${printString(index)}`, { index }, i + 1); - } - if (index.value > newValues.length) { - throw EvaluationError.atArg(`assoc index ${index.value} is out of bounds for vector of length ${newValues.length}`, { index, collection }, i + 1); - } - newValues[index.value] = args[i + 1]; - } - return cljVector(newValues); - } - if (isMap(collection)) { - const newEntries = [...collection.entries]; - for (let i = 0;i < args.length; i += 2) { - const key = args[i]; - const value = args[i + 1]; - const entryIdx = newEntries.findIndex(function findEntryByKey(entry) { - return isEqual(entry[0], key); - }); - if (entryIdx === -1) { - newEntries.push([key, value]); - } else { - newEntries[entryIdx] = [key, value]; - } - } - return cljMap(newEntries); - } - throw new EvaluationError(`unhandled collection type, got ${printString(collection)}`, { collection }); - }), "Associates the value val with the key k in collection. If collection is a map, returns a new map with the same mappings, otherwise returns a vector with the new value at index k.", [["collection", "&", "kvals"]]), - dissoc: withDoc(cljNativeFunction("dissoc", function dissocImpl(collection, ...args) { - if (!collection) { - throw new EvaluationError("dissoc expects a collection as first argument", { collection }); - } - if (isList(collection)) { - throw EvaluationError.atArg("dissoc on lists is not supported, use vectors instead", { collection }, 0); - } - if (!isCollection(collection)) { - throw EvaluationError.atArg(`dissoc expects a collection, got ${printString(collection)}`, { collection }, 0); - } - if (isVector(collection)) { - if (collection.value.length === 0) { - return collection; - } - const newValues = [...collection.value]; - for (let i = 0;i < args.length; i += 1) { - const index = args[i]; - if (index.kind !== "number") { - throw EvaluationError.atArg(`dissoc on vectors expects each key argument to be a index (number), got ${printString(index)}`, { index }, i + 1); - } - if (index.value >= newValues.length) { - throw EvaluationError.atArg(`dissoc index ${index.value} is out of bounds for vector of length ${newValues.length}`, { index, collection }, i + 1); - } - newValues.splice(index.value, 1); - } - return cljVector(newValues); - } - if (isMap(collection)) { - if (collection.entries.length === 0) { - return collection; - } - const newEntries = [...collection.entries]; - for (let i = 0;i < args.length; i += 1) { - const key = args[i]; - const entryIdx = newEntries.findIndex(function findEntryByKey(entry) { - return isEqual(entry[0], key); - }); - if (entryIdx === -1) { - return collection; - } - newEntries.splice(entryIdx, 1); - } - return cljMap(newEntries); - } - throw new EvaluationError(`unhandled collection type, got ${printString(collection)}`, { collection }); - }), "Dissociates the key k from collection. If collection is a map, returns a new map with the same mappings, otherwise returns a vector with the value at index k removed.", [["collection", "&", "keys"]]), - get: withDoc(cljNativeFunction("get", function getImpl(target, key, notFound) { - const defaultValue = notFound ?? cljNil(); - switch (target.kind) { - case valueKeywords.map: { - const entries = target.entries; - for (const [k, v] of entries) { - if (isEqual(k, key)) { - return v; - } - } - return defaultValue; - } - case valueKeywords.vector: { - const values = target.value; - if (key.kind !== "number") { - throw new EvaluationError("get on vectors expects a 0-based index as parameter", { key }); - } - if (key.value < 0 || key.value >= values.length) { - return defaultValue; - } - return values[key.value]; - } - default: - return defaultValue; - } - }), "Returns the value associated with key in target. If target is a map, returns the value associated with key, otherwise returns the value at index key in target. If not-found is provided, it is returned if the key is not found, otherwise nil is returned.", [ - ["target", "key"], - ["target", "key", "not-found"] - ]), - nth: withDoc(cljNativeFunction("nth", function nthImpl(coll, n, notFound) { - if (coll === undefined || !isList(coll) && !isVector(coll)) { - throw new EvaluationError(`nth expects a list or vector${coll !== undefined ? `, got ${printString(coll)}` : ""}`, { coll }); - } - if (n === undefined || n.kind !== "number") { - throw new EvaluationError(`nth expects a number index${n !== undefined ? `, got ${printString(n)}` : ""}`, { n }); - } - const index = n.value; - const items = coll.value; - if (index < 0 || index >= items.length) { - if (notFound !== undefined) - return notFound; - const err = new EvaluationError(`nth index ${index} is out of bounds for collection of length ${items.length}`, { coll, n }); - err.data = { argIndex: 1 }; - throw err; - } - return items[index]; - }), "Returns the nth element of the given collection. If not-found is provided, it is returned if the index is out of bounds, otherwise an error is thrown.", [["coll", "n", "not-found"]]), - concat: withDoc(cljNativeFunction("concat", function concatImpl(...colls) { - const result = []; - for (const coll of colls) { - if (!isSeqable(coll)) { - throw new EvaluationError(`concat expects collections or strings, got ${printString(coll)}`, { coll }); - } - result.push(...toSeq(coll)); - } - return cljList(result); - }), "Returns a new sequence that is the concatenation of the given sequences or strings.", [["&", "colls"]]), - zipmap: withDoc(cljNativeFunction("zipmap", function zipmapImpl(ks, vs) { - if (ks === undefined || !isSeqable(ks)) { - throw new EvaluationError(`zipmap expects a collection or string as first argument${ks !== undefined ? `, got ${printString(ks)}` : ""}`, { ks }); - } - if (vs === undefined || !isSeqable(vs)) { - throw new EvaluationError(`zipmap expects a collection or string as second argument${vs !== undefined ? `, got ${printString(vs)}` : ""}`, { vs }); - } - const keys = toSeq(ks); - const vals = toSeq(vs); - const len = Math.min(keys.length, vals.length); - const entries = []; - for (let i = 0;i < len; i++) { - entries.push([keys[i], vals[i]]); - } - return cljMap(entries); - }), "Returns a new map with the keys and values of the given collections.", [["ks", "vs"]]), - last: withDoc(cljNativeFunction("last", function lastImpl(coll) { - if (coll === undefined || !isList(coll) && !isVector(coll)) { - throw new EvaluationError(`last expects a list or vector${coll !== undefined ? `, got ${printString(coll)}` : ""}`, { coll }); - } - const items = coll.value; - return items.length === 0 ? cljNil() : items[items.length - 1]; - }), "Returns the last element of the given collection.", [["coll"]]), - reverse: withDoc(cljNativeFunction("reverse", function reverseImpl(coll) { - if (coll === undefined || !isList(coll) && !isVector(coll)) { - throw EvaluationError.atArg(`reverse expects a list or vector${coll !== undefined ? `, got ${printString(coll)}` : ""}`, { coll }, 0); - } - return cljList([...coll.value].reverse()); - }), "Returns a new sequence with the elements of the given collection in reverse order.", [["coll"]]), - "empty?": withDoc(cljNativeFunction("empty?", function emptyPredImpl(coll) { - if (coll === undefined) { - throw EvaluationError.atArg("empty? expects one argument", {}, 0); - } - if (coll.kind === "nil") - return cljBoolean(true); - if (!isSeqable(coll)) { - throw EvaluationError.atArg(`empty? expects a collection, string, or nil, got ${printString(coll)}`, { coll }, 0); - } - return cljBoolean(toSeq(coll).length === 0); - }), "Returns true if coll has no items. Accepts collections, strings, and nil.", [["coll"]]), - "contains?": withDoc(cljNativeFunction("contains?", function containsPredImpl(coll, key) { - if (coll === undefined) { - throw EvaluationError.atArg("contains? expects a collection as first argument", {}, 0); - } - if (key === undefined) { - throw EvaluationError.atArg("contains? expects a key as second argument", {}, 1); - } - if (coll.kind === "nil") - return cljBoolean(false); - if (isMap(coll)) { - return cljBoolean(coll.entries.some(function checkKeyMatch([k]) { - return isEqual(k, key); - })); - } - if (isVector(coll)) { - if (key.kind !== "number") - return cljBoolean(false); - return cljBoolean(key.value >= 0 && key.value < coll.value.length); - } - throw EvaluationError.atArg(`contains? expects a map, vector, or nil, got ${printString(coll)}`, { coll }, 0); - }), "Returns true if key is present in coll. For maps checks key existence (including keys with nil values). For vectors checks index bounds.", [["coll", "key"]]), - repeat: withDoc(cljNativeFunction("repeat", function repeatImpl(n, x) { - if (n === undefined || n.kind !== "number") { - throw EvaluationError.atArg(`repeat expects a number as first argument${n !== undefined ? `, got ${printString(n)}` : ""}`, { n }, 0); - } - return cljList(Array(n.value).fill(x)); - }), "Returns a sequence of n copies of x.", [["n", "x"]]), - range: withDoc(cljNativeFunction("range", function rangeImpl(...args) { - if (args.length === 0 || args.length > 3) { - throw new EvaluationError("range expects 1, 2, or 3 arguments: (range n), (range start end), or (range start end step)", { args }); - } - const badIdx = args.findIndex(function checkIsNumber(a) { - return a.kind !== "number"; - }); - if (badIdx !== -1) { - throw EvaluationError.atArg("range expects number arguments", { args }, badIdx); - } - let start; - let end; - let step; - if (args.length === 1) { - start = 0; - end = args[0].value; - step = 1; - } else if (args.length === 2) { - start = args[0].value; - end = args[1].value; - step = 1; - } else { - start = args[0].value; - end = args[1].value; - step = args[2].value; - } - if (step === 0) { - throw EvaluationError.atArg("range step cannot be zero", { args }, args.length - 1); - } - const result = []; - if (step > 0) { - for (let i = start;i < end; i += step) { - result.push(cljNumber(i)); - } - } else { - for (let i = start;i > end; i += step) { - result.push(cljNumber(i)); - } - } - return cljList(result); - }), "Returns a sequence of numbers from start (inclusive) to end (exclusive), incrementing by step. If step is positive, the sequence is generated from start to end, otherwise it is generated from end to start.", [["n"], ["start", "end"], ["start", "end", "step"]]), - keys: withDoc(cljNativeFunction("keys", function keysImpl(m) { - if (m === undefined || !isMap(m)) { - throw EvaluationError.atArg(`keys expects a map${m !== undefined ? `, got ${printString(m)}` : ""}`, { m }, 0); - } - return cljVector(m.entries.map(function extractKey([k]) { - return k; - })); - }), "Returns a vector of the keys of the given map.", [["m"]]), - vals: withDoc(cljNativeFunction("vals", function valsImpl(m) { - if (m === undefined || !isMap(m)) { - throw EvaluationError.atArg(`vals expects a map${m !== undefined ? `, got ${printString(m)}` : ""}`, { m }, 0); - } - return cljVector(m.entries.map(function extractVal([, v]) { - return v; - })); - }), "Returns a vector of the values of the given map.", [["m"]]), - count: withDoc(cljNativeFunction("count", function countImpl(countable) { - if (![ - valueKeywords.list, - valueKeywords.vector, - valueKeywords.map, - valueKeywords.string - ].includes(countable.kind)) { - throw EvaluationError.atArg(`count expects a countable value, got ${printString(countable)}`, { countable }, 0); - } - switch (countable.kind) { - case valueKeywords.list: - return cljNumber(countable.value.length); - case valueKeywords.vector: - return cljNumber(countable.value.length); - case valueKeywords.map: - return cljNumber(countable.entries.length); - case valueKeywords.string: - return cljNumber(countable.value.length); - default: - throw new EvaluationError(`count expects a countable value, got ${printString(countable)}`, { countable }); - } - }), "Returns the number of elements in the given countable value.", [["countable"]]) -}; - -// src/core/stdlib/errors.ts -var errorFunctions = { - throw: withDoc(cljNativeFunction("throw", function throwImpl(...args) { - if (args.length !== 1) { - throw new EvaluationError(`throw requires exactly 1 argument, got ${args.length}`, { args }); - } - throw new CljThrownSignal(args[0]); - }), "Throws a value as an exception. The value may be any CljValue; maps are idiomatic.", [["value"]]), - "ex-info": withDoc(cljNativeFunction("ex-info", function exInfoImpl(...args) { - if (args.length < 2) { - throw new EvaluationError(`ex-info requires at least 2 arguments, got ${args.length}`, { args }); - } - const [msg, data, cause] = args; - if (msg.kind !== "string") { - throw new EvaluationError("ex-info: first argument must be a string", { msg }); - } - const entries = [ - [cljKeyword(":message"), msg], - [cljKeyword(":data"), data] - ]; - if (cause !== undefined) { - entries.push([cljKeyword(":cause"), cause]); - } - return cljMap(entries); - }), "Creates an error map with :message and :data keys. Optionally accepts a :cause.", [["msg", "data"], ["msg", "data", "cause"]]), - "ex-message": withDoc(cljNativeFunction("ex-message", function exMessageImpl(...args) { - const [e] = args; - if (!isMap(e)) - return cljNil(); - const entry = e.entries.find(function findMessageKey([k]) { - return isKeyword(k) && k.name === ":message"; - }); - return entry ? entry[1] : cljNil(); - }), "Returns the :message of an error map, or nil.", [["e"]]), - "ex-data": withDoc(cljNativeFunction("ex-data", function exDataImpl(...args) { - const [e] = args; - if (!isMap(e)) - return cljNil(); - const entry = e.entries.find(function findDataKey([k]) { - return isKeyword(k) && k.name === ":data"; - }); - return entry ? entry[1] : cljNil(); - }), "Returns the :data map of an error map, or nil.", [["e"]]), - "ex-cause": withDoc(cljNativeFunction("ex-cause", function exCauseImpl(...args) { - const [e] = args; - if (!isMap(e)) - return cljNil(); - const entry = e.entries.find(function findCauseKey([k]) { - return isKeyword(k) && k.name === ":cause"; - }); - return entry ? entry[1] : cljNil(); - }), "Returns the :cause of an error map, or nil.", [["e"]]) -}; - -// src/core/stdlib/hof.ts -var hofFunctions = { - reduce: withDoc(cljNativeFunctionWithContext("reduce", function reduce(ctx, callEnv, fn, ...rest) { - if (fn === undefined || !isAFunction(fn)) { - throw EvaluationError.atArg(`reduce expects a function as first argument${fn !== undefined ? `, got ${printString(fn)}` : ""}`, { fn }, 0); - } - if (rest.length === 0 || rest.length > 2) { - throw new EvaluationError("reduce expects 2 or 3 arguments: (reduce f coll) or (reduce f init coll)", { fn }); - } - const hasInit = rest.length === 2; - const init = hasInit ? rest[0] : undefined; - const collection = hasInit ? rest[1] : rest[0]; - if (collection.kind === "nil") { - if (!hasInit) { - throw new EvaluationError("reduce called on empty collection with no initial value", { fn }); - } - return init; - } - if (!isSeqable(collection)) { - throw EvaluationError.atArg(`reduce expects a collection or string, got ${printString(collection)}`, { collection }, rest.length); - } - const items = toSeq(collection); - if (!hasInit) { - if (items.length === 0) { - throw new EvaluationError("reduce called on empty collection with no initial value", { fn }); - } - if (items.length === 1) - return items[0]; - let acc2 = items[0]; - for (let i = 1;i < items.length; i++) { - const result = ctx.applyFunction(fn, [acc2, items[i]], callEnv); - if (isReduced(result)) - return result.value; - acc2 = result; - } - return acc2; - } - let acc = init; - for (const item of items) { - const result = ctx.applyFunction(fn, [acc, item], callEnv); - if (isReduced(result)) - return result.value; - acc = result; - } - return acc; - }), "Reduces a collection to a single value by iteratively applying f. (reduce f coll) or (reduce f init coll).", [ - ["f", "coll"], - ["f", "val", "coll"] - ]), - apply: withDoc(cljNativeFunctionWithContext("apply", (ctx, callEnv, fn, ...rest) => { - if (fn === undefined || !isCallable(fn)) { - throw EvaluationError.atArg(`apply expects a callable as first argument${fn !== undefined ? `, got ${printString(fn)}` : ""}`, { fn }, 0); - } - if (rest.length === 0) { - throw new EvaluationError("apply expects at least 2 arguments", { - fn - }); - } - const lastArg = rest[rest.length - 1]; - if (!isNil(lastArg) && !isSeqable(lastArg)) { - throw EvaluationError.atArg(`apply expects a collection or string as last argument, got ${printString(lastArg)}`, { lastArg }, rest.length); - } - const args = [ - ...rest.slice(0, -1), - ...isNil(lastArg) ? [] : toSeq(lastArg) - ]; - return ctx.applyCallable(fn, args, callEnv); - }), "Calls f with the elements of the last argument (a collection) as its arguments, optionally prepended by fixed args.", [ - ["f", "args"], - ["f", "&", "args"] - ]), - partial: withDoc(cljNativeFunction("partial", (fn, ...preArgs) => { - if (fn === undefined || !isCallable(fn)) { - throw EvaluationError.atArg(`partial expects a callable as first argument${fn !== undefined ? `, got ${printString(fn)}` : ""}`, { fn }, 0); - } - const capturedFn = fn; - return cljNativeFunctionWithContext("partial", (ctx, callEnv, ...moreArgs) => { - return ctx.applyCallable(capturedFn, [...preArgs, ...moreArgs], callEnv); - }); - }), "Returns a function that calls f with pre-applied args prepended to any additional arguments.", [["f", "&", "args"]]), - comp: withDoc(cljNativeFunction("comp", (...fns) => { - if (fns.length === 0) { - return cljNativeFunction("identity", (x) => x); - } - const badIdx = fns.findIndex((f) => !isCallable(f)); - if (badIdx !== -1) { - throw EvaluationError.atArg("comp expects functions or other callable values (keywords, maps)", { fns }, badIdx); - } - const capturedFns = fns; - return cljNativeFunctionWithContext("composed", (ctx, callEnv, ...args) => { - let result = ctx.applyCallable(capturedFns[capturedFns.length - 1], args, callEnv); - for (let i = capturedFns.length - 2;i >= 0; i--) { - result = ctx.applyCallable(capturedFns[i], [result], callEnv); - } - return result; - }); - }), "Returns the composition of fns, applied right-to-left. (comp f g) is equivalent to (fn [x] (f (g x))). Accepts any callable: functions, keywords, and maps.", [[], ["f"], ["f", "g"], ["f", "g", "&", "fns"]]), - identity: withDoc(cljNativeFunction("identity", (x) => { - if (x === undefined) { - throw EvaluationError.atArg("identity expects one argument", {}, 0); - } - return x; - }), "Returns its single argument unchanged.", [["x"]]) -}; - -// src/core/stdlib/meta.ts -var metaFunctions = { - meta: withDoc(cljNativeFunction("meta", function metaImpl(val) { - if (val === undefined) { - throw EvaluationError.atArg("meta expects one argument", {}, 0); - } - if (val.kind === "function" || val.kind === "native-function" || val.kind === "var" || val.kind === "list" || val.kind === "vector" || val.kind === "map" || val.kind === "symbol" || val.kind === "atom") { - return val.meta ?? cljNil(); - } - return cljNil(); - }), "Returns the metadata map of a value, or nil if the value has no metadata.", [["val"]]), - "with-meta": withDoc(cljNativeFunction("with-meta", function withMetaImpl(val, m) { - if (val === undefined) { - throw EvaluationError.atArg("with-meta expects two arguments", {}, 0); - } - if (m === undefined) { - throw EvaluationError.atArg("with-meta expects two arguments", {}, 1); - } - if (m.kind !== "map" && m.kind !== "nil") { - throw EvaluationError.atArg(`with-meta expects a map as second argument, got ${printString(m)}`, { m }, 1); - } - const metaSupported = val.kind === "function" || val.kind === "native-function" || val.kind === "list" || val.kind === "vector" || val.kind === "map" || val.kind === "symbol"; - if (!metaSupported) { - throw EvaluationError.atArg(`with-meta does not support ${val.kind}, got ${printString(val)}`, { val }, 0); - } - const meta = m.kind === "nil" ? undefined : m; - return { ...val, meta }; - }), "Returns a new value with the metadata map m applied to val.", [["val", "m"]]), - "alter-meta!": withDoc(cljNativeFunctionWithContext("alter-meta!", function alterMetaImpl(ctx, callEnv, ref, f, ...args) { - if (ref === undefined) { - throw EvaluationError.atArg("alter-meta! expects at least two arguments", {}, 0); - } - if (f === undefined) { - throw EvaluationError.atArg("alter-meta! expects at least two arguments", {}, 1); - } - if (ref.kind !== "var" && ref.kind !== "atom") { - throw EvaluationError.atArg(`alter-meta! expects a Var or Atom as first argument, got ${ref.kind}`, {}, 0); - } - if (!isAFunction(f)) { - throw EvaluationError.atArg(`alter-meta! expects a function as second argument, got ${f.kind}`, {}, 1); - } - const currentMeta = ref.meta ?? cljNil(); - const newMeta = ctx.applyCallable(f, [currentMeta, ...args], callEnv); - if (newMeta.kind !== "map" && newMeta.kind !== "nil") { - throw new EvaluationError(`alter-meta! function must return a map or nil, got ${newMeta.kind}`, {}); - } - ref.meta = newMeta.kind === "nil" ? undefined : newMeta; - return newMeta; - }), "Applies f to ref's current metadata (with optional args), sets the result as the new metadata, and returns it.", [["ref", "f", "&", "args"]]) -}; - -// src/core/evaluator/apply.ts -function applyFunctionWithContext(fn, args, ctx, callEnv) { - if (fn.kind === "native-function") { - if (fn.fnWithContext) { - return fn.fnWithContext(ctx, callEnv, ...args); - } - return fn.fn(...args); - } - if (fn.kind === "function") { - const arity = resolveArity(fn.arities, args.length); - let currentArgs = args; - while (true) { - const localEnv = bindParams(arity.params, arity.restParam, currentArgs, fn.env, ctx, callEnv); - try { - return ctx.evaluateForms(arity.body, localEnv); - } catch (e) { - if (e instanceof RecurSignal) { - currentArgs = e.args; - continue; - } - throw e; - } - } - } - throw new EvaluationError(`${fn.kind} is not a callable function`, { - fn, - args - }); -} -function applyCallableWithContext(fn, args, ctx, callEnv) { - if (isAFunction(fn)) { - return applyFunctionWithContext(fn, args, ctx, callEnv); - } - if (isKeyword(fn)) { - const target = args[0]; - const defaultVal = args.length > 1 ? args[1] : cljNil(); - if (isMap(target)) { - const entry = target.entries.find(([k]) => isEqual(k, fn)); - return entry ? entry[1] : defaultVal; - } - return defaultVal; - } - if (isMap(fn)) { - if (args.length === 0) { - throw new EvaluationError("Map used as function requires at least one argument", { fn, args }); - } - const key = args[0]; - const defaultVal = args.length > 1 ? args[1] : cljNil(); - const entry = fn.entries.find(([k]) => isEqual(k, key)); - return entry ? entry[1] : defaultVal; - } - throw new EvaluationError(`${printString(fn)} is not a callable value`, { - fn, - args - }); -} -function applyMacroWithContext(macro, rawArgs, ctx) { - const arity = resolveArity(macro.arities, rawArgs.length); - const localEnv = bindParams(arity.params, arity.restParam, rawArgs, macro.env, ctx, macro.env); - return ctx.evaluateForms(arity.body, localEnv); -} - -// src/core/evaluator/expand.ts -function macroExpandAllWithContext(form, env, ctx) { - if (isVector(form)) { - const expanded2 = form.value.map((sub) => macroExpandAllWithContext(sub, env, ctx)); - return expanded2.every((e, i) => e === form.value[i]) ? form : cljVector(expanded2); - } - if (isMap(form)) { - const expanded2 = form.entries.map(([k, v]) => [ - macroExpandAllWithContext(k, env, ctx), - macroExpandAllWithContext(v, env, ctx) - ]); - return expanded2.every(([k, v], i) => k === form.entries[i][0] && v === form.entries[i][1]) ? form : cljMap(expanded2); - } - if (!isList(form)) - return form; - if (form.value.length === 0) - return form; - const first = form.value[0]; - if (!isSymbol(first)) { - const expanded2 = form.value.map((sub) => macroExpandAllWithContext(sub, env, ctx)); - return expanded2.every((e, i) => e === form.value[i]) ? form : cljList(expanded2); - } - const name = first.name; - if (name === "quote" || name === "quasiquote") - return form; - const macroOrUnknown = tryLookup(name, env); - if (macroOrUnknown !== undefined && isMacro(macroOrUnknown)) { - const expanded2 = ctx.applyMacro(macroOrUnknown, form.value.slice(1)); - return macroExpandAllWithContext(expanded2, env, ctx); - } - const expanded = form.value.map((sub) => macroExpandAllWithContext(sub, env, ctx)); - return expanded.every((e, i) => e === form.value[i]) ? form : cljList(expanded); -} - -// src/core/evaluator/collections.ts -function evaluateVector(vector, env, ctx) { - const evaluated = vector.value.map((v) => ctx.evaluate(v, env)); - if (vector.meta) - return { kind: "vector", value: evaluated, meta: vector.meta }; - return cljVector(evaluated); -} -function evaluateMap(map, env, ctx) { - let entries = []; - for (const [key, value] of map.entries) { - const evaluatedKey = ctx.evaluate(key, env); - const evaluatedValue = ctx.evaluate(value, env); - entries.push([evaluatedKey, evaluatedValue]); - } - if (map.meta) - return { kind: "map", entries, meta: map.meta }; - return cljMap(entries); -} - -// src/core/evaluator/dispatch.ts -function dispatchMultiMethod(mm, args, ctx, env) { - const dispatchVal = ctx.applyFunction(mm.dispatchFn, args, env); - const method = mm.methods.find(({ dispatchVal: dv }) => isEqual(dv, dispatchVal)); - if (method) - return ctx.applyFunction(method.fn, args, env); - if (mm.defaultMethod) - return ctx.applyFunction(mm.defaultMethod, args, env); - throw new EvaluationError(`No method in multimethod '${mm.name}' for dispatch value ${printString(dispatchVal)}`, { mm, dispatchVal }); -} -function evaluateList(list, env, ctx) { - if (list.value.length === 0) { - throw new EvaluationError("Unexpected empty list", { list, env }); - } - const first = list.value[0]; - if (isSpecialForm(first)) { - return evaluateSpecialForm(first.name, list, env, ctx); - } - const evaledFirst = ctx.evaluate(first, env); - if (isMultiMethod(evaledFirst)) { - const args2 = list.value.slice(1).map((v) => ctx.evaluate(v, env)); - return dispatchMultiMethod(evaledFirst, args2, ctx, env); - } - if (!isCallable(evaledFirst)) { - const name = isSymbol(first) ? first.name : printString(first); - throw new EvaluationError(`${name} is not callable`, { list, env }); - } - const args = list.value.slice(1).map((v) => ctx.evaluate(v, env)); - try { - return ctx.applyCallable(evaledFirst, args, env); - } catch (e) { - if (e instanceof EvaluationError && e.data?.argIndex !== undefined && !e.pos) { - const argForm = list.value[e.data.argIndex + 1]; - if (argForm) { - const pos = getPos(argForm); - if (pos) - e.pos = pos; - } - } - throw e; - } -} - -// src/core/evaluator/evaluate.ts -function evaluateWithContext(expr, env, ctx) { - try { - switch (expr.kind) { - case valueKeywords.number: - case valueKeywords.string: - case valueKeywords.keyword: - case valueKeywords.nil: - case valueKeywords.function: - case valueKeywords.multiMethod: - case valueKeywords.boolean: - case valueKeywords.regex: - return expr; - case valueKeywords.symbol: { - const slashIdx = expr.name.indexOf("/"); - if (slashIdx > 0 && slashIdx < expr.name.length - 1) { - const alias = expr.name.slice(0, slashIdx); - const sym = expr.name.slice(slashIdx + 1); - const nsEnv = getNamespaceEnv(env); - const aliasCljNs = nsEnv.ns?.aliases.get(alias); - if (aliasCljNs) { - const v = aliasCljNs.vars.get(sym); - if (v === undefined) { - throw new EvaluationError(`Symbol ${expr.name} not found`, { - symbol: expr.name, - env - }); - } - return v.value; - } - const targetEnv = getRootEnv(env).resolveNs?.(alias) ?? null; - if (!targetEnv) { - throw new EvaluationError(`No such namespace or alias: ${alias}`, { - symbol: expr.name, - env - }); - } - return lookup(sym, targetEnv); - } - return lookup(expr.name, env); - } - case valueKeywords.vector: - return evaluateVector(expr, env, ctx); - case valueKeywords.map: - return evaluateMap(expr, env, ctx); - case valueKeywords.list: - return evaluateList(expr, env, ctx); - default: - throw new EvaluationError("Unexpected value", { expr, env }); - } - } catch (e) { - if (e instanceof EvaluationError && !e.pos) { - const p = getPos(expr); - if (p) - e.pos = p; - } - throw e; - } -} -function evaluateFormsWithContext(forms, env, ctx) { - let result = cljNil(); - for (const form of forms) { - result = ctx.evaluate(form, env); - } - return result; -} - -// src/core/evaluator/index.ts -function createEvaluationContext() { - const ctx = { - evaluate: (expr, env) => evaluateWithContext(expr, env, ctx), - evaluateForms: (forms, env) => evaluateFormsWithContext(forms, env, ctx), - applyFunction: (fn, args, callEnv) => applyFunctionWithContext(fn, args, ctx, callEnv), - applyCallable: (fn, args, callEnv) => applyCallableWithContext(fn, args, ctx, callEnv), - applyMacro: (macro, rawArgs) => applyMacroWithContext(macro, rawArgs, ctx), - expandAll: (form, env) => macroExpandAllWithContext(form, env, ctx) - }; - return ctx; -} -function applyFunction(fn, args, callEnv = makeEnv()) { - return createEvaluationContext().applyFunction(fn, args, callEnv); -} - -// src/core/stdlib/predicates.ts -var predicateFunctions = { - "nil?": withDoc(cljNativeFunction("nil?", function nilPredImpl(arg) { - return cljBoolean(arg.kind === "nil"); - }), "Returns true if the value is nil, false otherwise.", [["arg"]]), - "true?": withDoc(cljNativeFunction("true?", function truePredImpl(arg) { - if (arg.kind !== "boolean") { - return cljBoolean(false); - } - return cljBoolean(arg.value === true); - }), "Returns true if the value is a boolean and true, false otherwise.", [["arg"]]), - "false?": withDoc(cljNativeFunction("false?", function falsePredImpl(arg) { - if (arg.kind !== "boolean") { - return cljBoolean(false); - } - return cljBoolean(arg.value === false); - }), "Returns true if the value is a boolean and false, false otherwise.", [["arg"]]), - "truthy?": withDoc(cljNativeFunction("truthy?", function truthyPredImpl(arg) { - return cljBoolean(isTruthy(arg)); - }), "Returns true if the value is not nil or false, false otherwise.", [["arg"]]), - "falsy?": withDoc(cljNativeFunction("falsy?", function falsyPredImpl(arg) { - return cljBoolean(isFalsy(arg)); - }), "Returns true if the value is nil or false, false otherwise.", [["arg"]]), - "not=": withDoc(cljNativeFunction("not=", function notEqualImpl(...vals) { - if (vals.length < 2) { - throw new EvaluationError("not= expects at least two arguments", { - args: vals - }); - } - for (let i = 1;i < vals.length; i++) { - if (!isEqual(vals[i], vals[i - 1])) { - return cljBoolean(true); - } - } - return cljBoolean(false); - }), "Returns true if any two adjacent arguments are not equal, false otherwise.", [["&", "vals"]]), - "number?": withDoc(cljNativeFunction("number?", function numberPredImpl(x) { - return cljBoolean(x !== undefined && x.kind === "number"); - }), "Returns true if the value is a number, false otherwise.", [["x"]]), - "string?": withDoc(cljNativeFunction("string?", function stringPredImpl(x) { - return cljBoolean(x !== undefined && x.kind === "string"); - }), "Returns true if the value is a string, false otherwise.", [["x"]]), - "boolean?": withDoc(cljNativeFunction("boolean?", function booleanPredImpl(x) { - return cljBoolean(x !== undefined && x.kind === "boolean"); - }), "Returns true if the value is a boolean, false otherwise.", [["x"]]), - "vector?": withDoc(cljNativeFunction("vector?", function vectorPredImpl(x) { - return cljBoolean(x !== undefined && isVector(x)); - }), "Returns true if the value is a vector, false otherwise.", [["x"]]), - "list?": withDoc(cljNativeFunction("list?", function listPredImpl(x) { - return cljBoolean(x !== undefined && isList(x)); - }), "Returns true if the value is a list, false otherwise.", [["x"]]), - "map?": withDoc(cljNativeFunction("map?", function mapPredImpl(x) { - return cljBoolean(x !== undefined && isMap(x)); - }), "Returns true if the value is a map, false otherwise.", [["x"]]), - "keyword?": withDoc(cljNativeFunction("keyword?", function keywordPredImpl(x) { - return cljBoolean(x !== undefined && isKeyword(x)); - }), "Returns true if the value is a keyword, false otherwise.", [["x"]]), - "qualified-keyword?": withDoc(cljNativeFunction("qualified-keyword?", function qualifiedKeywordPredImpl(x) { - return cljBoolean(x !== undefined && isKeyword(x) && x.name.includes("/")); - }), "Returns true if the value is a qualified keyword, false otherwise.", [["x"]]), - "symbol?": withDoc(cljNativeFunction("symbol?", function symbolPredImpl(x) { - return cljBoolean(x !== undefined && isSymbol(x)); - }), "Returns true if the value is a symbol, false otherwise.", [["x"]]), - "qualified-symbol?": withDoc(cljNativeFunction("qualified-symbol?", function qualifiedSymbolPredImpl(x) { - return cljBoolean(x !== undefined && isSymbol(x) && x.name.includes("/")); - }), "Returns true if the value is a qualified symbol, false otherwise.", [["x"]]), - "fn?": withDoc(cljNativeFunction("fn?", function fnPredImpl(x) { - return cljBoolean(x !== undefined && isAFunction(x)); - }), "Returns true if the value is a function, false otherwise.", [["x"]]), - "coll?": withDoc(cljNativeFunction("coll?", function collPredImpl(x) { - return cljBoolean(x !== undefined && isCollection(x)); - }), "Returns true if the value is a collection, false otherwise.", [["x"]]), - some: withDoc(cljNativeFunction("some", function someImpl(pred, coll) { - if (pred === undefined || !isAFunction(pred)) { - throw EvaluationError.atArg(`some expects a function as first argument${pred !== undefined ? `, got ${printString(pred)}` : ""}`, { pred }, 0); - } - if (coll === undefined) { - return cljNil(); - } - if (!isSeqable(coll)) { - throw EvaluationError.atArg(`some expects a collection or string as second argument, got ${printString(coll)}`, { coll }, 1); - } - for (const item of toSeq(coll)) { - const result = applyFunction(pred, [item]); - if (isTruthy(result)) { - return result; - } - } - return cljNil(); - }), "Returns the first truthy result of applying pred to each item in coll, or nil if no item satisfies pred.", [["pred", "coll"]]), - "every?": withDoc(cljNativeFunction("every?", function everyPredImpl(pred, coll) { - if (pred === undefined || !isAFunction(pred)) { - throw EvaluationError.atArg(`every? expects a function as first argument${pred !== undefined ? `, got ${printString(pred)}` : ""}`, { pred }, 0); - } - if (coll === undefined || !isSeqable(coll)) { - throw EvaluationError.atArg(`every? expects a collection or string as second argument${coll !== undefined ? `, got ${printString(coll)}` : ""}`, { coll }, 1); - } - for (const item of toSeq(coll)) { - if (isFalsy(applyFunction(pred, [item]))) { - return cljBoolean(false); - } - } - return cljBoolean(true); - }), "Returns true if all items in coll satisfy pred, false otherwise.", [["pred", "coll"]]) -}; - -// src/core/stdlib/regex.ts -function extractInlineFlags(raw) { - let remaining = raw; - let flags = ""; - const flagGroupRe = /^\(\?([imsx]+)\)/; - let m; - while ((m = flagGroupRe.exec(remaining)) !== null) { - for (const f of m[1]) { - if (f === "x") { - throw new EvaluationError("Regex flag (?x) (verbose mode) has no JavaScript equivalent and is not supported", {}); - } - if (!flags.includes(f)) - flags += f; - } - remaining = remaining.slice(m[0].length); - } - return { pattern: remaining, flags }; -} -function assertRegex(val, fnName) { - if (!isRegex(val)) { - throw new EvaluationError(`${fnName} expects a regex as first argument, got ${printString(val)}`, { val }); - } - return val; -} -function assertStringArg(val, fnName) { - if (val.kind !== "string") { - throw new EvaluationError(`${fnName} expects a string as second argument, got ${printString(val)}`, { val }); - } - return val.value; -} -function matchToClj(match) { - if (match.length === 1) - return cljString(match[0]); - return cljVector(match.map(function mapMatchToClj(m) { - return m == null ? cljNil() : cljString(m); - })); -} -var regexFunctions = { - "regexp?": withDoc(cljNativeFunction("regexp?", function regexpPredImpl(x) { - return cljBoolean(x !== undefined && isRegex(x)); - }), "Returns true if x is a regular expression pattern.", [["x"]]), - "re-pattern": withDoc(cljNativeFunction("re-pattern", function rePatternImpl(s) { - if (s === undefined || s.kind !== "string") { - throw new EvaluationError(`re-pattern expects a string argument${s !== undefined ? `, got ${printString(s)}` : ""}`, { s }); - } - const { pattern, flags } = extractInlineFlags(s.value); - return cljRegex(pattern, flags); - }), `Returns an instance of java.util.regex.Pattern, for use, e.g. in re-matcher. - (re-pattern "\\\\d+") produces the same pattern as #"\\d+".`, [["s"]]), - "re-find": withDoc(cljNativeFunction("re-find", function reFindImpl(reVal, sVal) { - const re = assertRegex(reVal, "re-find"); - const s = assertStringArg(sVal, "re-find"); - const jsRe = new RegExp(re.pattern, re.flags); - const match = jsRe.exec(s); - if (!match) - return cljNil(); - return matchToClj(match); - }), `Returns the next regex match, if any, of string to pattern, using - java.util.regex.Matcher.find(). Returns the match or nil. When there - are groups, returns a vector of the whole match and groups (nil for - unmatched optional groups).`, [["re", "s"]]), - "re-matches": withDoc(cljNativeFunction("re-matches", function reMatchesImpl(reVal, sVal) { - const re = assertRegex(reVal, "re-matches"); - const s = assertStringArg(sVal, "re-matches"); - const jsRe = new RegExp(re.pattern, re.flags); - const match = jsRe.exec(s); - if (!match || match.index !== 0 || match[0].length !== s.length) { - return cljNil(); - } - return matchToClj(match); - }), `Returns the match, if any, of string to pattern, using - java.util.regex.Matcher.matches(). The entire string must match. - Returns the match or nil. When there are groups, returns a vector - of the whole match and groups (nil for unmatched optional groups).`, [["re", "s"]]), - "re-seq": withDoc(cljNativeFunction("re-seq", function reSeqImpl(reVal, sVal) { - const re = assertRegex(reVal, "re-seq"); - const s = assertStringArg(sVal, "re-seq"); - const jsRe = new RegExp(re.pattern, re.flags + "g"); - const results = []; - let match; - while ((match = jsRe.exec(s)) !== null) { - if (match[0].length === 0) { - jsRe.lastIndex++; - continue; - } - results.push(matchToClj(match)); - } - if (results.length === 0) - return cljNil(); - return { kind: "list", value: results }; - }), `Returns a lazy sequence of successive matches of pattern in string, - using java.util.regex.Matcher.find(), each such match processed with - re-groups.`, [["re", "s"]]), - "str-split*": withDoc(cljNativeFunction("str-split*", function strSplitImpl(sVal, sepVal, limitVal) { - if (sVal === undefined || sVal.kind !== "string") { - throw new EvaluationError(`str-split* expects a string as first argument${sVal !== undefined ? `, got ${printString(sVal)}` : ""}`, { sVal }); - } - const s = sVal.value; - const hasLimit = limitVal !== undefined && limitVal.kind !== "nil"; - const limit = hasLimit && limitVal.kind === "number" ? limitVal.value : undefined; - let jsPattern; - let jsFlags; - if (sepVal.kind !== "regex") { - throw new EvaluationError(`str-split* expects a regex pattern as second argument, got ${printString(sepVal)}`, { sepVal }); - } - if (sepVal.pattern === "") { - const chars = [...s]; - if (limit === undefined || limit >= chars.length) { - return cljVector(chars.map(cljString)); - } - const parts = [...chars.slice(0, limit - 1), chars.slice(limit - 1).join("")]; - return cljVector(parts.map(function mapPartToString(p) { - return cljString(p); - })); - } - jsPattern = sepVal.pattern; - jsFlags = sepVal.flags; - const re = new RegExp(jsPattern, jsFlags + "g"); - const rawParts = splitWithRegex(s, re, limit); - return cljVector(rawParts.map(function mapRawPartToString(p) { - return cljString(p); - })); - }), `Internal helper for clojure.string/split. Splits string s by a regex or - string separator. Optional limit keeps all parts when provided.`, [["s", "sep"], ["s", "sep", "limit"]]) -}; -function splitWithRegex(s, re, limit) { - const parts = []; - let lastIndex = 0; - let match; - let count = 0; - while ((match = re.exec(s)) !== null) { - if (match[0].length === 0) { - re.lastIndex++; - continue; - } - if (limit !== undefined && count >= limit - 1) - break; - parts.push(s.slice(lastIndex, match.index)); - lastIndex = match.index + match[0].length; - count++; - } - parts.push(s.slice(lastIndex)); - if (limit === undefined) { - while (parts.length > 0 && parts[parts.length - 1] === "") { - parts.pop(); - } - } - return parts; -} - -// src/core/stdlib/strings.ts -function assertStr(val, fnName) { - if (val === undefined || val.kind !== "string") { - throw new EvaluationError(`${fnName} expects a string as first argument${val !== undefined ? `, got ${printString(val)}` : ""}`, { val }); - } - return val.value; -} -function assertStrArg(val, pos, fnName) { - if (val === undefined || val.kind !== "string") { - throw new EvaluationError(`${fnName} expects a string as ${pos} argument${val !== undefined ? `, got ${printString(val)}` : ""}`, { val }); - } - return val.value; -} -function escapeRegex(s) { - return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} -function escapeDollarInReplacement(s) { - return s.replace(/\$/g, "$$$$"); -} -function buildMatchValue(whole, args) { - let offsetIdx = -1; - for (let i = args.length - 1;i >= 0; i--) { - if (typeof args[i] === "number") { - offsetIdx = i; - break; - } - } - const groups = offsetIdx > 0 ? args.slice(0, offsetIdx) : []; - if (groups.length === 0) - return cljString(whole); - return cljVector([ - cljString(whole), - ...groups.map(function mapGroupToClj(g) { - return g == null ? cljNil() : cljString(String(g)); - }) - ]); -} -function doReplace(ctx, callEnv, fnName, sVal, matchVal, replVal, global) { - const s = assertStr(sVal, fnName); - if (matchVal === undefined || replVal === undefined) { - throw new EvaluationError(`${fnName} expects 3 arguments`, {}); - } - if (matchVal.kind === "string") { - if (replVal.kind !== "string") { - throw new EvaluationError(`${fnName}: when match is a string, replacement must also be a string, got ${printString(replVal)}`, { replVal }); - } - const re = new RegExp(escapeRegex(matchVal.value), global ? "g" : ""); - return cljString(s.replace(re, escapeDollarInReplacement(replVal.value))); - } - if (matchVal.kind === "regex") { - const re = matchVal; - const flags = global ? re.flags + "g" : re.flags; - const jsRe = new RegExp(re.pattern, flags); - if (replVal.kind === "string") { - return cljString(s.replace(jsRe, replVal.value)); - } - if (isAFunction(replVal)) { - const fn = replVal; - const result = s.replace(jsRe, function replaceCallback(whole, ...args) { - const matchClj = buildMatchValue(whole, args); - const replResult = ctx.applyFunction(fn, [matchClj], callEnv); - return valueToString(replResult); - }); - return cljString(result); - } - throw new EvaluationError(`${fnName}: replacement must be a string or function, got ${printString(replVal)}`, { replVal }); - } - throw new EvaluationError(`${fnName}: match must be a string or regex, got ${printString(matchVal)}`, { matchVal }); -} -var stringFunctions = { - "str-upper-case*": withDoc(cljNativeFunction("str-upper-case*", function strUpperCaseImpl(sVal) { - return cljString(assertStr(sVal, "str-upper-case*").toUpperCase()); - }), "Internal helper. Converts s to upper-case.", [["s"]]), - "str-lower-case*": withDoc(cljNativeFunction("str-lower-case*", function strLowerCaseImpl(sVal) { - return cljString(assertStr(sVal, "str-lower-case*").toLowerCase()); - }), "Internal helper. Converts s to lower-case.", [["s"]]), - "str-trim*": withDoc(cljNativeFunction("str-trim*", function strTrimImpl(sVal) { - return cljString(assertStr(sVal, "str-trim*").trim()); - }), "Internal helper. Removes whitespace from both ends of s.", [["s"]]), - "str-triml*": withDoc(cljNativeFunction("str-triml*", function strTrimlImpl(sVal) { - return cljString(assertStr(sVal, "str-triml*").trimStart()); - }), "Internal helper. Removes whitespace from the left of s.", [["s"]]), - "str-trimr*": withDoc(cljNativeFunction("str-trimr*", function strTrimrImpl(sVal) { - return cljString(assertStr(sVal, "str-trimr*").trimEnd()); - }), "Internal helper. Removes whitespace from the right of s.", [["s"]]), - "str-reverse*": withDoc(cljNativeFunction("str-reverse*", function strReverseImpl(sVal) { - return cljString([...assertStr(sVal, "str-reverse*")].reverse().join("")); - }), "Internal helper. Returns s with its characters reversed (Unicode-safe).", [["s"]]), - "str-starts-with*": withDoc(cljNativeFunction("str-starts-with*", function strStartsWithImpl(sVal, substrVal) { - const s = assertStr(sVal, "str-starts-with*"); - const substr = assertStrArg(substrVal, "second", "str-starts-with*"); - return cljBoolean(s.startsWith(substr)); - }), "Internal helper. Returns true if s starts with substr.", [["s", "substr"]]), - "str-ends-with*": withDoc(cljNativeFunction("str-ends-with*", function strEndsWithImpl(sVal, substrVal) { - const s = assertStr(sVal, "str-ends-with*"); - const substr = assertStrArg(substrVal, "second", "str-ends-with*"); - return cljBoolean(s.endsWith(substr)); - }), "Internal helper. Returns true if s ends with substr.", [["s", "substr"]]), - "str-includes*": withDoc(cljNativeFunction("str-includes*", function strIncludesImpl(sVal, substrVal) { - const s = assertStr(sVal, "str-includes*"); - const substr = assertStrArg(substrVal, "second", "str-includes*"); - return cljBoolean(s.includes(substr)); - }), "Internal helper. Returns true if s contains substr.", [["s", "substr"]]), - "str-index-of*": withDoc(cljNativeFunction("str-index-of*", function strIndexOfImpl(sVal, valVal, fromVal) { - const s = assertStr(sVal, "str-index-of*"); - const needle = assertStrArg(valVal, "second", "str-index-of*"); - let idx; - if (fromVal !== undefined && fromVal.kind !== "nil") { - if (fromVal.kind !== "number") { - throw new EvaluationError(`str-index-of* expects a number as third argument, got ${printString(fromVal)}`, { fromVal }); - } - idx = s.indexOf(needle, fromVal.value); - } else { - idx = s.indexOf(needle); - } - return idx === -1 ? cljNil() : cljNumber(idx); - }), "Internal helper. Returns index of value in s, or nil if not found.", [["s", "value"], ["s", "value", "from-index"]]), - "str-last-index-of*": withDoc(cljNativeFunction("str-last-index-of*", function strLastIndexOfImpl(sVal, valVal, fromVal) { - const s = assertStr(sVal, "str-last-index-of*"); - const needle = assertStrArg(valVal, "second", "str-last-index-of*"); - let idx; - if (fromVal !== undefined && fromVal.kind !== "nil") { - if (fromVal.kind !== "number") { - throw new EvaluationError(`str-last-index-of* expects a number as third argument, got ${printString(fromVal)}`, { fromVal }); - } - idx = s.lastIndexOf(needle, fromVal.value); - } else { - idx = s.lastIndexOf(needle); - } - return idx === -1 ? cljNil() : cljNumber(idx); - }), "Internal helper. Returns last index of value in s, or nil if not found.", [["s", "value"], ["s", "value", "from-index"]]), - "str-replace*": withDoc(cljNativeFunctionWithContext("str-replace*", function strReplaceImpl(ctx, callEnv, sVal, matchVal, replVal) { - return doReplace(ctx, callEnv, "str-replace*", sVal, matchVal, replVal, true); - }), "Internal helper. Replaces all occurrences of match with replacement in s.", [["s", "match", "replacement"]]), - "str-replace-first*": withDoc(cljNativeFunctionWithContext("str-replace-first*", function strReplaceFirstImpl(ctx, callEnv, sVal, matchVal, replVal) { - return doReplace(ctx, callEnv, "str-replace-first*", sVal, matchVal, replVal, false); - }), "Internal helper. Replaces the first occurrence of match with replacement in s.", [["s", "match", "replacement"]]) -}; - -// src/core/stdlib/transducers.ts -var transducerFunctions = { - reduced: withDoc(cljNativeFunction("reduced", function reducedImpl(value) { - if (value === undefined) { - throw new EvaluationError("reduced expects one argument", {}); - } - return cljReduced(value); - }), "Returns a reduced value, indicating termination of the reduction process.", [["value"]]), - "reduced?": withDoc(cljNativeFunction("reduced?", function isReducedImpl(value) { - if (value === undefined) { - throw new EvaluationError("reduced? expects one argument", {}); - } - return cljBoolean(isReduced(value)); - }), "Returns true if the given value is a reduced value, false otherwise.", [["value"]]), - unreduced: withDoc(cljNativeFunction("unreduced", function unreducedImpl(value) { - if (value === undefined) { - throw new EvaluationError("unreduced expects one argument", {}); - } - return isReduced(value) ? value.value : value; - }), "Returns the unreduced value of the given value. If the value is not a reduced value, it is returned unchanged.", [["value"]]), - "ensure-reduced": withDoc(cljNativeFunction("ensure-reduced", function ensureReducedImpl(value) { - if (value === undefined) { - throw new EvaluationError("ensure-reduced expects one argument", {}); - } - return isReduced(value) ? value : cljReduced(value); - }), "Returns the given value if it is a reduced value, otherwise returns a reduced value with the given value as its value.", [["value"]]), - "volatile!": withDoc(cljNativeFunction("volatile!", function volatileImpl(value) { - if (value === undefined) { - throw new EvaluationError("volatile! expects one argument", {}); - } - return cljVolatile(value); - }), "Returns a volatile value with the given value as its value.", [["value"]]), - "volatile?": withDoc(cljNativeFunction("volatile?", function isVolatileImpl(value) { - if (value === undefined) { - throw new EvaluationError("volatile? expects one argument", {}); - } - return cljBoolean(isVolatile(value)); - }), "Returns true if the given value is a volatile value, false otherwise.", [["value"]]), - "vreset!": withDoc(cljNativeFunction("vreset!", function vresetImpl(vol, newVal) { - if (!isVolatile(vol)) { - throw new EvaluationError(`vreset! expects a volatile as its first argument, got ${printString(vol)}`, { vol }); - } - if (newVal === undefined) { - throw new EvaluationError("vreset! expects two arguments", { vol }); - } - vol.value = newVal; - return newVal; - }), "Resets the value of the given volatile to the given new value and returns the new value.", [["vol", "newVal"]]), - "vswap!": withDoc(cljNativeFunctionWithContext("vswap!", function vswapImpl(ctx, callEnv, vol, fn, ...extraArgs) { - if (!isVolatile(vol)) { - throw new EvaluationError(`vswap! expects a volatile as its first argument, got ${printString(vol)}`, { vol }); - } - if (!isAFunction(fn)) { - throw new EvaluationError(`vswap! expects a function as its second argument, got ${printString(fn)}`, { fn }); - } - const newVal = ctx.applyFunction(fn, [vol.value, ...extraArgs], callEnv); - vol.value = newVal; - return newVal; - }), "Applies fn to the current value of the volatile, replacing the current value with the result. Returns the new value.", [ - ["vol", "fn"], - ["vol", "fn", "&", "extraArgs"] - ]), - transduce: withDoc(cljNativeFunctionWithContext("transduce", function transduceImpl(ctx, callEnv, xform, f, init, coll) { - if (!isAFunction(xform)) { - throw new EvaluationError(`transduce expects a transducer (function) as first argument, got ${printString(xform)}`, { xf: xform }); - } - if (!isAFunction(f)) { - throw new EvaluationError(`transduce expects a reducing function as second argument, got ${printString(f)}`, { f }); - } - if (init === undefined) { - throw new EvaluationError("transduce expects 3 or 4 arguments: (transduce xf f coll) or (transduce xf f init coll)", {}); - } - let actualInit; - let actualColl; - if (coll === undefined) { - actualColl = init; - actualInit = ctx.applyFunction(f, [], callEnv); - } else { - actualInit = init; - actualColl = coll; - } - const rf = ctx.applyFunction(xform, [f], callEnv); - if (isNil(actualColl)) { - return ctx.applyFunction(rf, [actualInit], callEnv); - } - if (!isSeqable(actualColl)) { - throw new EvaluationError(`transduce expects a collection or string as ${coll === undefined ? "third" : "fourth"} argument, got ${printString(actualColl)}`, { coll: actualColl }); - } - const items = toSeq(actualColl); - let acc = actualInit; - for (const item of items) { - const result = ctx.applyFunction(rf, [acc, item], callEnv); - if (isReduced(result)) { - acc = result.value; - break; - } - acc = result; - } - return ctx.applyFunction(rf, [acc], callEnv); - }), joinLines([ - "reduce with a transformation of f (xf). If init is not", - "supplied, (f) will be called to produce it. f should be a reducing", - "step function that accepts both 1 and 2 arguments, if it accepts", - "only 2 you can add the arity-1 with 'completing'. Returns the result", - "of applying (the transformed) xf to init and the first item in coll,", - "then applying xf to that result and the 2nd item, etc. If coll", - "contains no items, returns init and f is not called. Note that", - "certain transforms may inject or skip items." - ]), [ - ["xform", "f", "coll"], - ["xform", "f", "init", "coll"] - ]) -}; - -// src/core/stdlib/utils.ts -var utilFunctions = { - str: withDoc(cljNativeFunction("str", function strImpl(...args) { - return cljString(args.map(valueToString).join("")); - }), "Returns a concatenated string representation of the given values.", [["&", "args"]]), - subs: withDoc(cljNativeFunction("subs", function subsImpl(s, start, end) { - if (s === undefined || s.kind !== "string") { - throw EvaluationError.atArg(`subs expects a string as first argument${s !== undefined ? `, got ${printString(s)}` : ""}`, { s }, 0); - } - if (start === undefined || start.kind !== "number") { - throw EvaluationError.atArg(`subs expects a number as second argument${start !== undefined ? `, got ${printString(start)}` : ""}`, { start }, 1); - } - if (end !== undefined && end.kind !== "number") { - throw EvaluationError.atArg(`subs expects a number as optional third argument${end !== undefined ? `, got ${printString(end)}` : ""}`, { end }, 2); - } - const from = start.value; - const to = end?.value; - return cljString(to === undefined ? s.value.slice(from) : s.value.slice(from, to)); - }), "Returns the substring of s beginning at start, and optionally ending before end.", [ - ["s", "start"], - ["s", "start", "end"] - ]), - type: withDoc(cljNativeFunction("type", function typeImpl(x) { - if (x === undefined) { - throw new EvaluationError("type expects an argument", { x }); - } - const kindToKeyword = { - number: ":number", - string: ":string", - boolean: ":boolean", - nil: ":nil", - keyword: ":keyword", - symbol: ":symbol", - list: ":list", - vector: ":vector", - map: ":map", - function: ":function", - regex: ":regex", - var: ":var", - "native-function": ":function" - }; - const name = kindToKeyword[x.kind]; - if (!name) { - throw new EvaluationError(`type: unhandled kind ${x.kind}`, { x }); - } - return cljKeyword(name); - }), "Returns a keyword representing the type of the given value.", [["x"]]), - gensym: withDoc(cljNativeFunction("gensym", function gensymImpl(...args) { - if (args.length > 1) { - throw new EvaluationError("gensym takes 0 or 1 arguments", { args }); - } - const prefix = args[0]; - if (prefix !== undefined && prefix.kind !== "string") { - throw EvaluationError.atArg(`gensym prefix must be a string${prefix !== undefined ? `, got ${printString(prefix)}` : ""}`, { prefix }, 0); - } - const p = prefix?.kind === "string" ? prefix.value : "G"; - return cljSymbol(makeGensym(p)); - }), 'Returns a unique symbol with the given prefix. Defaults to "G" if no prefix is provided.', [[], ["prefix"]]), - eval: withDoc(cljNativeFunctionWithContext("eval", function evalImpl(ctx, callEnv, form) { - if (form === undefined) { - throw new EvaluationError("eval expects a form as argument", { - form - }); - } - const expanded = ctx.expandAll(form, callEnv); - return ctx.evaluate(expanded, callEnv); - }), "Evaluates the given form in the global environment and returns the result.", [["form"]]), - "macroexpand-1": withDoc(cljNativeFunctionWithContext("macroexpand-1", function macroexpand1Impl(ctx, callEnv, form) { - if (!isList(form) || form.value.length === 0) - return form; - const head = form.value[0]; - if (!isSymbol(head)) - return form; - const macroValue = tryLookup(head.name, callEnv); - if (macroValue === undefined) - return form; - if (!isMacro(macroValue)) - return form; - return ctx.applyMacro(macroValue, form.value.slice(1)); - }), "If the head of the form is a macro, expands it and returns the resulting forms. Otherwise, returns the form unchanged.", [["form"]]), - macroexpand: withDoc(cljNativeFunctionWithContext("macroexpand", function macroexpandImpl(ctx, callEnv, form) { - let current = form; - while (true) { - if (!isList(current) || current.value.length === 0) - return current; - const head = current.value[0]; - if (!isSymbol(head)) - return current; - const macroValue = tryLookup(head.name, callEnv); - if (macroValue === undefined) - return current; - if (!isMacro(macroValue)) - return current; - current = ctx.applyMacro(macroValue, current.value.slice(1)); - } - }), joinLines([ - "Expands all macros until the expansion is stable (head is no longer a macro)", - "", - "Note neither macroexpand-1 nor macroexpand will expand macros in sub-forms" - ]), [["form"]]), - "macroexpand-all": withDoc(cljNativeFunctionWithContext("macroexpand-all", function macroexpandAllImpl(ctx, callEnv, form) { - return ctx.expandAll(form, callEnv); - }), joinLines([ - "Fully expands all macros in a form recursively — including in sub-forms.", - "", - "Unlike macroexpand, this descends into every sub-expression.", - "Expansion stops at quote/quasiquote boundaries and fn/loop bodies." - ]), [["form"]]), - namespace: withDoc(cljNativeFunction("namespace", function namespaceImpl(x) { - if (x === undefined) { - throw EvaluationError.atArg("namespace expects an argument", { x }, 0); - } - let raw; - if (isKeyword(x)) { - raw = x.name.slice(1); - } else if (isSymbol(x)) { - raw = x.name; - } else { - throw EvaluationError.atArg(`namespace expects a keyword or symbol, got ${printString(x)}`, { x }, 0); - } - const slashIdx = raw.indexOf("/"); - if (slashIdx <= 0) - return cljNil(); - return cljString(raw.slice(0, slashIdx)); - }), "Returns the namespace string of a qualified keyword or symbol, or nil if the argument is not qualified.", [["x"]]), - name: withDoc(cljNativeFunction("name", function nameImpl(x) { - if (x === undefined) { - throw EvaluationError.atArg("name expects an argument", { x }, 0); - } - let raw; - if (isKeyword(x)) { - raw = x.name.slice(1); - } else if (isSymbol(x)) { - raw = x.name; - } else if (x.kind === "string") { - return x; - } else { - throw EvaluationError.atArg(`name expects a keyword, symbol, or string, got ${printString(x)}`, { x }, 0); - } - const slashIdx = raw.indexOf("/"); - return cljString(slashIdx >= 0 ? raw.slice(slashIdx + 1) : raw); - }), "Returns the local name of a qualified keyword or symbol, or the string value if the argument is a string.", [["x"]]), - keyword: withDoc(cljNativeFunction("keyword", function keywordImpl(...args) { - if (args.length === 0 || args.length > 2) { - throw new EvaluationError("keyword expects 1 or 2 string arguments", { - args - }); - } - if (args[0].kind !== "string") { - throw EvaluationError.atArg(`keyword expects a string, got ${printString(args[0])}`, { args }, 0); - } - if (args.length === 1) { - return cljKeyword(`:${args[0].value}`); - } - if (args[1].kind !== "string") { - throw EvaluationError.atArg(`keyword second argument must be a string, got ${printString(args[1])}`, { args }, 1); - } - return cljKeyword(`:${args[0].value}/${args[1].value}`); - }), joinLines([ - "Constructs a keyword with the given name and namespace strings. Returns a keyword value.", - "", - "Note: do not use : in the keyword strings, it will be added automatically.", - 'e.g. (keyword "foo") => :foo' - ]), [["name"], ["ns", "name"]]), - boolean: withDoc(cljNativeFunction("boolean", function booleanImpl(x) { - if (x === undefined) - return cljBoolean(false); - return cljBoolean(isTruthy(x)); - }), "Coerces to boolean. Everything is true except false and nil.", [["x"]]) -}; - -// src/core/stdlib/vars.ts -var varFunctions = { - "var?": withDoc(cljNativeFunction("var?", function isVarImpl(x) { - return cljBoolean(isVar(x)); - }), "Returns true if x is a Var.", [["x"]]), - "var-get": withDoc(cljNativeFunction("var-get", function varGetImpl(x) { - if (!isVar(x)) { - throw new EvaluationError(`var-get expects a Var, got ${x.kind}`, { x }); - } - return x.value; - }), "Returns the value in the Var object.", [["x"]]), - "alter-var-root": withDoc(cljNativeFunctionWithContext("alter-var-root", function alterVarRootImpl(ctx, callEnv, varVal, f, ...args) { - if (!isVar(varVal)) { - throw new EvaluationError(`alter-var-root expects a Var as its first argument, got ${varVal.kind}`, { varVal }); - } - if (!isAFunction(f)) { - throw new EvaluationError(`alter-var-root expects a function as its second argument, got ${f.kind}`, { f }); - } - const newVal = ctx.applyFunction(f, [varVal.value, ...args], callEnv); - varVal.value = newVal; - return newVal; - }), "Atomically alters the root binding of var v by applying f to its current value plus any additional args.", [["v", "f", "&", "args"]]) -}; - -// src/core/core-env.ts -var nativeFunctions = { - ...arithmeticFunctions, - ...atomFunctions, - ...collectionFunctions, - ...errorFunctions, - ...predicateFunctions, - ...hofFunctions, - ...metaFunctions, - ...transducerFunctions, - ...regexFunctions, - ...stringFunctions, - ...utilFunctions, - ...varFunctions -}; -function loadCoreFunctions(env, output) { - for (const [key, value] of Object.entries(nativeFunctions)) { - internVar(key, value, env); - } - const emit = output ?? ((text) => console.log(text)); - internVar("println", cljNativeFunction("println", (...args) => { - emit(args.map(valueToString).join(" ") + ` -`); - return cljNil(); - }), env); - internVar("print", cljNativeFunction("print", (...args) => { - emit(args.map(valueToString).join(" ")); - return cljNil(); - }), env); - internVar("newline", cljNativeFunction("newline", () => { - emit(` -`); - return cljNil(); - }), env); -} - -// src/core/scanners.ts -var createCursor = (line, col, offset) => ({ - line, - col, - offset -}); -var makeScannerPrimitives = (input, cursor) => { - return { - peek: (ahead = 0) => { - const idx = cursor.offset + ahead; - if (idx >= input.length) - return null; - return input[idx]; - }, - isAtEnd: () => { - return cursor.offset >= input.length; - }, - position: () => { - return { - offset: cursor.offset, - line: cursor.line, - col: cursor.col - }; - } - }; -}; -function makeCharScanner(input) { - const cursor = createCursor(0, 0, 0); - const api = { - ...makeScannerPrimitives(input, cursor), - advance: () => { - if (cursor.offset >= input.length) - return null; - const ch = input[cursor.offset]; - cursor.offset++; - if (ch === ` -`) { - cursor.line++; - cursor.col = 0; - } else { - cursor.col++; - } - return ch; - }, - consumeWhile(predicate) { - const buffer = []; - while (!api.isAtEnd() && predicate(api.peek())) { - buffer.push(api.advance()); - } - return buffer.join(""); - } - }; - return api; -} -function makeTokenScanner(input) { - const cursor = createCursor(0, 0, 0); - const api = { - ...makeScannerPrimitives(input, cursor), - advance: () => { - if (cursor.offset >= input.length) - return null; - const token = input[cursor.offset]; - cursor.offset++; - cursor.col = token.end.col; - cursor.line = token.end.line; - return token; - }, - consumeWhile(predicate) { - const buffer = []; - while (!api.isAtEnd() && predicate(api.peek())) { - buffer.push(api.advance()); - } - return buffer; - }, - consumeN(n) { - for (let i = 0;i < n; i++) { - api.advance(); - } - } - }; - return api; -} - -// src/core/tokenizer.ts -var isNewline = (char) => char === ` -`; -var isWhitespace = (char) => [" ", ",", ` -`, "\r", "\t"].includes(char); -var isComment = (char) => char === ";"; -var isLParen = (char) => char === "("; -var isRParen = (char) => char === ")"; -var isLBracket = (char) => char === "["; -var isRBracket = (char) => char === "]"; -var isLBrace = (char) => char === "{"; -var isRBrace = (char) => char === "}"; -var isDoubleQuote = (char) => char === '"'; -var isSingleQuote = (char) => char === "'"; -var isBacktick = (char) => char === "`"; -var isTilde = (char) => char === "~"; -var isAt = (char) => char === "@"; -var isNumber = (char) => { - const parsed = parseInt(char); - if (isNaN(parsed)) { - return false; - } - return parsed >= 0 && parsed <= 9; -}; -var isDot = (char) => char === "."; -var isKeywordStart = (char) => char === ":"; -var isHash = (char) => char === "#"; -var isCaret = (char) => char === "^"; -var isDelimiter = (char) => isLParen(char) || isRParen(char) || isLBracket(char) || isRBracket(char) || isLBrace(char) || isRBrace(char) || isBacktick(char) || isSingleQuote(char) || isAt(char) || isCaret(char); -var parseWhitespace = (ctx) => { - const scanner = ctx.scanner; - const start = scanner.position(); - scanner.consumeWhile(isWhitespace); - return { - kind: tokenKeywords.Whitespace, - start, - end: scanner.position() - }; -}; -var parseComment = (ctx) => { - const scanner = ctx.scanner; - const start = scanner.position(); - scanner.advance(); - const value = scanner.consumeWhile((char) => !isNewline(char)); - if (!scanner.isAtEnd() && scanner.peek() === ` -`) { - scanner.advance(); - } - return { - kind: tokenKeywords.Comment, - value, - start, - end: scanner.position() - }; -}; -var parseString = (ctx) => { - const scanner = ctx.scanner; - const start = scanner.position(); - scanner.advance(); - const buffer = []; - let foundClosingQuote = false; - while (!scanner.isAtEnd()) { - const ch = scanner.peek(); - if (ch === "\\") { - scanner.advance(); - const nextChar = scanner.peek(); - switch (nextChar) { - case '"': - buffer.push('"'); - break; - case "\\": - buffer.push("\\"); - break; - case "n": - buffer.push(` -`); - break; - case "r": - buffer.push("\r"); - break; - case "t": - buffer.push("\t"); - break; - default: - buffer.push(nextChar); - } - if (!scanner.isAtEnd()) { - scanner.advance(); - } - continue; - } - if (ch === '"') { - scanner.advance(); - foundClosingQuote = true; - break; - } - buffer.push(scanner.advance()); - } - if (!foundClosingQuote) { - throw new TokenizerError(`Unterminated string detected at ${start.offset}`, scanner.position()); - } - return { - kind: tokenKeywords.String, - value: buffer.join(""), - start, - end: scanner.position() - }; -}; -var parseKeyword = (ctx) => { - const scanner = ctx.scanner; - const start = scanner.position(); - const value = scanner.consumeWhile((char) => isKeywordStart(char) || !isWhitespace(char) && !isDelimiter(char) && !isComment(char)); - return { - kind: tokenKeywords.Keyword, - value, - start, - end: scanner.position() - }; -}; -function isNumberToken(char, ctx) { - const scanner = ctx.scanner; - const next = scanner.peek(1); - return isNumber(char) || char === "-" && next !== null && isNumber(next); -} -var parseNumber = (ctx) => { - const scanner = ctx.scanner; - const start = scanner.position(); - let value = ""; - if (scanner.peek() === "-") { - value += scanner.advance(); - } - value += scanner.consumeWhile(isNumber); - if (!scanner.isAtEnd() && scanner.peek() === "." && scanner.peek(1) !== null && isNumber(scanner.peek(1))) { - value += scanner.advance(); - value += scanner.consumeWhile(isNumber); - } - if (!scanner.isAtEnd() && (scanner.peek() === "e" || scanner.peek() === "E")) { - value += scanner.advance(); - if (!scanner.isAtEnd() && (scanner.peek() === "+" || scanner.peek() === "-")) { - value += scanner.advance(); - } - const exponentDigits = scanner.consumeWhile(isNumber); - if (exponentDigits.length === 0) { - throw new TokenizerError(`Invalid number format at line ${start.line} column ${start.col}: "${value}"`, { start, end: scanner.position() }); - } - value += exponentDigits; - } - if (!scanner.isAtEnd() && isDot(scanner.peek())) { - throw new TokenizerError(`Invalid number format at line ${start.line} column ${start.col}: "${value}${scanner.consumeWhile((ch) => !isWhitespace(ch) && !isDelimiter(ch))}"`, { start, end: scanner.position() }); - } - return { - kind: tokenKeywords.Number, - value: Number(value), - start, - end: scanner.position() - }; -}; -var parseSymbol = (ctx) => { - const scanner = ctx.scanner; - const start = scanner.position(); - const value = scanner.consumeWhile((char) => !isWhitespace(char) && !isDelimiter(char) && !isComment(char)); - return { - kind: tokenKeywords.Symbol, - value, - start, - end: scanner.position() - }; -}; -var parseDerefToken = (ctx) => { - const scanner = ctx.scanner; - const start = scanner.position(); - scanner.advance(); - return { kind: "Deref", start, end: scanner.position() }; -}; -var parseMetaToken = (ctx) => { - const scanner = ctx.scanner; - const start = scanner.position(); - scanner.advance(); - return { kind: "Meta", start, end: scanner.position() }; -}; -var parseRegexLiteral = (ctx, start) => { - const scanner = ctx.scanner; - scanner.advance(); - const buffer = []; - let foundClosingQuote = false; - while (!scanner.isAtEnd()) { - const ch = scanner.peek(); - if (ch === "\\") { - scanner.advance(); - const next = scanner.peek(); - if (next === null) { - throw new TokenizerError(`Unterminated regex literal at ${start.offset}`, scanner.position()); - } - if (next === '"') { - buffer.push('"'); - } else { - buffer.push("\\"); - buffer.push(next); - } - scanner.advance(); - continue; - } - if (ch === '"') { - scanner.advance(); - foundClosingQuote = true; - break; - } - buffer.push(scanner.advance()); - } - if (!foundClosingQuote) { - throw new TokenizerError(`Unterminated regex literal at ${start.offset}`, scanner.position()); - } - return { - kind: tokenKeywords.Regex, - value: buffer.join(""), - start, - end: scanner.position() - }; -}; -function parseDispatch(ctx) { - const scanner = ctx.scanner; - const start = scanner.position(); - scanner.advance(); - const next = scanner.peek(); - if (next === "(") { - scanner.advance(); - return { kind: tokenKeywords.AnonFnStart, start, end: scanner.position() }; - } - if (next === '"') { - return parseRegexLiteral(ctx, start); - } - if (next === "'") { - scanner.advance(); - return { kind: tokenKeywords.VarQuote, start, end: scanner.position() }; - } - if (next === "{") { - throw new TokenizerError("Set literals are not yet supported", start); - } - throw new TokenizerError(`Unknown dispatch character: #${next ?? "EOF"}`, start); -} -function parseCharToken(kind, value) { - return (ctx) => { - const scanner = ctx.scanner; - const start = scanner.position(); - scanner.advance(); - return { - kind, - value, - start, - end: scanner.position() - }; - }; -} -function parseTilde(ctx) { - const scanner = ctx.scanner; - const start = scanner.position(); - scanner.advance(); - const nextChar = scanner.peek(); - if (!nextChar) { - throw new TokenizerError(`Unexpected end of input while parsing unquote at ${start.offset}`, start); - } - if (isAt(nextChar)) { - scanner.advance(); - return { - kind: tokenKeywords.UnquoteSplicing, - value: tokenSymbols.UnquoteSplicing, - start, - end: scanner.position() - }; - } - return { - kind: tokenKeywords.Unquote, - value: tokenSymbols.Unquote, - start, - end: scanner.position() - }; -} -var tokenParseEntries = [ - [isWhitespace, parseWhitespace], - [isComment, parseComment], - [isLParen, parseCharToken(tokenKeywords.LParen, tokenSymbols.LParen)], - [isRParen, parseCharToken(tokenKeywords.RParen, tokenSymbols.RParen)], - [isLBracket, parseCharToken(tokenKeywords.LBracket, tokenSymbols.LBracket)], - [isRBracket, parseCharToken(tokenKeywords.RBracket, tokenSymbols.RBracket)], - [isLBrace, parseCharToken(tokenKeywords.LBrace, tokenSymbols.LBrace)], - [isRBrace, parseCharToken(tokenKeywords.RBrace, tokenSymbols.RBrace)], - [isDoubleQuote, parseString], - [isKeywordStart, parseKeyword], - [isNumberToken, parseNumber], - [isSingleQuote, parseCharToken(tokenKeywords.Quote, tokenSymbols.Quote)], - [ - isBacktick, - parseCharToken(tokenKeywords.Quasiquote, tokenSymbols.Quasiquote) - ], - [isTilde, parseTilde], - [isAt, parseDerefToken], - [isCaret, parseMetaToken], - [isHash, parseDispatch] -]; -function parseNextToken(ctx) { - const scanner = ctx.scanner; - const char = scanner.peek(); - const entry = tokenParseEntries.find(([check]) => check(char, ctx)); - if (entry) { - const [, parse] = entry; - return parse(ctx); - } - return parseSymbol(ctx); -} -function parseAllTokens(ctx) { - const tokens = []; - let error = undefined; - try { - while (!ctx.scanner.isAtEnd()) { - const result = parseNextToken(ctx); - if (!result) { - break; - } - if (result.kind === tokenKeywords.Whitespace) { - continue; - } - tokens.push(result); - } - } catch (e) { - error = e; - } - const parsed = { - tokens, - scanner: ctx.scanner, - error - }; - return parsed; -} -function getTokenValue(token) { - if ("value" in token) { - return token.value; - } - return ""; -} -function tokenize(input) { - const inputLength = input.length; - const scanner = makeCharScanner(input); - const tokenizationContext = { - scanner - }; - const tokensResult = parseAllTokens(tokenizationContext); - if (tokensResult.error) { - throw tokensResult.error; - } - if (tokensResult.scanner.position().offset !== inputLength) { - throw new TokenizerError(`Unexpected end of input, expected ${inputLength} characters, got ${tokensResult.scanner.position().offset}`, tokensResult.scanner.position()); - } - return tokensResult.tokens; -} - -// src/core/reader.ts -function readAtom(ctx) { - const scanner = ctx.scanner; - const token = scanner.peek(); - if (!token) { - throw new ReaderError("Unexpected end of input", scanner.position()); - } - switch (token.kind) { - case tokenKeywords.Symbol: - return readSymbol(scanner); - case tokenKeywords.String: { - scanner.advance(); - const val = { kind: "string", value: token.value }; - setPos(val, { start: token.start.offset, end: token.end.offset }); - return val; - } - case tokenKeywords.Number: { - scanner.advance(); - const val = { kind: "number", value: token.value }; - setPos(val, { start: token.start.offset, end: token.end.offset }); - return val; - } - case tokenKeywords.Keyword: { - scanner.advance(); - const kwName = token.value; - let val; - if (kwName.startsWith("::")) { - const rest = kwName.slice(2); - if (rest.includes("/")) { - const slashIdx = rest.indexOf("/"); - const alias = rest.slice(0, slashIdx); - const localName = rest.slice(slashIdx + 1); - const fullNs = ctx.aliases.get(alias); - if (!fullNs) { - throw new ReaderError(`No namespace alias '${alias}' found for ::${alias}/${localName}`, token, { start: token.start.offset, end: token.end.offset }); - } - val = { kind: "keyword", name: `:${fullNs}/${localName}` }; - } else { - val = { kind: "keyword", name: `:${ctx.namespace}/${rest}` }; - } - } else { - val = { kind: "keyword", name: kwName }; - } - setPos(val, { start: token.start.offset, end: token.end.offset }); - return val; - } - } - throw new ReaderError(`Unexpected token: ${token.kind}`, token, { - start: token.start.offset, - end: token.end.offset - }); -} -var readQuote = (ctx) => { - const scanner = ctx.scanner; - const token = scanner.peek(); - if (!token) { - throw new ReaderError("Unexpected end of input while parsing quote", scanner.position()); - } - scanner.advance(); - const value = readForm(ctx); - if (!value) { - throw new ReaderError(`Unexpected token: ${getTokenValue(token)}`, token); - } - return { kind: valueKeywords.list, value: [cljSymbol("quote"), value] }; -}; -var readQuasiquote = (ctx) => { - const scanner = ctx.scanner; - const token = scanner.peek(); - if (!token) { - throw new ReaderError("Unexpected end of input while parsing quasiquote", scanner.position()); - } - scanner.advance(); - const value = readForm(ctx); - if (!value) { - throw new ReaderError(`Unexpected token: ${getTokenValue(token)}`, token); - } - return { kind: valueKeywords.list, value: [cljSymbol("quasiquote"), value] }; -}; -var readUnquote = (ctx) => { - const scanner = ctx.scanner; - const token = scanner.peek(); - if (!token) { - throw new ReaderError("Unexpected end of input while parsing unquote", scanner.position()); - } - scanner.advance(); - const value = readForm(ctx); - if (!value) { - throw new ReaderError(`Unexpected token: ${getTokenValue(token)}`, token); - } - return { kind: valueKeywords.list, value: [cljSymbol("unquote"), value] }; -}; -var readMeta = (ctx) => { - const scanner = ctx.scanner; - const token = scanner.peek(); - if (!token) { - throw new ReaderError("Unexpected end of input while parsing metadata", scanner.position()); - } - scanner.advance(); - const metaForm = readForm(ctx); - const target = readForm(ctx); - let metaEntries; - if (metaForm.kind === "keyword") { - metaEntries = [[metaForm, cljBoolean(true)]]; - } else if (metaForm.kind === "map") { - metaEntries = metaForm.entries; - } else if (metaForm.kind === "symbol") { - metaEntries = [[cljKeyword(":tag"), metaForm]]; - } else { - throw new ReaderError("Metadata must be a keyword, map, or symbol", token); - } - if (target.kind === "symbol" || target.kind === "list" || target.kind === "vector" || target.kind === "map") { - const existingEntries = target.meta ? target.meta.entries : []; - const result = { ...target, meta: cljMap([...existingEntries, ...metaEntries]) }; - const pos = getPos(target); - if (pos) - setPos(result, pos); - return result; - } - return target; -}; -var readVarQuote = (ctx) => { - const scanner = ctx.scanner; - const token = scanner.peek(); - if (!token) { - throw new ReaderError("Unexpected end of input while parsing var quote", scanner.position()); - } - scanner.advance(); - const value = readForm(ctx); - return cljList([cljSymbol("var"), value]); -}; -var readDeref = (ctx) => { - const scanner = ctx.scanner; - const token = scanner.peek(); - if (!token) { - throw new ReaderError("Unexpected end of input while parsing deref", scanner.position()); - } - scanner.advance(); - const value = readForm(ctx); - if (!value) { - throw new ReaderError(`Unexpected token: ${getTokenValue(token)}`, token); - } - return { kind: valueKeywords.list, value: [cljSymbol("deref"), value] }; -}; -var readUnquoteSplicing = (ctx) => { - const scanner = ctx.scanner; - const token = scanner.peek(); - if (!token) { - throw new ReaderError("Unexpected end of input while parsing unquote splicing", scanner.position()); - } - scanner.advance(); - const value = readForm(ctx); - if (!value) { - throw new ReaderError(`Unexpected token: ${getTokenValue(token)}`, token); - } - return { - kind: valueKeywords.list, - value: [cljSymbol("unquote-splicing"), value] - }; -}; -var isClosingToken = (token) => { - return [ - tokenKeywords.RParen, - tokenKeywords.RBracket, - tokenKeywords.RBrace - ].includes(token.kind); -}; -var collectionReader = (valueType, closeToken) => { - return function(ctx) { - const scanner = ctx.scanner; - const startToken = scanner.peek(); - if (!startToken) { - throw new ReaderError("Unexpected end of input while parsing collection", scanner.position()); - } - scanner.advance(); - const values = []; - let pairMatched = false; - let closingEnd; - while (!scanner.isAtEnd()) { - const token = scanner.peek(); - if (!token) { - break; - } - if (isClosingToken(token) && token.kind !== closeToken) { - throw new ReaderError(`Expected '${closeToken}' to close ${valueType} started at line ${startToken.start.line} column ${startToken.start.col}, but got '${getTokenValue(token)}' at line ${token.start.line} column ${token.start.col}`, token, { start: token.start.offset, end: token.end.offset }); - } - if (token.kind === closeToken) { - closingEnd = token.end.offset; - scanner.advance(); - pairMatched = true; - break; - } - const value = readForm(ctx); - values.push(value); - } - if (!pairMatched) { - throw new ReaderError(`Unmatched ${valueType} started at line ${startToken.start.line} column ${startToken.start.col}`, scanner.peek()); - } - const result = { kind: valueType, value: values }; - if (closingEnd !== undefined) { - setPos(result, { start: startToken.start.offset, end: closingEnd }); - } - return result; - }; -}; -var readList = collectionReader("list", tokenKeywords.RParen); -var readVector = collectionReader("vector", tokenKeywords.RBracket); -var readSymbol = (scanner) => { - const token = scanner.peek(); - if (!token) { - throw new ReaderError("Unexpected end of input", scanner.position()); - } - if (token.kind !== tokenKeywords.Symbol) { - throw new ReaderError(`Unexpected token: ${getTokenValue(token)}`, token, { - start: token.start.offset, - end: token.end.offset - }); - } - scanner.advance(); - let val; - switch (token.value) { - case "true": - case "false": - val = cljBoolean(token.value === "true"); - break; - case "nil": - val = cljNil(); - break; - default: - val = cljSymbol(token.value); - } - setPos(val, { start: token.start.offset, end: token.end.offset }); - return val; -}; -var readMap = (ctx) => { - const scanner = ctx.scanner; - const startToken = scanner.peek(); - if (!startToken) { - throw new ReaderError("Unexpected end of input while parsing map", scanner.position()); - } - let pairMatched = false; - let closingEnd; - scanner.advance(); - const entries = []; - while (!scanner.isAtEnd()) { - const token = scanner.peek(); - if (!token) { - break; - } - if (isClosingToken(token) && token.kind !== tokenKeywords.RBrace) { - throw new ReaderError(`Expected '}' to close map started at line ${startToken.start.line} column ${startToken.start.col}, but got '${token.kind}' at line ${token.start.line} column ${token.start.col}`, token, { start: token.start.offset, end: token.end.offset }); - } - if (token.kind === tokenKeywords.RBrace) { - closingEnd = token.end.offset; - scanner.advance(); - pairMatched = true; - break; - } - const key = readForm(ctx); - const nextToken = scanner.peek(); - if (!nextToken) { - throw new ReaderError(`Expected value in map started at line ${startToken.start.line} column ${startToken.start.col}, but got end of input`, scanner.position()); - } - if (nextToken.kind === tokenKeywords.RBrace) { - throw new ReaderError(`Map started at line ${startToken.start.line} column ${startToken.start.col} has key ${key.kind} but no value`, scanner.position()); - } - const value = readForm(ctx); - if (!value) { - break; - } - entries.push([key, value]); - } - if (!pairMatched) { - throw new ReaderError(`Unmatched map started at line ${startToken.start.line} column ${startToken.start.col}`, scanner.peek()); - } - const result = { kind: valueKeywords.map, entries }; - if (closingEnd !== undefined) { - setPos(result, { start: startToken.start.offset, end: closingEnd }); - } - return result; -}; -function collectAnonFnParams(forms) { - let maxIndex = 0; - let hasRest = false; - function walk(form) { - switch (form.kind) { - case "symbol": { - const name = form.name; - if (name === "%" || name === "%1") { - maxIndex = Math.max(maxIndex, 1); - } else if (/^%[2-9]$/.test(name)) { - maxIndex = Math.max(maxIndex, parseInt(name[1])); - } else if (name === "%&") { - hasRest = true; - } - break; - } - case "list": - case "vector": - for (const child of form.value) - walk(child); - break; - case "map": - for (const [k, v] of form.entries) { - walk(k); - walk(v); - } - break; - default: - break; - } - } - for (const form of forms) - walk(form); - return { maxIndex, hasRest }; -} -function substituteAnonFnParams(form) { - switch (form.kind) { - case "symbol": { - const name = form.name; - if (name === "%" || name === "%1") - return cljSymbol("p1"); - if (/^%[2-9]$/.test(name)) - return cljSymbol(`p${name[1]}`); - if (name === "%&") - return cljSymbol("rest"); - return form; - } - case "list": - return { ...form, value: form.value.map(substituteAnonFnParams) }; - case "vector": - return { ...form, value: form.value.map(substituteAnonFnParams) }; - case "map": - return { - ...form, - entries: form.entries.map(([k, v]) => [substituteAnonFnParams(k), substituteAnonFnParams(v)]) - }; - default: - return form; - } -} -var readAnonFn = (ctx) => { - const scanner = ctx.scanner; - const startToken = scanner.peek(); - if (!startToken) { - throw new ReaderError("Unexpected end of input while parsing anonymous function", scanner.position()); - } - scanner.advance(); - const bodyForms = []; - let pairMatched = false; - let closingEnd; - while (!scanner.isAtEnd()) { - const token = scanner.peek(); - if (!token) - break; - if (isClosingToken(token) && token.kind !== tokenKeywords.RParen) { - throw new ReaderError(`Expected ')' to close anonymous function started at line ${startToken.start.line} column ${startToken.start.col}, but got '${getTokenValue(token)}' at line ${token.start.line} column ${token.start.col}`, token, { start: token.start.offset, end: token.end.offset }); - } - if (token.kind === tokenKeywords.RParen) { - closingEnd = token.end.offset; - scanner.advance(); - pairMatched = true; - break; - } - if (token.kind === tokenKeywords.AnonFnStart) { - throw new ReaderError("Nested anonymous functions (#(...)) are not allowed", token, { start: token.start.offset, end: token.end.offset }); - } - bodyForms.push(readForm(ctx)); - } - if (!pairMatched) { - throw new ReaderError(`Unmatched anonymous function started at line ${startToken.start.line} column ${startToken.start.col}`, scanner.peek()); - } - const bodyList = { kind: "list", value: bodyForms }; - const { maxIndex, hasRest } = collectAnonFnParams([bodyList]); - const paramSymbols = []; - for (let i = 1;i <= maxIndex; i++) { - paramSymbols.push(cljSymbol(`p${i}`)); - } - if (hasRest) { - paramSymbols.push(cljSymbol("&")); - paramSymbols.push(cljSymbol("rest")); - } - const substitutedBody = substituteAnonFnParams(bodyList); - const result = cljList([ - cljSymbol("fn"), - cljVector(paramSymbols), - substitutedBody - ]); - if (closingEnd !== undefined) { - setPos(result, { start: startToken.start.offset, end: closingEnd }); - } - return result; -}; -function extractInlineFlags2(raw) { - let remaining = raw; - let flags = ""; - const flagGroupRe = /^\(\?([imsx]+)\)/; - let m; - while ((m = flagGroupRe.exec(remaining)) !== null) { - for (const f of m[1]) { - if (f === "x") { - throw new ReaderError("Regex flag (?x) (verbose mode) has no JavaScript equivalent and is not supported", null); - } - if (!flags.includes(f)) - flags += f; - } - remaining = remaining.slice(m[0].length); - } - return { pattern: remaining, flags }; -} -var readRegex = (ctx) => { - const scanner = ctx.scanner; - const token = scanner.peek(); - if (!token || token.kind !== tokenKeywords.Regex) { - throw new ReaderError("Expected regex token", scanner.position()); - } - scanner.advance(); - const { pattern, flags } = extractInlineFlags2(token.value); - const val = cljRegex(pattern, flags); - setPos(val, { start: token.start.offset, end: token.end.offset }); - return val; -}; -function readForm(ctx) { - const scanner = ctx.scanner; - const token = scanner.peek(); - if (!token) { - throw new ReaderError("Unexpected end of input", scanner.position()); - } - switch (token.kind) { - case tokenKeywords.String: - case tokenKeywords.Number: - case tokenKeywords.Keyword: - case tokenKeywords.Symbol: - return readAtom(ctx); - case tokenKeywords.LParen: - return readList(ctx); - case tokenKeywords.LBrace: - return readMap(ctx); - case tokenKeywords.LBracket: - return readVector(ctx); - case tokenKeywords.Quote: - return readQuote(ctx); - case tokenKeywords.Quasiquote: - return readQuasiquote(ctx); - case tokenKeywords.Unquote: - return readUnquote(ctx); - case tokenKeywords.UnquoteSplicing: - return readUnquoteSplicing(ctx); - case tokenKeywords.AnonFnStart: - return readAnonFn(ctx); - case tokenKeywords.Deref: - return readDeref(ctx); - case tokenKeywords.VarQuote: - return readVarQuote(ctx); - case tokenKeywords.Meta: - return readMeta(ctx); - case tokenKeywords.Regex: - return readRegex(ctx); - default: - throw new ReaderError(`Unexpected token: ${getTokenValue(token)} at line ${token.start.line} column ${token.start.col}`, token, { start: token.start.offset, end: token.end.offset }); - } -} -function readForms(input, currentNs = "user", aliases = new Map) { - const withoutComments = input.filter((t) => t.kind !== tokenKeywords.Comment); - const scanner = makeTokenScanner(withoutComments); - const ctx = { - scanner, - namespace: currentNs, - aliases - }; - const values = []; - while (!scanner.isAtEnd()) { - values.push(readForm(ctx)); - } - return values; -} +(defmacro and [& forms] + (if (nil? forms) + true + (if (nil? (seq (rest forms))) + (first forms) + \`(let [v# ~(first forms)] + (if v# (and ~@(rest forms)) v#))))) + +(defmacro or [& forms] + (if (nil? forms) + nil + (if (nil? (seq (rest forms))) + (first forms) + \`(let [v# ~(first forms)] + (if v# v# (or ~@(rest forms))))))) + +(defmacro cond [& clauses] + (if (nil? clauses) + nil + \`(if ~(first clauses) + ~(first (next clauses)) + (cond ~@(rest (rest clauses)))))) + +(defmacro -> [x & forms] + (if (nil? forms) + x + (let [form (first forms) + more (rest forms) + threaded (if (list? form) + \`(~(first form) ~x ~@(rest form)) + \`(~form ~x))] + \`(-> ~threaded ~@more)))) + +(defmacro ->> [x & forms] + (if (nil? forms) + x + (let [form (first forms) + more (rest forms) + threaded (if (list? form) + \`(~(first form) ~@(rest form) ~x) + \`(~form ~x))] + \`(->> ~threaded ~@more)))) + +(defmacro comment + "Ignores body, yields nil" + [& body]) + +(defmacro as-> + [expr name & forms] + \`(let [~name ~expr + ~@(reduce (fn [acc form] (conj acc name form)) [] forms)] + ~name)) + +(defmacro cond-> + [expr & clauses] + (let [g (gensym "cv") + steps (reduce + (fn [acc pair] + (let [test (first pair) + form (second pair) + threaded (if (list? form) + \`(~(first form) ~g ~@(rest form)) + \`(~form ~g))] + (conj acc \`(if ~test ~threaded ~g)))) + [] + (partition-all 2 clauses))] + \`(let [~g ~expr + ~@(reduce (fn [acc step] (conj acc g step)) [] steps)] + ~g))) + +(defmacro cond->> + [expr & clauses] + (let [g (gensym "cv") + steps (reduce + (fn [acc pair] + (let [test (first pair) + form (second pair) + threaded (if (list? form) + \`(~(first form) ~@(rest form) ~g) + \`(~form ~g))] + (conj acc \`(if ~test ~threaded ~g)))) + [] + (partition-all 2 clauses))] + \`(let [~g ~expr + ~@(reduce (fn [acc step] (conj acc g step)) [] steps)] + ~g))) + +(defmacro some-> + [expr & forms] + (if (nil? forms) + expr + \`(let [v# ~expr] + (if (nil? v#) + nil + (some-> (-> v# ~(first forms)) ~@(rest forms)))))) + +(defmacro some->> + [expr & forms] + (if (nil? forms) + expr + \`(let [v# ~expr] + (if (nil? v#) + nil + (some->> (->> v# ~(first forms)) ~@(rest forms)))))) + +(defn constantly + "Returns a function that takes any number of arguments and returns x." + [x] (fn [& _] x)) + +(defn some? + "Returns true if x is not nil, false otherwise" + [x] (not (nil? x))) + +(defn any? + "Returns true for any given argument" + [_x] true) + +(defn complement + "Takes a fn f and returns a fn that takes the same arguments as f, + has the same effects, if any, and returns the opposite truth value." + [f] + (fn + ([] (not (f))) + ([x] (not (f x))) + ([x y] (not (f x y))) + ([x y & zs] (not (apply f x y zs))))) + +(defn juxt + "Takes a set of functions and returns a fn that is the juxtaposition + of those fns. The returned fn takes a variable number of args and + returns a vector containing the result of applying each fn to the args." + [& fns] + (fn [& args] + (reduce (fn [acc f] (conj acc (apply f args))) [] fns))) + +(defn merge + "Returns a map that consists of the rest of the maps conj-ed onto + the first. If a key occurs in more than one map, the mapping from + the latter (left-to-right) will be the mapping in the result." + [& maps] + (if (nil? maps) + nil + (reduce + (fn [acc m] + (if (nil? m) + acc + (if (nil? acc) + m + (reduce + (fn [macc entry] + (assoc macc (first entry) (second entry))) + acc + m)))) + nil + maps))) + +(defn select-keys + "Returns a map containing only those entries in map whose key is in keys." + [m keys] + (if (or (nil? m) (nil? keys)) + {} + (let [missing (gensym)] + (reduce + (fn [acc k] + (let [v (get m k missing)] + (if (= v missing) + acc + (assoc acc k v)))) + {} + keys)))) + +(defn update + "Updates a value in an associative structure where k is a key and f is a + function that will take the old value and any supplied args and return the + new value, and returns a new structure." + [m k f & args] + (let [target (if (nil? m) {} m)] + (assoc target k (if (nil? args) + (f (get target k)) + (apply f (get target k) args))))) + +(defn get-in + "Returns the value in a nested associative structure, where ks is a + sequence of keys. Returns nil if the key is not present, or the not-found + value if supplied." + ([m ks] + (reduce get m ks)) + ([m ks not-found] + (loop [m m, ks (seq ks)] + (if (nil? ks) + m + (if (contains? m (first ks)) + (recur (get m (first ks)) (next ks)) + not-found))))) + +(defn assoc-in + "Associates a value in a nested associative structure, where ks is a + sequence of keys and v is the new value. Returns a new nested structure." + [m [k & ks] v] + (if ks + (assoc m k (assoc-in (get m k) ks v)) + (assoc m k v))) + +(defn update-in + "Updates a value in a nested associative structure, where ks is a + sequence of keys and f is a function that will take the old value and any + supplied args and return the new value. Returns a new nested structure." + [m ks f & args] + (assoc-in m ks (apply f (get-in m ks) args))) + +(defn fnil + "Takes a function f, and returns a function that calls f, replacing + a nil first argument with x, optionally nil second with y, nil third with z." + ([f x] + (fn [a & more] + (apply f (if (nil? a) x a) more))) + ([f x y] + (fn [a b & more] + (apply f (if (nil? a) x a) (if (nil? b) y b) more))) + ([f x y z] + (fn [a b c & more] + (apply f (if (nil? a) x a) (if (nil? b) y b) (if (nil? c) z c) more)))) + +(defn frequencies + "Returns a map from distinct items in coll to the number of times they appear." + [coll] + (if (nil? coll) + {} + (reduce + (fn [counts item] + (assoc counts item (inc (get counts item 0)))) + {} + coll))) + +(defn group-by + "Returns a map of the elements of coll keyed by the result of f on each + element. The value at each key is a vector of matching elements." + [f coll] + (if (nil? coll) + {} + (reduce + (fn [acc item] + (let [k (f item)] + (assoc acc k (conj (get acc k []) item)))) + {} + coll))) + +(defn distinct + "Returns a vector of the elements of coll with duplicates removed, + preserving first-seen order." + [coll] + (if (nil? coll) + [] + (get + (reduce + (fn [state item] + (let [seen (get state 0) + out (get state 1)] + (if (get seen item false) + state + [(assoc seen item true) (conj out item)]))) + [{} []] + coll) + 1))) + +(defn flatten-step + "Internal helper for flatten." + [v] + (if (or (list? v) (vector? v)) + (reduce + (fn [acc item] + (into acc (flatten-step item))) + [] + v) + [v])) + +(defn flatten + "Takes any nested combination of sequential things (lists/vectors) and + returns their contents as a single flat vector." + [x] + (if (nil? x) + [] + (flatten-step x))) + +(defn reduce-kv + "Reduces an associative structure. f should be a function of 3 + arguments: accumulator, key/index, value." + [f init coll] + (cond + (map? coll) + (reduce + (fn [acc entry] + (f acc (first entry) (second entry))) + init + coll) + + (vector? coll) + (loop [idx 0 + acc init] + (if (< idx (count coll)) + (recur (inc idx) (f acc idx (nth coll idx))) + acc)) + + :else + (throw + (ex-info + "reduce-kv expects a map or vector" + {:coll coll})))) + +(defn sort-compare + "Internal helper: normalizes comparator results." + [cmp a b] + (let [r (cmp a b)] + (if (number? r) + (< r 0) + r))) + +(defn insert-sorted + "Internal helper for insertion-sort based sort implementation." + [cmp x sorted] + (loop [left [] + right sorted] + (if (nil? (seq right)) + (conj left x) + (let [y (first right)] + (if (sort-compare cmp x y) + (into (conj left x) right) + (recur (conj left y) (rest right))))))) + +(defn sort + "Returns the items in coll in sorted order. With no comparator, sorts + ascending using <. Comparator may return boolean or number." + ([coll] (sort < coll)) + ([cmp coll] + (if (nil? coll) + [] + (reduce + (fn [acc item] + (insert-sorted cmp item acc)) + [] + coll)))) + +(defn sort-by + "Returns a sorted sequence of items in coll, where the sort order is + determined by comparing (keyfn item)." + ([keyfn coll] (sort-by keyfn < coll)) + ([keyfn cmp coll] + (sort + (fn [a b] + (cmp (keyfn a) (keyfn b))) + coll))) + +(def not-any? (comp not some)) + +(defn not-every? + "Returns false if (pred x) is logical true for every x in + coll, else true." + [pred coll] (not (every? pred coll))) + +;; ── Transducer protocol ────────────────────────────────────────────────────── + +;; into: 2-arity uses reduce+conj; 3-arity uses transduce +(defn into + "Returns a new coll consisting of to-coll with all of the items of + from-coll conjoined. A transducer may be supplied." + ([to from] (reduce conj to from)) + ([to xf from] (transduce xf conj to from))) + +;; sequence: materialise a transducer over a collection into a seq (list) +(defn sequence + "Coerces coll to a (possibly empty) sequence, if it is not already + one. Will not force a seq. (sequence nil) yields (), When a + transducer is supplied, returns a lazy sequence of applications of + the transform to the items in coll" + ([coll] (apply list (into [] coll))) + ([xf coll] (apply list (into [] xf coll)))) + +(defn completing + "Takes a reducing function f of 2 args and returns a fn suitable for + transduce by adding an arity-1 signature that calls cf (default - + identity) on the result argument." + ([f] (completing f identity)) + ([f cf] + (fn + ([] (f)) + ([x] (cf x)) + ([x y] (f x y))))) + +;; map: 1-arg returns transducer; 2-arg is eager; 3+-arg zips collections +(defn map + "Returns a sequence consisting of the result of applying f to the set + of first items of each coll, followed by applying f to the set of + second items in each coll, until any one of the colls is exhausted. + Any remaining items in other colls are ignored. Returns a transducer + when no collection is provided." + ([f] + (fn [rf] + (fn + ([] (rf)) + ([result] (rf result)) + ([result input] (rf result (f input)))))) + ([f coll] + (lazy-seq + (when-let [s (seq coll)] + (cons (f (first s)) (map f (rest s)))))) + ([f c1 c2] + (loop [s1 (seq c1) + s2 (seq c2) + acc []] + (if (or (nil? s1) (nil? s2)) + acc + (recur + (next s1) + (next s2) + (conj acc (f (first s1) (first s2))))))) + ([f c1 c2 & colls] + (loop [seqs (map seq (cons c1 (cons c2 colls))) + acc []] + (if (some nil? seqs) + acc + (recur (map next seqs) (conj acc (apply f (map first seqs)))))))) + +;; filter: 1-arg returns transducer; 2-arg is eager +(defn filter + "Returns a sequence of the items in coll for which + (pred item) returns logical true. pred must be free of side-effects. + Returns a transducer when no collection is provided." + ([pred] + (fn [rf] + (fn + ([] (rf)) + ([result] (rf result)) + ([result input] + (if (pred input) + (rf result input) + result))))) + ([pred coll] + (lazy-seq + (when-let [s (seq coll)] + (if (pred (first s)) + (cons (first s) (filter pred (rest s))) + (filter pred (rest s))))))) + +(defn remove + "Returns a lazy sequence of the items in coll for which + (pred item) returns logical false. pred must be free of side-effects. + Returns a transducer when no collection is provided." + ([pred] (filter (complement pred))) + ([pred coll] + (filter (complement pred) coll))) + + + +;; take: stateful transducer; signals early termination after n items +;; r > 0 → keep going; r = 0 → take last item and stop; r < 0 → already past limit, stop +(defn take + "Returns a sequence of the first n items in coll, or all items if + there are fewer than n. Returns a stateful transducer when + no collection is provided." + ([n] + (fn [rf] + (let [remaining (volatile! n)] + (fn + ([] (rf)) + ([result] (rf result)) + ([result input] + (let [n @remaining + nrem (vswap! remaining dec) + result (if (pos? n) + (rf result input) + result)] + (if (not (pos? nrem)) + (ensure-reduced result) + result))))))) + ([n coll] + (lazy-seq + (when (pos? n) + (when-let [s (seq coll)] + (cons (first s) (take (dec n) (rest s)))))))) + +;; take-while: stateless transducer; emits reduced when pred fails +(defn take-while + "Returns a sequence of successive items from coll while + (pred item) returns logical true. pred must be free of side-effects. + Returns a transducer when no collection is provided." + ([pred] + (fn [rf] + (fn + ([] (rf)) + ([result] (rf result)) + ([result input] + (if (pred input) + (rf result input) + (reduced result)))))) + ([pred coll] + (lazy-seq + (when-let [s (seq coll)] + (when (pred (first s)) + (cons (first s) (take-while pred (rest s)))))))) + +;; drop: stateful transducer; skips first n items +;; r >= 0 → still skipping; r < 0 → past the drop zone, start taking +(defn drop + "Returns a sequence of all but the first n items in coll. + Returns a stateful transducer when no collection is provided." + ([n] + (fn [rf] + (let [remaining (volatile! n)] + (fn + ([] (rf)) + ([result] (rf result)) + ([result input] + (let [rem @remaining] + (vswap! remaining dec) + (if (pos? rem) + result + (rf result input)))))))) + ([n coll] + (if (pos? n) + (lazy-seq (drop (dec n) (rest coll))) + (lazy-seq (seq coll))))) + +(defn drop-last + "Return a sequence of all but the last n (default 1) items in coll" + ([coll] (drop-last 1 coll)) + ([n coll] (map (fn [x _] x) coll (drop n coll)))) + +(defn take-last + "Returns a sequence of the last n items in coll. Depending on the type + of coll may be no better than linear time. For vectors, see also subvec." + [n coll] + (loop [s (seq coll), lead (seq (drop n coll))] + (if lead + (recur (next s) (next lead)) + s))) + +;; drop-while: stateful transducer; passes through once pred fails +(defn drop-while + "Returns a sequence of the items in coll starting from the + first item for which (pred item) returns logical false. Returns a + stateful transducer when no collection is provided." + ([pred] + (fn [rf] + (let [dropping (volatile! true)] + (fn + ([] (rf)) + ([result] (rf result)) + ([result input] + (if (and @dropping (pred input)) + result + (do + (vreset! dropping false) + (rf result input)))))))) + ([pred coll] + (lazy-seq + (let [s (seq coll)] + (if (and s (pred (first s))) + (drop-while pred (rest s)) + s))))) + +;; map-indexed: stateful transducer; passes index and item to f +(defn map-indexed + "Returns a sequence consisting of the result of applying f to 0 + and the first item of coll, followed by applying f to 1 and the second + item in coll, etc, until coll is exhausted. Thus function f should + accept 2 arguments, index and item. Returns a stateful transducer when + no collection is provided." + ([f] + (fn [rf] + (let [i (volatile! -1)] + (fn + ([] (rf)) + ([result] (rf result)) + ([result input] + (rf result (f (vswap! i inc) input))))))) + ([f coll] + (letfn [(step [i s] + (lazy-seq + (when-let [xs (seq s)] + (cons (f i (first xs)) (step (inc i) (rest xs))))))] + (step 0 coll)))) + +;; dedupe: stateful transducer; removes consecutive duplicates +(defn dedupe + "Returns a sequence removing consecutive duplicates in coll. + Returns a transducer when no collection is provided." + ([] + (fn [rf] + (let [pv (volatile! ::none)] + (fn + ([] (rf)) + ([result] (rf result)) + ([result input] + (let [prior @pv] + (vreset! pv input) + (if (= prior input) + result + (rf result input)))))))) + ([coll] + (sequence (dedupe) coll))) + +;; partition-all: stateful transducer; groups items into vectors of size n +(defn partition-all + "Returns a sequence of lists like partition, but may include + partitions with fewer than n items at the end. Returns a stateful + transducer when no collection is provided." + ([n] + (fn [rf] + (let [buf (volatile! [])] + (fn + ([] (rf)) + ([result] + (let [b @buf] + (vreset! buf []) + (if (empty? b) + (rf result) + (rf (unreduced (rf result b)))))) + ([result input] + (let [nb (conj @buf input)] + (if (= (count nb) n) + (do + (vreset! buf []) + (rf result nb)) + (do + (vreset! buf nb) + result)))))))) + ([n coll] + (sequence (partition-all n) coll))) -// src/clojure/generated/clojure-core-source.ts -var clojure_coreSource = `(ns clojure.core) +;; ── Documentation ──────────────────────────────────────────────────────────── -(defmacro defn [name & fdecl] - (let [doc (if (string? (first fdecl)) (first fdecl) nil) - rest-decl (if doc (rest fdecl) fdecl) - arglists (if (vector? (first rest-decl)) - (vector (first rest-decl)) - (reduce (fn [acc arity] (conj acc (first arity))) [] rest-decl))] - (if doc - \`(def ~name (with-meta (fn ~@rest-decl) {:doc ~doc :arglists '~arglists})) - \`(def ~name (with-meta (fn ~@rest-decl) {:arglists '~arglists}))))) +(defmacro doc [sym] + \`(let [v# (var ~sym) + m# (meta v#) + d# (:doc m#) + args# (:arglists m#) + args-str# (when args# + (str "(" + (reduce + (fn [acc# a#] + (if (= acc# "") + (str a#) + (str acc# " \\n " a#))) + "" + args#) + ")"))] + (println (str "-------------------------\\n" + ~(str sym) "\\n" + (if args-str# (str args-str# "\\n") "") + " " (or d# "No documentation available."))))) +(defn make-err + "Creates an error map with type, message, data and optionally cause" + ([type message] (make-err type message nil nil)) + ([type message data] (make-err type message data nil)) + ([type message data cause] {:type type :message message :data data :cause cause})) -(defn vary-meta - "Returns an object of the same type and value as obj, with - (apply f (meta obj) args) as its metadata." - [obj f & args] - (with-meta obj (apply f (meta obj) args))) +;; ── Sequence utilities ────────────────────────────────────────────────────── -(defn next - "Returns a seq of the items after the first. Calls seq on its - argument. If there are no more items, returns nil." +(defn butlast + "Return a seq of all but the last item in coll, in linear time" [coll] - (seq (rest coll))) - -(defn not - "Returns true if x is logical false, false otherwise." - [x] (if x false true)) + (loop [ret [] s (seq coll)] + (if (next s) + (recur (conj ret (first s)) (next s)) + (seq ret)))) -(defn second +(defn fnext "Same as (first (next x))" + [x] (first (next x))) + +(defn nfirst + "Same as (next (first x))" + [x] (next (first x))) + +(defn nnext + "Same as (next (next x))" + [x] (next (next x))) + +(defn nthrest + "Returns the nth rest of coll, coll when n is 0." + [coll n] + (loop [n n xs coll] + (if (and (pos? n) (seq xs)) + (recur (dec n) (rest xs)) + xs))) + +(defn nthnext + "Returns the nth next of coll, (seq coll) when n is 0." + [coll n] + (loop [n n xs (seq coll)] + (if (and (pos? n) xs) + (recur (dec n) (next xs)) + xs))) + +(defn list* + "Creates a new seq containing the items prepended to the rest, the + last of which will be treated as a sequence." + ([args] (seq args)) + ([a args] (cons a args)) + ([a b args] (cons a (cons b args))) + ([a b c args] (cons a (cons b (cons c args)))) + ([a b c d & more] + (cons a (cons b (cons c (apply list* d more)))))) + +(defn mapv + "Returns a vector consisting of the result of applying f to the + set of first items of each coll, followed by applying f to the set + of second items in each coll, until any one of the colls is exhausted." + ([f coll] (into [] (map f) coll)) + ([f c1 c2] (into [] (map f c1 c2))) + ([f c1 c2 c3] (into [] (map f c1 c2 c3))) + ([f c1 c2 c3 & colls] (into [] (apply map f c1 c2 c3 colls)))) + +(defn filterv + "Returns a vector of the items in coll for which + (pred item) returns logical true." + [pred coll] + (into [] (filter pred) coll)) + +(defn run! + "Runs the supplied procedure (via reduce), for purposes of side + effects, on successive items in the collection. Returns nil." + [proc coll] + (reduce (fn [_ x] (proc x) nil) nil coll)) + +(defn keep + "Returns a sequence of the non-nil results of (f item). Note, + this means false return values will be included. f must be free of + side-effects. Returns a transducer when no collection is provided." + ([f] + (fn [rf] + (fn + ([] (rf)) + ([result] (rf result)) + ([result input] + (let [v (f input)] + (if (nil? v) + result + (rf result v))))))) + ([f coll] + (lazy-seq + (when-let [s (seq coll)] + (let [v (f (first s))] + (if (nil? v) + (keep f (rest s)) + (cons v (keep f (rest s))))))))) + +(defn keep-indexed + "Returns a sequence of the non-nil results of (f index item). Note, + this means false return values will be included. f must be free of + side-effects. Returns a stateful transducer when no collection is provided." + ([f] + (fn [rf] + (let [i (volatile! -1)] + (fn + ([] (rf)) + ([result] (rf result)) + ([result input] + (let [v (f (vswap! i inc) input)] + (if (nil? v) + result + (rf result v)))))))) + ([f coll] + (letfn [(step [i s] + (lazy-seq + (when-let [xs (seq s)] + (let [v (f i (first xs))] + (if (nil? v) + (step (inc i) (rest xs)) + (cons v (step (inc i) (rest xs))))))))] + (step 0 coll)))) + +(defn mapcat + "Returns the result of applying concat to the result of applying map + to f and colls. Thus function f should return a collection. Returns + a transducer when no collections are provided." + ([f] + (fn [rf] + (let [inner ((map f) (fn + ([] (rf)) + ([result] (rf result)) + ([result input] + (reduce rf result input))))] + inner))) + ([f coll] + (lazy-seq + (when-let [s (seq coll)] + (concat (f (first s)) (mapcat f (rest s)))))) + ([f coll & more] + (apply concat (apply map f coll more)))) + +(defn interleave + "Returns a lazy sequence of the first item in each coll, then the second etc. + Stops as soon as any coll is exhausted." + ([c1 c2] + (lazy-seq + (let [s1 (seq c1) s2 (seq c2)] + (when (and s1 s2) + (cons (first s1) (cons (first s2) (interleave (rest s1) (rest s2)))))))) + ([c1 c2 & colls] + (lazy-seq + (let [seqs (map seq (cons c1 (cons c2 colls)))] + (when (every? some? seqs) + (concat (map first seqs) (apply interleave (map rest seqs)))))))) + +(defn interpose + "Returns a sequence of the elements of coll separated by sep. + Returns a transducer when no collection is provided." + ([sep] + (fn [rf] + (let [started (volatile! false)] + (fn + ([] (rf)) + ([result] (rf result)) + ([result input] + (if @started + (let [sepr (rf result sep)] + (if (reduced? sepr) + sepr + (rf sepr input))) + (do + (vreset! started true) + (rf result input)))))))) + ([sep coll] + (drop 1 (interleave (repeat sep) coll)))) + +;; ── Lazy concat (shadows native eager concat) ────────────────────────────── +(defn concat + "Returns a lazy seq representing the concatenation of the elements in the + supplied colls." + ([] nil) + ([x] (lazy-seq (seq x))) + ([x y] + (lazy-seq + (let [s (seq x)] + (if s + (cons (first s) (concat (rest s) y)) + (seq y))))) + ([x y & zs] + (let [cat (fn cat [xy zs] + (lazy-seq + (let [xys (seq xy)] + (if xys + (cons (first xys) (cat (rest xys) zs)) + (when (seq zs) + (cat (first zs) (next zs)))))))] + (cat (concat x y) zs)))) + +(defn iterate + "Returns a lazy sequence of x, (f x), (f (f x)) etc. + With 3 args, returns a finite sequence of n items (backwards compat)." + ([f x] + (lazy-seq (cons x (iterate f (f x))))) + ([f x n] + (loop [i 0 v x acc []] + (if (< i n) + (recur (inc i) (f v) (conj acc v)) + acc)))) + +(defn repeatedly + "Takes a function of no args, presumably with side effects, and + returns a lazy infinite sequence of calls to it. + With 2 args (n f), returns a finite sequence of n calls." + ([f] (lazy-seq (cons (f) (repeatedly f)))) + ([n f] + (loop [i 0 acc []] + (if (< i n) + (recur (inc i) (conj acc (f))) + acc)))) + +(defn cycle + "Returns a lazy infinite sequence of repetitions of the items in coll. + With 2 args (n coll), returns a finite sequence (backwards compat)." + ([coll] + (lazy-seq + (when (seq coll) + (concat coll (cycle coll))))) + ([n coll] + (let [s (into [] coll)] + (loop [i 0 acc []] + (if (< i n) + (recur (inc i) (into acc s)) + acc))))) + +(defn repeat + "Returns a lazy infinite sequence of xs. + With 2 args (n x), returns a finite sequence of n copies." + ([x] (lazy-seq (cons x (repeat x)))) + ([n x] (repeat* n x))) + +(defn range + "Returns a lazy infinite sequence of integers from 0. + With args, returns a finite sequence (delegates to native range*)." + ([] (iterate inc 0)) + ([end] (range* end)) + ([start end] (range* start end)) + ([start end step] (range* start end step))) + +(defn newline + "Writes a newline to *out*." + [] (println "")) + +(defn dorun + "Forces realization of a (possibly lazy) sequence. Walks the sequence + without retaining the head. Returns nil." [coll] - (first (next coll))) + (when (seq coll) + (recur (rest coll)))) +(defn doall + "Forces realization of a (possibly lazy) sequence. Unlike dorun, + retains the head and returns the seq." + [coll] + (dorun coll) + coll) -(defmacro when [condition & body] - \`(if ~condition (do ~@body) nil)) +(defn take-nth + "Returns a sequence of every nth item in coll. Returns a stateful + transducer when no collection is provided." + ([n] + (fn [rf] + (let [i (volatile! -1)] + (fn + ([] (rf)) + ([result] (rf result)) + ([result input] + (let [idx (vswap! i inc)] + (if (zero? (mod idx n)) + (rf result input) + result))))))) + ([n coll] + (sequence (take-nth n) coll))) + +(defn partition + "Returns a sequence of lists of n items each, at offsets step + apart. If step is not supplied, defaults to n, i.e. the partitions + do not overlap. If a pad collection is supplied, use its elements as + necessary to complete last partition up to n items. In case there are + not enough padding elements, return a partition with less than n items." + ([n coll] (partition n n coll)) + ([n step coll] + (loop [s (seq coll) acc []] + (if (nil? s) + acc + (let [p (into [] (take n) s)] + (if (< (count p) n) + acc + (recur (seq (drop step s)) (conj acc p))))))) + ([n step pad coll] + (loop [s (seq coll) acc []] + (if (nil? s) + acc + (let [p (into [] (take n) s)] + (if (< (count p) n) + (conj acc (into [] (take n) (concat p pad))) + (recur (seq (drop step s)) (conj acc p)))))))) + +(defn partition-by + "Applies f to each value in coll, splitting it each time f returns a + new value. Returns a sequence of partitions. Returns a stateful + transducer when no collection is provided." + ([f] + (fn [rf] + (let [pv (volatile! ::none) + buf (volatile! [])] + (fn + ([] (rf)) + ([result] + (let [b @buf] + (vreset! buf []) + (if (empty? b) + (rf result) + (rf (unreduced (rf result b)))))) + ([result input] + (let [v (f input) + p @pv] + (vreset! pv v) + (if (or (= p ::none) (= v p)) + (do (vswap! buf conj input) result) + (let [b @buf] + (vreset! buf [input]) + (rf result b))))))))) + ([f coll] + (lazy-seq + (when-let [s (seq coll)] + (let [fv (f (first s)) + run (into [] (cons (first s) (take-while #(= (f %) fv) (next s)))) + remaining (drop-while #(= (f %) fv) (next s))] + (cons run (partition-by f remaining))))))) + +(defn reductions + "Returns a sequence of the intermediate values of the reduction (as + by reduce) of coll by f, starting with init." + ([f coll] + (if (empty? coll) + (list (f)) + (reductions f (first coll) (rest coll)))) + ([f init coll] + (loop [acc [init] val init s (seq coll)] + (if (nil? s) + acc + (let [nval (f val (first s))] + (if (reduced? nval) + (conj acc (unreduced nval)) + (recur (conj acc nval) nval (next s)))))))) -(defmacro when-not [condition & body] - \`(if ~condition nil (do ~@body))) +(defn split-at + "Returns a vector of [(take n coll) (drop n coll)]" + [n coll] + [(into [] (take n) coll) (into [] (drop n) coll)]) -(defmacro if-let - ([bindings then] \`(if-let ~bindings ~then nil)) +(defn split-with + "Returns a vector of [(take-while pred coll) (drop-while pred coll)]" + [pred coll] + [(into [] (take-while pred) coll) (into [] (drop-while pred) coll)]) + +(defn merge-with + "Returns a map that consists of the rest of the maps conj-ed onto + the first. If a key occurs in more than one map, the mapping(s) + from the latter (left-to-right) will be combined with the mapping in + the result by calling (f val-in-result val-in-latter)." + [f & maps] + (reduce + (fn [acc m] + (if (nil? m) + acc + (reduce + (fn [macc entry] + (let [k (first entry) + v (second entry)] + (if (contains? macc k) + (assoc macc k (f (get macc k) v)) + (assoc macc k v)))) + (or acc {}) + m))) + nil + maps)) + +(defn update-keys + "m f => apply f to each key in m" + [m f] + (reduce + (fn [acc entry] + (assoc acc (f (first entry)) (second entry))) + {} + m)) + +(defn update-vals + "m f => apply f to each val in m" + [m f] + (reduce + (fn [acc entry] + (assoc acc (first entry) (f (second entry)))) + {} + m)) + +(defn not-empty + "If coll is empty, returns nil, else coll" + [coll] + (when (seq coll) coll)) + +(defn memoize + "Returns a memoized version of a referentially transparent function. The + memoized version of the function keeps a cache of the mapping from arguments + to results and, when calls with the same arguments are repeated often, has + higher performance at the expense of higher memory use." + [f] + (let [mem (atom {})] + (fn [& args] + (let [cached (get @mem args ::not-found)] + (if (= cached ::not-found) + (let [ret (apply f args)] + (swap! mem assoc args ret) + ret) + cached))))) + +(defn trampoline + "trampoline can be used to convert algorithms requiring mutual + recursion without stack consumption. Calls f with supplied args, if + any. If f returns a fn, calls that fn with no arguments, and + continues to repeat, until the return value is not a fn, then + returns that non-fn value." + ([f] + (loop [ret (f)] + (if (fn? ret) + (recur (ret)) + ret))) + ([f & args] + (loop [ret (apply f args)] + (if (fn? ret) + (recur (ret)) + ret)))) + +(defmacro with-redefs + "binding => var-symbol temp-value-expr + Temporarily redefines Vars while executing the body. The + temp-value-exprs will be evaluated and each resulting value will + replace in parallel the root value of its Var. Always restores + the original values, even if body throws." + [bindings & body] + (let [pairs (partition 2 bindings) + names (mapv first pairs) + new-vals (mapv second pairs) + orig-syms (mapv (fn [_] (gensym "orig")) names)] + \`(let [~@(interleave orig-syms (map (fn [n] \`(var-get (var ~n))) names))] + (try + (do ~@(map (fn [n v] \`(alter-var-root (var ~n) (constantly ~v))) names new-vals) + ~@body) + (finally + ~@(map (fn [n o] \`(alter-var-root (var ~n) (constantly ~o))) names orig-syms)))))) + +;; ── Macros: conditionals and control flow ─────────────────────────────────── + +(defmacro if-some + "bindings => binding-form test + If test is not nil, evaluates then with binding-form bound to the + value of test, if not, yields else" + ([bindings then] \`(if-some ~bindings ~then nil)) ([bindings then else] (let [form (first bindings) tst (second bindings)] - \`(let [~form ~tst] - (if ~form ~then ~else))))) - -(defmacro when-let [bindings & body] + \`(let [temp# ~tst] + (if (nil? temp#) + ~else + (let [~form temp#] + ~then)))))) + +(defmacro when-some + "bindings => binding-form test + When test is not nil, evaluates body with binding-form bound to the + value of test" + [bindings & body] (let [form (first bindings) tst (second bindings)] - \`(let [~form ~tst] - (when ~form ~@body)))) - -(defmacro and [& forms] - (if (nil? forms) - true - (if (nil? (seq (rest forms))) - (first forms) - \`(let [v# ~(first forms)] - (if v# (and ~@(rest forms)) v#))))) - -(defmacro or [& forms] - (if (nil? forms) - nil - (if (nil? (seq (rest forms))) - (first forms) - \`(let [v# ~(first forms)] - (if v# v# (or ~@(rest forms))))))) - -(defmacro cond [& clauses] + \`(let [temp# ~tst] + (when (some? temp#) + (let [~form temp#] + ~@body))))) + +(defmacro when-first + "bindings => x xs + Roughly the same as (when (seq xs) (let [x (first xs)] body)) but xs is evaluated only once" + [bindings & body] + (let [x (first bindings) + xs (second bindings)] + \`(let [temp# (seq ~xs)] + (when temp# + (let [~x (first temp#)] + ~@body))))) + +(defn condp-emit [gpred gexpr clauses] (if (nil? clauses) - nil - \`(if ~(first clauses) - ~(first (next clauses)) - (cond ~@(rest (rest clauses)))))) - -(defmacro -> [x & forms] - (if (nil? forms) - x - (let [form (first forms) - more (rest forms) - threaded (if (list? form) - \`(~(first form) ~x ~@(rest form)) - \`(~form ~x))] - \`(-> ~threaded ~@more)))) - -(defmacro ->> [x & forms] - (if (nil? forms) - x - (let [form (first forms) - more (rest forms) - threaded (if (list? form) - \`(~(first form) ~@(rest form) ~x) - \`(~form ~x))] - \`(->> ~threaded ~@more)))) - -(defmacro comment - ; Ignores body, yields nil - [& body]) + \`(throw (ex-info (str "No matching clause: " ~gexpr) {})) + (if (nil? (next clauses)) + (first clauses) + \`(if (~gpred ~(first clauses) ~gexpr) + ~(second clauses) + ~(condp-emit gpred gexpr (next (next clauses))))))) + +(defmacro condp + "Takes a binary predicate, an expression, and a set of clauses. + Each clause can take the form of either: + test-expr result-expr + The predicate is applied to each test-expr and the expression in turn." + [pred expr & clauses] + (let [gpred (gensym "pred__") + gexpr (gensym "expr__")] + \`(let [~gpred ~pred + ~gexpr ~expr] + ~(condp-emit gpred gexpr clauses)))) + +(defn case-emit [ge clauses] + (if (nil? clauses) + \`(throw (ex-info (str "No matching clause: " ~ge) {})) + (if (nil? (next clauses)) + (first clauses) + \`(if (= ~ge ~(first clauses)) + ~(second clauses) + ~(case-emit ge (next (next clauses))))))) + +(defmacro case + "Takes an expression, and a set of clauses. Each clause can take the form of + either: + test-constant result-expr + If no clause matches, and there is an odd number of forms (a default), the + last expression is returned." + [e & clauses] + (let [ge (gensym "case__")] + \`(let [~ge ~e] + ~(case-emit ge clauses)))) + +(defmacro dotimes + "bindings => name n + Repeatedly executes body (presumably for side-effects) with name + bound to integers from 0 through n-1." + [bindings & body] + (let [i (first bindings) + n (second bindings)] + \`(let [n# ~n] + (loop [~i 0] + (when (< ~i n#) + ~@body + (recur (inc ~i))))))) + +(defmacro while + "Repeatedly executes body while test expression is true. Presumes + some side-effect will cause test to become false/nil." + [test & body] + \`(loop [] + (when ~test + ~@body + (recur)))) + +(defmacro doseq + "Repeatedly executes body (presumably for side-effects) with + bindings. Supports :let, :when, and :while modifiers." + [seq-exprs & body] + (let [bindings (partition 2 seq-exprs) + first-binding (first bindings) + rest-bindings (next bindings)] + (if (nil? first-binding) + \`(do ~@body nil) + (let [k (first first-binding) + v (second first-binding)] + (cond + (= k :let) + \`(let ~v (doseq ~(apply concat rest-bindings) ~@body)) + + (= k :when) + \`(when ~v (doseq ~(apply concat rest-bindings) ~@body)) + + (= k :while) + \`(if ~v (doseq ~(apply concat rest-bindings) ~@body) nil) + + :else + (if rest-bindings + \`(run! (fn [~k] (doseq ~(apply concat rest-bindings) ~@body)) ~v) + \`(run! (fn [~k] ~@body) ~v))))))) + +(defmacro for + "List comprehension. Takes a vector of one or more + binding-form/collection-expr pairs, each followed by zero or more + modifiers, and yields a sequence of evaluations of expr. + Supported modifiers: :let, :when, :while." + [seq-exprs & body] + (let [bindings (partition 2 seq-exprs) + first-binding (first bindings) + rest-bindings (next bindings)] + (if (nil? first-binding) + \`(list ~@body) + (let [k (first first-binding) + v (second first-binding)] + (cond + (= k :let) + \`(let ~v (for ~(apply concat rest-bindings) ~@body)) + + (= k :when) + \`(if ~v (for ~(apply concat rest-bindings) ~@body) (list)) + + (= k :while) + \`(if ~v (for ~(apply concat rest-bindings) ~@body) (list)) + + :else + (if rest-bindings + \`(mapcat (fn [~k] (for ~(apply concat rest-bindings) ~@body)) ~v) + \`(map (fn [~k] ~@body) ~v))))))) + +(defmacro with-out-str + "Evaluates body in a context in which *out* is bound to a fresh string + accumulator. Returns the string of all output produced by println, print, + pr, prn, pprint and newline during the evaluation." + [& body] + \`(let [buf# (atom "")] + (binding [*out* (fn [s#] (swap! buf# str s#))] + ~@body) + @buf#)) + +(defmacro with-err-str + "Like with-out-str but captures *err* output (warn, etc.)." + [& body] + \`(let [buf# (atom "")] + (binding [*err* (fn [s#] (swap! buf# str s#))] + ~@body) + @buf#)) +`; -(defn constantly - "Returns a function that takes any number of arguments and returns x." - [x] (fn [& _] x)) +// src/clojure/generated/clojure-set-source.ts +var clojure_setSource = `(ns clojure.set) + +(defn union + "Return a set that is the union of the input sets." + ([] #{}) + ([s] s) + ([s1 s2] + (reduce conj s1 s2)) + ([s1 s2 & sets] + (reduce union (union s1 s2) sets))) + +(defn intersection + "Return a set that is the intersection of the input sets." + ([s] s) + ([s1 s2] + (reduce (fn [acc x] + (if (contains? s2 x) + (conj acc x) + acc)) + #{} + s1)) + ([s1 s2 & sets] + (reduce intersection (intersection s1 s2) sets))) + +(defn difference + "Return a set that is the first set without elements of the remaining sets." + ([s] s) + ([s1 s2] + (reduce (fn [acc x] + (if (contains? s2 x) + acc + (conj acc x))) + #{} + s1)) + ([s1 s2 & sets] + (reduce difference (difference s1 s2) sets))) + +(defn select + "Returns a set of the elements for which pred is true." + [pred s] + (reduce (fn [acc x] + (if (pred x) + (conj acc x) + acc)) + #{} + s)) + +(defn project + "Returns a rel of the elements of xrel with only the keys in ks." + [xrel ks] + (reduce (fn [acc m] + (conj acc (select-keys m ks))) + #{} + xrel)) + +(defn rename-keys + "Returns the map with the keys in kmap renamed to the vals in kmap." + [m kmap] + (reduce (fn [acc [old-k new-k]] + (if (contains? acc old-k) + (-> acc + (assoc new-k (get acc old-k)) + (dissoc old-k)) + acc)) + m + kmap)) + +(defn rename + "Returns a rel of the maps in xrel with the keys in kmap renamed to the vals in kmap." + [xrel kmap] + (reduce (fn [acc m] + (conj acc (rename-keys m kmap))) + #{} + xrel)) + +(defn index + "Returns a map of the distinct values of ks in the xrel mapped to a + set of the maps in xrel with the corresponding values of ks." + [xrel ks] + (reduce (fn [acc m] + (let [k (select-keys m ks)] + (assoc acc k (conj (get acc k #{}) m)))) + {} + xrel)) + +(defn map-invert + "Returns the map with the vals mapped to the keys." + [m] + (reduce (fn [acc [k v]] + (assoc acc v k)) + {} + m)) -(defn some? - "Returns true if x is not nil, false otherwise" - [x] (not (nil? x))) +(defn join + "When passed 2 rels, returns the relation corresponding to the natural + join. When passed an additional keymap, joins on the corresponding keys." + ([xrel yrel] + (if (and (seq xrel) (seq yrel)) + (let [ks (intersection (set (keys (first xrel))) + (set (keys (first yrel))))] + (if (empty? ks) + (reduce (fn [acc mx] + (reduce (fn [acc2 my] + (conj acc2 (merge mx my))) + acc + yrel)) + #{} + xrel) + (join xrel yrel (zipmap ks ks)))) + #{})) + ([xrel yrel km] + (let [idx (index yrel (vals km))] + (reduce (fn [acc mx] + (let [found (get idx (rename-keys (select-keys mx (keys km)) km))] + (if found + (reduce (fn [acc2 my] + (conj acc2 (merge my mx))) + acc + found) + acc))) + #{} + xrel)))) + +(defn subset? + "Is set1 a subset of set2?" + [s1 s2] + (every? #(contains? s2 %) s1)) + +(defn superset? + "Is set1 a superset of set2?" + [s1 s2] + (every? #(contains? s1 %) s2)) +`; -(defn any? - "Returns true for any given argument" - [_x] true) +// src/clojure/generated/clojure-string-source.ts +var clojure_stringSource = `(ns clojure.string) -(defn complement - "Takes a fn f and returns a fn that takes the same arguments as f, - has the same effects, if any, and returns the opposite truth value." - [f] - (fn - ([] (not (f))) - ([x] (not (f x))) - ([x y] (not (f x y))) - ([x y & zs] (not (apply f x y zs))))) +;; Runtime-injected native helpers. Declared here so clojure-lsp can resolve +;; them; the interpreter treats bare (def name) as a no-op and leaves the +;; native binding from coreEnv intact. +(def str-split*) +(def str-upper-case*) +(def str-lower-case*) +(def str-trim*) +(def str-triml*) +(def str-trimr*) +(def str-reverse*) +(def str-starts-with*) +(def str-ends-with*) +(def str-includes*) +(def str-index-of*) +(def str-last-index-of*) +(def str-replace*) +(def str-replace-first*) -(defn juxt - "Takes a set of functions and returns a fn that is the juxtaposition - of those fns. The returned fn takes a variable number of args and - returns a vector containing the result of applying each fn to the args." - [& fns] - (fn [& args] - (reduce (fn [acc f] (conj acc (apply f args))) [] fns))) +;; --------------------------------------------------------------------------- +;; Joining / splitting +;; --------------------------------------------------------------------------- -(defn merge - "Returns a map that consists of the rest of the maps conj-ed onto - the first. If a key occurs in more than one map, the mapping from - the latter (left-to-right) will be the mapping in the result." - [& maps] - (if (nil? maps) - nil - (reduce - (fn [acc m] - (if (nil? m) - acc - (if (nil? acc) - m - (reduce - (fn [macc entry] - (assoc macc (first entry) (second entry))) - acc - m)))) - nil - maps))) +(defn join + "Returns a string of all elements in coll, as returned by (str), separated + by an optional separator." + ([coll] (join "" coll)) + ([separator coll] + (if (nil? coll) + "" + (reduce + (fn [acc x] + (if (= acc "") + (str x) + (str acc separator x))) + "" + coll)))) -(defn select-keys - "Returns a map containing only those entries in map whose key is in keys." - [m keys] - (if (or (nil? m) (nil? keys)) - {} - (let [missing (gensym)] - (reduce - (fn [acc k] - (let [v (get m k missing)] - (if (= v missing) - acc - (assoc acc k v)))) - {} - keys)))) +(defn split + "Splits string on a regular expression. Optional limit is the maximum number + of parts returned. Trailing empty strings are not returned by default; pass + a limit of -1 to return all." + ([s sep] (str-split* s sep)) + ([s sep limit] (str-split* s sep limit))) -(defn update - "Updates a value in an associative structure where k is a key and f is a - function that will take the old value and any supplied args and return the - new value, and returns a new structure." - [m k f & args] - (let [target (if (nil? m) {} m)] - (assoc target k (if (nil? args) - (f (get target k)) - (apply f (get target k) args))))) +(defn split-lines + "Splits s on \\\\n or \\\\r\\\\n. Trailing empty lines are not returned." + [s] + (split s #"\\r?\\n")) -(defn get-in - "Returns the value in a nested associative structure, where ks is a - sequence of keys. Returns nil if the key is not present, or the not-found - value if supplied." - ([m ks] - (reduce get m ks)) - ([m ks not-found] - (loop [m m, ks (seq ks)] - (if (nil? ks) - m - (if (contains? m (first ks)) - (recur (get m (first ks)) (next ks)) - not-found))))) +;; --------------------------------------------------------------------------- +;; Case conversion +;; --------------------------------------------------------------------------- -(defn assoc-in - "Associates a value in a nested associative structure, where ks is a - sequence of keys and v is the new value. Returns a new nested structure." - [m [k & ks] v] - (if ks - (assoc m k (assoc-in (get m k) ks v)) - (assoc m k v))) +(defn upper-case + "Converts string to all upper-case." + [s] + (str-upper-case* s)) -(defn update-in - "Updates a value in a nested associative structure, where ks is a - sequence of keys and f is a function that will take the old value and any - supplied args and return the new value. Returns a new nested structure." - [m ks f & args] - (assoc-in m ks (apply f (get-in m ks) args))) +(defn lower-case + "Converts string to all lower-case." + [s] + (str-lower-case* s)) -(defn fnil - "Takes a function f, and returns a function that calls f, replacing - a nil first argument with x, optionally nil second with y, nil third with z." - ([f x] - (fn [a & more] - (apply f (if (nil? a) x a) more))) - ([f x y] - (fn [a b & more] - (apply f (if (nil? a) x a) (if (nil? b) y b) more))) - ([f x y z] - (fn [a b c & more] - (apply f (if (nil? a) x a) (if (nil? b) y b) (if (nil? c) z c) more)))) +(defn capitalize + "Converts first character of the string to upper-case, all other + characters to lower-case." + [s] + (if (< (count s) 2) + (upper-case s) + (str (upper-case (subs s 0 1)) (lower-case (subs s 1))))) -(defn frequencies - "Returns a map from distinct items in coll to the number of times they appear." - [coll] - (if (nil? coll) - {} - (reduce - (fn [counts item] - (assoc counts item (inc (get counts item 0)))) - {} - coll))) +;; --------------------------------------------------------------------------- +;; Trimming +;; --------------------------------------------------------------------------- -(defn group-by - "Returns a map of the elements of coll keyed by the result of f on each - element. The value at each key is a vector of matching elements." - [f coll] - (if (nil? coll) - {} - (reduce - (fn [acc item] - (let [k (f item)] - (assoc acc k (conj (get acc k []) item)))) - {} - coll))) +(defn trim + "Removes whitespace from both ends of string." + [s] + (str-trim* s)) -(defn distinct - "Returns a vector of the elements of coll with duplicates removed, - preserving first-seen order." - [coll] - (if (nil? coll) - [] - (get - (reduce - (fn [state item] - (let [seen (get state 0) - out (get state 1)] - (if (get seen item false) - state - [(assoc seen item true) (conj out item)]))) - [{} []] - coll) - 1))) +(defn triml + "Removes whitespace from the left side of string." + [s] + (str-triml* s)) -(defn flatten-step - "Internal helper for flatten." - [v] - (if (or (list? v) (vector? v)) - (reduce - (fn [acc item] - (into acc (flatten-step item))) - [] - v) - [v])) +(defn trimr + "Removes whitespace from the right side of string." + [s] + (str-trimr* s)) -(defn flatten - "Takes any nested combination of sequential things (lists/vectors) and - returns their contents as a single flat vector." - [x] - (if (nil? x) - [] - (flatten-step x))) +(defn trim-newline + "Removes all trailing newline \\\\n or return \\\\r characters from string. + Similar to Perl's chomp." + [s] + (replace s #"[\\r\\n]+$" "")) -(defn reduce-kv - "Reduces an associative structure. f should be a function of 3 - arguments: accumulator, key/index, value." - [f init coll] - (cond - (map? coll) - (reduce - (fn [acc entry] - (f acc (first entry) (second entry))) - init - coll) +;; --------------------------------------------------------------------------- +;; Predicates +;; --------------------------------------------------------------------------- - (vector? coll) - (loop [idx 0 - acc init] - (if (< idx (count coll)) - (recur (inc idx) (f acc idx (nth coll idx))) - acc)) +(defn blank? + "True if s is nil, empty, or contains only whitespace." + [s] + (or (nil? s) (not (nil? (re-matches #"\\s*" s))))) - :else - (throw - (ex-info - "reduce-kv expects a map or vector" - {:coll coll})))) +(defn starts-with? + "True if s starts with substr." + [s substr] + (str-starts-with* s substr)) -(defn sort-compare - "Internal helper: normalizes comparator results." - [cmp a b] - (let [r (cmp a b)] - (if (number? r) - (< r 0) - r))) +(defn ends-with? + "True if s ends with substr." + [s substr] + (str-ends-with* s substr)) -(defn insert-sorted - "Internal helper for insertion-sort based sort implementation." - [cmp x sorted] - (loop [left [] - right sorted] - (if (nil? (seq right)) - (conj left x) - (let [y (first right)] - (if (sort-compare cmp x y) - (into (conj left x) right) - (recur (conj left y) (rest right))))))) +(defn includes? + "True if s includes substr." + [s substr] + (str-includes* s substr)) -(defn sort - "Returns the items in coll in sorted order. With no comparator, sorts - ascending using <. Comparator may return boolean or number." - ([coll] (sort < coll)) - ([cmp coll] - (if (nil? coll) - [] - (reduce - (fn [acc item] - (insert-sorted cmp item acc)) - [] - coll)))) +;; --------------------------------------------------------------------------- +;; Search +;; --------------------------------------------------------------------------- -(defn sort-by - "Returns a sorted sequence of items in coll, where the sort order is - determined by comparing (keyfn item)." - ([keyfn coll] (sort-by keyfn < coll)) - ([keyfn cmp coll] - (sort - (fn [a b] - (cmp (keyfn a) (keyfn b))) - coll))) +(defn index-of + "Return index of value (string) in s, optionally searching forward from + from-index. Return nil if value not found." + ([s value] (str-index-of* s value)) + ([s value from-index] (str-index-of* s value from-index))) -(def not-any? (comp not some)) +(defn last-index-of + "Return last index of value (string) in s, optionally searching backward + from from-index. Return nil if value not found." + ([s value] (str-last-index-of* s value)) + ([s value from-index] (str-last-index-of* s value from-index))) -(defn not-every? - "Returns false if (pred x) is logical true for every x in - coll, else true." - [pred coll] (not (every? pred coll))) +;; --------------------------------------------------------------------------- +;; Replacement +;; --------------------------------------------------------------------------- -;; ── Transducer protocol ────────────────────────────────────────────────────── +(defn replace + "Replaces all instances of match with replacement in s. -;; into: 2-arity uses reduce+conj; 3-arity uses transduce -(defn into - "Returns a new coll consisting of to-coll with all of the items of - from-coll conjoined. A transducer may be supplied." - ([to from] (reduce conj to from)) - ([to xf from] (transduce xf conj to from))) + match/replacement can be: + string / string — literal match, literal replacement + pattern / string — regex match; $1, $2, etc. substituted from groups + pattern / fn — regex match; fn called with match (string or vector + of [whole g1 g2 ...]), return value used as replacement. -;; sequence: materialise a transducer over a collection into a seq (list) -(defn sequence - "Coerces coll to a (possibly empty) sequence, if it is not already - one. Will not force a seq. (sequence nil) yields (), When a - transducer is supplied, returns a lazy sequence of applications of - the transform to the items in coll" - ([coll] (apply list (into [] coll))) - ([xf coll] (apply list (into [] xf coll)))) + See also replace-first." + [s match replacement] + (str-replace* s match replacement)) -(defn completing - "Takes a reducing function f of 2 args and returns a fn suitable for - transduce by adding an arity-1 signature that calls cf (default - - identity) on the result argument." - ([f] (completing f identity)) - ([f cf] - (fn - ([] (f)) - ([x] (cf x)) - ([x y] (f x y))))) +(defn replace-first + "Replaces the first instance of match with replacement in s. + Same match/replacement semantics as replace." + [s match replacement] + (str-replace-first* s match replacement)) -;; map: 1-arg returns transducer; 2-arg is eager; 3+-arg zips collections -(defn map - "Returns a sequence consisting of the result of applying f to the set - of first items of each coll, followed by applying f to the set of - second items in each coll, until any one of the colls is exhausted. - Any remaining items in other colls are ignored. Returns a transducer - when no collection is provided." - ([f] - (fn [rf] - (fn - ([] (rf)) - ([result] (rf result)) - ([result input] (rf result (f input)))))) - ([f coll] - (sequence (map f) coll)) - ([f c1 c2] - (loop [s1 (seq c1) - s2 (seq c2) - acc []] - (if (or (nil? s1) (nil? s2)) - acc - (recur - (next s1) - (next s2) - (conj acc (f (first s1) (first s2))))))) - ([f c1 c2 & colls] - (loop [seqs (map seq (cons c1 (cons c2 colls))) - acc []] - (if (some nil? seqs) - acc - (recur (map next seqs) (conj acc (apply f (map first seqs)))))))) +(defn re-quote-replacement + "Given a replacement string that you wish to be a literal replacement for a + pattern match in replace or replace-first, escape any special replacement + characters ($ signs) so they are treated literally." + [s] + (replace s #"\\$" "$$$$")) -;; filter: 1-arg returns transducer; 2-arg is eager -(defn filter - "Returns a sequence of the items in coll for which - (pred item) returns logical true. pred must be free of side-effects. - Returns a transducer when no collection is provided." - ([pred] - (fn [rf] - (fn - ([] (rf)) - ([result] (rf result)) - ([result input] - (if (pred input) - (rf result input) - result))))) - ([pred coll] - (sequence (filter pred) coll))) +;; --------------------------------------------------------------------------- +;; Miscellaneous +;; --------------------------------------------------------------------------- -(defn remove - "Returns a lazy sequence of the items in coll for which - (pred item) returns logical false. pred must be free of side-effects. - Returns a transducer when no collection is provided." - ([pred] (filter (complement pred))) - ([pred coll] - (filter (complement pred) coll))) +(defn reverse + "Returns s with its characters reversed." + [s] + (str-reverse* s)) + +(defn escape + "Return a new string, using cmap to escape each character ch from s as + follows: if (cmap ch) is nil, append ch to the new string; otherwise append + (str (cmap ch)). + cmap may be a map or a function. Maps are callable directly (IFn semantics). + Note: Clojure uses char literal keys (e.g. {\\\\< \\"<\\"}). This interpreter + has no char type, so map keys must be single-character strings instead + (e.g. {\\"<\\" \\"<\\"})." + [s cmap] + (apply str (map (fn [c] + (let [r (cmap c)] + (if (nil? r) c (str r)))) + (split s #"")))) +`; -;; take: stateful transducer; signals early termination after n items -;; r > 0 → keep going; r = 0 → take last item and stop; r < 0 → already past limit, stop -(defn take - "Returns a sequence of the first n items in coll, or all items if - there are fewer than n. Returns a stateful transducer when - no collection is provided." - ([n] - (fn [rf] - (let [remaining (volatile! n)] - (fn - ([] (rf)) - ([result] (rf result)) - ([result input] - (let [n @remaining - nrem (vswap! remaining dec) - result (if (pos? n) - (rf result input) - result)] - (if (not (pos? nrem)) - (ensure-reduced result) - result))))))) - ([n coll] - (sequence (take n) coll))) +// src/clojure/generated/clojure-walk-source.ts +var clojure_walkSource = `(ns clojure.walk) -;; take-while: stateless transducer; emits reduced when pred fails -(defn take-while - "Returns a sequence of successive items from coll while - (pred item) returns logical true. pred must be free of side-effects. - Returns a transducer when no collection is provided." - ([pred] - (fn [rf] - (fn - ([] (rf)) - ([result] (rf result)) - ([result input] - (if (pred input) - (rf result input) - (reduced result)))))) - ([pred coll] - (sequence (take-while pred) coll))) +(defn walk + "Traverses form, an arbitrary data structure. inner and outer are + functions. Applies inner to each element of form, building up a + data structure of the same type, then applies outer to the result." + [inner outer form] + (cond + (list? form) (outer (apply list (map inner form))) + (vector? form) (outer (into [] (map inner) form)) + (map? form) (outer (into {} (map (fn [e] [(inner (first e)) (inner (second e))]) form))) + (set? form) (outer (into #{} (map inner) form)) + :else (outer form))) + +(defn postwalk + "Performs a depth-first, post-order traversal of form. Calls f on + each sub-form, uses f's return value in place of the original." + [f form] + (walk (fn [x] (postwalk f x)) f form)) + +(defn prewalk + "Like postwalk, but does pre-order traversal." + [f form] + (walk (fn [x] (prewalk f x)) identity (f form))) + +(defn postwalk-replace + "Recursively transforms form by replacing keys in smap with their + values. Like clojure/replace but works on any data structure." + [smap form] + (postwalk (fn [x] (if (contains? smap x) (get smap x) x)) form)) + +(defn prewalk-replace + "Recursively transforms form by replacing keys in smap with their + values. Like clojure/replace but works on any data structure." + [smap form] + (prewalk (fn [x] (if (contains? smap x) (get smap x) x)) form)) + +(defn keywordize-keys + "Recursively transforms all map keys from strings to keywords." + [m] + (postwalk + (fn [x] + (if (map? x) + (into {} (map (fn [e] + (let [k (first e)] + (if (string? k) + [(keyword k) (second e)] + e))) + x)) + x)) + m)) + +(defn stringify-keys + "Recursively transforms all map keys from keywords to strings." + [m] + (postwalk + (fn [x] + (if (map? x) + (into {} + (map + (fn [e] + (let [k (first e)] + (if (keyword? k) + [(name k) (second e)] + e))) + x)) + x)) + m)) +`; -;; drop: stateful transducer; skips first n items -;; r >= 0 → still skipping; r < 0 → past the drop zone, start taking -(defn drop - "Returns a sequence of all but the first n items in coll. - Returns a stateful transducer when no collection is provided." - ([n] - (fn [rf] - (let [remaining (volatile! n)] - (fn - ([] (rf)) - ([result] (rf result)) - ([result input] - (let [rem @remaining] - (vswap! remaining dec) - (if (pos? rem) - result - (rf result input)))))))) - ([n coll] - (sequence (drop n) coll))) +// src/clojure/generated/builtin-namespace-registry.ts +var builtInNamespaceSources = { + "clojure.core": () => clojure_coreSource, + "clojure.set": () => clojure_setSource, + "clojure.string": () => clojure_stringSource, + "clojure.walk": () => clojure_walkSource +}; -(defn drop-last - "Return a sequence of all but the last n (default 1) items in coll" - ([coll] (drop-last 1 coll)) - ([n coll] (map (fn [x _] x) coll (drop n coll)))) +// src/core/errors.ts +class TokenizerError extends Error { + context; + constructor(message, context) { + super(message); + this.name = "TokenizerError"; + this.context = context; + } +} -(defn take-last - "Returns a sequence of the last n items in coll. Depending on the type - of coll may be no better than linear time. For vectors, see also subvec." - [n coll] - (loop [s (seq coll), lead (seq (drop n coll))] - (if lead - (recur (next s) (next lead)) - s))) +class ReaderError extends Error { + context; + pos; + constructor(message, context, pos) { + super(message); + this.name = "ReaderError"; + this.context = context; + this.pos = pos; + } +} -;; drop-while: stateful transducer; passes through once pred fails -(defn drop-while - "Returns a sequence of the items in coll starting from the - first item for which (pred item) returns logical false. Returns a - stateful transducer when no collection is provided." - ([pred] - (fn [rf] - (let [dropping (volatile! true)] - (fn - ([] (rf)) - ([result] (rf result)) - ([result input] - (if (and @dropping (pred input)) - result - (do - (vreset! dropping false) - (rf result input)))))))) - ([pred coll] - (sequence (drop-while pred) coll))) +class EvaluationError extends Error { + context; + pos; + data; + constructor(message, context, pos) { + super(message); + this.name = "EvaluationError"; + this.context = context; + this.pos = pos; + } + static atArg(message, context, argIndex) { + const err = new EvaluationError(message, context); + err.data = { argIndex }; + return err; + } +} -;; map-indexed: stateful transducer; passes index and item to f -(defn map-indexed - "Returns a sequence consisting of the result of applying f to 0 - and the first item of coll, followed by applying f to 1 and the second - item in coll, etc, until coll is exhausted. Thus function f should - accept 2 arguments, index and item. Returns a stateful transducer when - no collection is provided." - ([f] - (fn [rf] - (let [i (volatile! -1)] - (fn - ([] (rf)) - ([result] (rf result)) - ([result input] - (rf result (f (vswap! i inc) input))))))) - ([f coll] - (sequence (map-indexed f) coll))) +class CljThrownSignal { + value; + constructor(value) { + this.value = value; + } +} -;; dedupe: stateful transducer; removes consecutive duplicates -(defn dedupe - "Returns a sequence removing consecutive duplicates in coll. - Returns a transducer when no collection is provided." - ([] - (fn [rf] - (let [pv (volatile! ::none)] - (fn - ([] (rf)) - ([result] (rf result)) - ([result input] - (let [prior @pv] - (vreset! pv input) - (if (= prior input) - result - (rf result input)))))))) - ([coll] - (sequence (dedupe) coll))) +// src/core/factories.ts +var cljNumber = (value) => ({ kind: "number", value }); +var cljString = (value) => ({ kind: "string", value }); +var cljBoolean = (value) => ({ kind: "boolean", value }); +var cljKeyword = (name) => ({ kind: "keyword", name }); +var cljNil = () => ({ kind: "nil", value: null }); +var cljSymbol = (name) => ({ kind: "symbol", name }); +var cljList = (value) => ({ kind: "list", value }); +var cljSet = (values) => ({ kind: "set", values }); +var cljVector = (value) => ({ kind: "vector", value }); +var cljMap = (entries) => ({ kind: "map", entries }); +var cljFunction = (params, restParam, body, env) => ({ + kind: "function", + arities: [{ params, restParam, body }], + env +}); +var cljMultiArityFunction = (arities, env) => ({ + kind: "function", + arities, + env +}); +var cljMacro = (params, restParam, body, env) => ({ + kind: "macro", + arities: [{ params, restParam, body }], + env +}); +var cljMultiArityMacro = (arities, env) => ({ + kind: "macro", + arities, + env +}); +var cljRegex = (pattern, flags = "") => ({ + kind: "regex", + pattern, + flags +}); +var cljVar = (ns, name, value, meta) => ({ kind: "var", ns, name, value, meta }); +var cljAtom = (value) => ({ kind: "atom", value }); +var cljReduced = (value) => ({ + kind: "reduced", + value +}); +var cljVolatile = (value) => ({ + kind: "volatile", + value +}); +var cljDelay = (thunk) => ({ + kind: "delay", + thunk, + realized: false +}); +var cljLazySeq = (thunk) => ({ + kind: "lazy-seq", + thunk, + realized: false +}); +var cljCons = (head, tail) => ({ + kind: "cons", + head, + tail +}); +var cljNamespace = (name) => ({ + kind: "namespace", + name, + vars: new Map, + aliases: new Map, + readerAliases: new Map +}); +var cljJsValue = (value) => ({ + kind: "js-value", + value +}); +var cljPending = (promise) => { + const pending = { kind: "pending", promise }; + promise.then((v) => { + pending.resolved = true; + pending.resolvedValue = v; + }, () => {}); + return pending; +}; +function buildDocMeta(text, arglists) { + return cljMap([ + [cljKeyword(":doc"), cljString(text)], + ...arglists ? [ + [ + cljKeyword(":arglists"), + cljVector(arglists.map((args) => cljVector(args.map(cljSymbol)))) + ] + ] : [] + ]); +} +function makeNativeFnBuilder(def) { + const plain = { + kind: "native-function", + name: def.name, + fn: def.fn, + ...def.fnWithContext !== undefined ? { fnWithContext: def.fnWithContext } : {}, + ...def.meta !== undefined ? { meta: def.meta } : {} + }; + return { + ...plain, + doc(text, arglists) { + return makeNativeFnBuilder({ + ...plain, + meta: buildDocMeta(text, arglists) + }); + } + }; +} +var cljMultiMethod = (name, dispatchFn, methods, defaultMethod) => ({ + kind: "multi-method", + name, + dispatchFn, + methods, + defaultMethod +}); +var v = { + number: cljNumber, + string: cljString, + boolean: cljBoolean, + keyword: cljKeyword, + nil: cljNil, + symbol: cljSymbol, + kw: cljKeyword, + list: cljList, + vector: cljVector, + map: cljMap, + set: cljSet, + cons: cljCons, + function: cljFunction, + multiArityFunction: cljMultiArityFunction, + macro: cljMacro, + multiArityMacro: cljMultiArityMacro, + multiMethod: cljMultiMethod, + nativeFn(name, fn) { + return makeNativeFnBuilder({ kind: "native-function", name, fn }); + }, + nativeFnCtx(name, fn) { + return makeNativeFnBuilder({ + kind: "native-function", + name, + fn: () => { + throw new EvaluationError("Native function called without context", { + name + }); + }, + fnWithContext: fn + }); + }, + var: cljVar, + atom: cljAtom, + regex: cljRegex, + reduced: cljReduced, + volatile: cljVolatile, + delay: cljDelay, + lazySeq: cljLazySeq, + namespace: cljNamespace, + pending: cljPending, + jsValue: cljJsValue +}; -;; partition-all: stateful transducer; groups items into vectors of size n -(defn partition-all - "Returns a sequence of lists like partition, but may include - partitions with fewer than n items at the end. Returns a stateful - transducer when no collection is provided." - ([n] - (fn [rf] - (let [buf (volatile! [])] - (fn - ([] (rf)) - ([result] - (let [b @buf] - (vreset! buf []) - (if (empty? b) - (rf result) - (rf (unreduced (rf result b)))))) - ([result input] - (let [nb (conj @buf input)] - (if (= (count nb) n) - (do - (vreset! buf []) - (rf result nb)) - (do - (vreset! buf nb) - result)))))))) - ([n coll] - (sequence (partition-all n) coll))) +// src/core/env.ts +class EnvError extends Error { + context; + constructor(message, context) { + super(message); + this.context = context; + this.name = "EnvError"; + } +} +function derefValue(val) { + if (val.kind !== "var") + return val; + if (val.dynamic && val.bindingStack && val.bindingStack.length > 0) { + return val.bindingStack[val.bindingStack.length - 1]; + } + return val.value; +} +function makeNamespace(name) { + return { + kind: "namespace", + name, + vars: new Map, + aliases: new Map, + readerAliases: new Map + }; +} +function makeEnv(outer) { + return { + bindings: new Map, + outer: outer ?? null + }; +} +function lookup(name, env) { + let current = env; + while (current) { + const raw = current.bindings.get(name); + if (raw !== undefined) + return raw; + const v2 = current.ns?.vars.get(name); + if (v2 !== undefined) + return derefValue(v2); + current = current.outer; + } + throw new EvaluationError(`Symbol ${name} not found`, { name }); +} +function tryLookup(name, env) { + let current = env; + while (current) { + const raw = current.bindings.get(name); + if (raw !== undefined) + return raw; + const v2 = current.ns?.vars.get(name); + if (v2 !== undefined) + return derefValue(v2); + current = current.outer; + } + return; +} +function internVar(name, value, nsEnv, meta) { + const ns = nsEnv.ns; + const existing = ns.vars.get(name); + if (existing) { + existing.value = value; + if (meta) + existing.meta = meta; + } else { + ns.vars.set(name, v.var(ns.name, name, value, meta)); + } +} +function lookupVar(name, env) { + let current = env; + while (current) { + const raw = current.bindings.get(name); + if (raw !== undefined && raw.kind === "var") + return raw; + const v2 = current.ns?.vars.get(name); + if (v2 !== undefined) + return v2; + current = current.outer; + } + return; +} +function define(name, value, env) { + env.bindings.set(name, value); +} +function extend(params, args, outer) { + if (params.length !== args.length) { + throw new EnvError("Number of parameters and arguments must match", { + params, + args, + outer + }); + } + const env = makeEnv(outer); + for (let i = 0;i < params.length; i++) { + define(params[i], args[i], env); + } + return env; +} +function getRootEnv(env) { + let current = env; + while (current?.outer) { + current = current.outer; + } + return current; +} +function getNamespaceEnv(env) { + let current = env; + while (current) { + if (current.ns) + return current; + current = current.outer; + } + return getRootEnv(env); +} + +// src/core/types.ts +var valueKeywords = { + number: "number", + string: "string", + boolean: "boolean", + keyword: "keyword", + nil: "nil", + symbol: "symbol", + list: "list", + vector: "vector", + map: "map", + function: "function", + nativeFunction: "native-function", + macro: "macro", + multiMethod: "multi-method", + atom: "atom", + reduced: "reduced", + volatile: "volatile", + regex: "regex", + var: "var", + set: "set", + delay: "delay", + lazySeq: "lazy-seq", + cons: "cons", + namespace: "namespace", + jsValue: "js-value" +}; +var tokenKeywords = { + LParen: "LParen", + RParen: "RParen", + LBracket: "LBracket", + RBracket: "RBracket", + LBrace: "LBrace", + RBrace: "RBrace", + String: "String", + Number: "Number", + Keyword: "Keyword", + Quote: "Quote", + Quasiquote: "Quasiquote", + Unquote: "Unquote", + UnquoteSplicing: "UnquoteSplicing", + Comment: "Comment", + Whitespace: "Whitespace", + Symbol: "Symbol", + AnonFnStart: "AnonFnStart", + Deref: "Deref", + Regex: "Regex", + VarQuote: "VarQuote", + Meta: "Meta", + SetStart: "SetStart" +}; +var tokenSymbols = { + Quote: "quote", + Quasiquote: "quasiquote", + Unquote: "unquote", + UnquoteSplicing: "unquote-splicing", + LParen: "(", + RParen: ")", + LBracket: "[", + RBracket: "]", + LBrace: "{", + RBrace: "}" +}; -;; ── Documentation ──────────────────────────────────────────────────────────── +// src/core/printer.ts +var LAZY_PRINT_CAP = 100; +function realizeLazy(ls) { + let current = ls; + while (current.kind === "lazy-seq") { + const lazy = current; + if (lazy.realized) { + current = lazy.value; + continue; + } + if (lazy.thunk) { + lazy.value = lazy.thunk(); + lazy.thunk = null; + lazy.realized = true; + current = lazy.value; + } else { + return { kind: "nil", value: null }; + } + } + return current; +} +function collectSeqElements(value, limit, depth) { + const items = []; + let current = value; + while (items.length < limit) { + if (current.kind === "nil") + break; + if (current.kind === "lazy-seq") { + current = realizeLazy(current); + continue; + } + if (current.kind === "cons") { + const c = current; + items.push(printString(c.head, depth + 1)); + current = c.tail; + continue; + } + if (current.kind === "list") { + for (const v2 of current.value) { + if (items.length >= limit) + break; + items.push(printString(v2, depth + 1)); + } + break; + } + if (current.kind === "vector") { + for (const v2 of current.value) { + if (items.length >= limit) + break; + items.push(printString(v2, depth + 1)); + } + break; + } + items.push(printString(current, depth + 1)); + break; + } + return { items, truncated: items.length >= limit }; +} +var _printCtx = { printLength: null, printLevel: null }; +function getPrintContext() { + return _printCtx; +} +function withPrintContext(ctx, fn) { + const prev = _printCtx; + _printCtx = ctx; + try { + return fn(); + } finally { + _printCtx = prev; + } +} +function printString(value, _depth = 0) { + const { printLevel } = _printCtx; + if (printLevel !== null && _depth >= printLevel) { + if (value.kind === "list" || value.kind === "vector" || value.kind === "map" || value.kind === "set" || value.kind === "cons" || value.kind === "lazy-seq") + return "#"; + } + return printStringImpl(value, _depth); +} +function printStringImpl(value, depth) { + switch (value.kind) { + case valueKeywords.number: + return value.value.toString(); + case valueKeywords.string: + let escapedBuffer = ""; + for (const char of value.value) { + switch (char) { + case '"': + escapedBuffer += "\\\""; + break; + case "\\": + escapedBuffer += "\\\\"; + break; + case ` +`: + escapedBuffer += "\\n"; + break; + case "\r": + escapedBuffer += "\\r"; + break; + case "\t": + escapedBuffer += "\\t"; + break; + default: + escapedBuffer += char; + } + } + return `"${escapedBuffer}"`; + case valueKeywords.boolean: + return value.value ? "true" : "false"; + case valueKeywords.nil: + return "nil"; + case valueKeywords.keyword: + return `${value.name}`; + case valueKeywords.symbol: + return `${value.name}`; + case valueKeywords.list: { + const { printLength } = _printCtx; + const items = printLength !== null ? value.value.slice(0, printLength) : value.value; + const suffix = printLength !== null && value.value.length > printLength ? " ..." : ""; + return `(${items.map((v2) => printString(v2, depth + 1)).join(" ")}${suffix})`; + } + case valueKeywords.vector: { + const { printLength } = _printCtx; + const items = printLength !== null ? value.value.slice(0, printLength) : value.value; + const suffix = printLength !== null && value.value.length > printLength ? " ..." : ""; + return `[${items.map((v2) => printString(v2, depth + 1)).join(" ")}${suffix}]`; + } + case valueKeywords.map: { + const { printLength } = _printCtx; + const entries = printLength !== null ? value.entries.slice(0, printLength) : value.entries; + const suffix = printLength !== null && value.entries.length > printLength ? " ..." : ""; + return `{${entries.map(([key, v2]) => `${printString(key, depth + 1)} ${printString(v2, depth + 1)}`).join(" ")}${suffix}}`; + } + case valueKeywords.function: { + if (value.arities.length === 1) { + const a = value.arities[0]; + const params = a.restParam ? [...a.params, { kind: "symbol", name: "&" }, a.restParam] : a.params; + return `(fn [${params.map(printString).join(" ")}] ${a.body.map(printString).join(" ")})`; + } + const clauses = value.arities.map((a) => { + const params = a.restParam ? [...a.params, { kind: "symbol", name: "&" }, a.restParam] : a.params; + return `([${params.map(printString).join(" ")}] ${a.body.map(printString).join(" ")})`; + }); + return `(fn ${clauses.join(" ")})`; + } + case valueKeywords.nativeFunction: + return `(native-fn ${value.name})`; + case valueKeywords.multiMethod: + return `(multi-method ${value.name})`; + case valueKeywords.atom: + return `#`; + case valueKeywords.reduced: + return `#`; + case valueKeywords.volatile: + return `#`; + case valueKeywords.regex: { + const escaped = value.pattern.replace(/"/g, "\\\""); + const prefix = value.flags ? `(?${value.flags})` : ""; + return `#"${prefix}${escaped}"`; + } + case valueKeywords.var: + return `#'${value.ns}/${value.name}`; + case valueKeywords.set: { + const { printLength } = _printCtx; + const items = printLength !== null ? value.values.slice(0, printLength) : value.values; + const suffix = printLength !== null && value.values.length > printLength ? " ..." : ""; + return `#{${items.map((v2) => printString(v2, depth + 1)).join(" ")}${suffix}}`; + } + case valueKeywords.delay: + if (value.realized) + return `#`; + return "#"; + case valueKeywords.lazySeq: + case valueKeywords.cons: { + const { printLength } = _printCtx; + const limit = printLength !== null ? printLength : LAZY_PRINT_CAP; + const { items, truncated } = collectSeqElements(value, limit, depth); + const suffix = truncated ? " ..." : ""; + return `(${items.join(" ")}${suffix})`; + } + case valueKeywords.namespace: + return `#namespace[${value.name}]`; + case "pending": + if (value.resolved && value.resolvedValue !== undefined) + return `#`; + return "#"; + case valueKeywords.jsValue: { + const raw = value.value; + let typeName; + if (raw === null) { + typeName = "null"; + } else if (raw === undefined) { + typeName = "undefined"; + } else if (typeof raw === "function") { + typeName = "Function"; + } else if (Array.isArray(raw)) { + typeName = "Array"; + } else if (raw instanceof Promise) { + typeName = "Promise"; + } else { + typeName = raw.constructor?.name ?? "Object"; + } + return `#`; + } + default: + throw new EvaluationError(`unhandled value type: ${value.kind}`, { + value + }); + } +} +function joinLines(lines) { + return lines.join(` +`); +} +var BODY_FORM_HEADER_COUNT = { + do: 0, + try: 0, + and: 0, + or: 0, + cond: 0, + "->": 0, + "->>": 0, + "some->": 0, + "some->>": 0, + when: 1, + "when-not": 1, + "when-let": 1, + "when-some": 1, + "when-first": 1, + if: 1, + "if-not": 1, + "if-let": 1, + "if-some": 1, + while: 1, + let: 1, + loop: 1, + binding: 1, + "with-open": 1, + "with-local-vars": 1, + locking: 1, + fn: 1, + "fn*": 1, + def: 1, + defonce: 1, + ns: 1, + doseq: 1, + dotimes: 1, + for: 1, + case: 1, + "cond->": 1, + "cond->>": 1, + defn: 2, + "defn-": 2, + defmacro: 2, + defmethod: 2 +}; +var BINDING_FORMS = new Set([ + "let", + "loop", + "binding", + "with-open", + "for", + "doseq", + "dotimes" +]); +var PAIR_BODY_FORMS = new Set(["cond", "condp", "case", "cond->", "cond->>"]); +function sp(n) { + return n > 0 ? " ".repeat(n) : ""; +} +function lastLineLen(s) { + const nl = s.lastIndexOf(` +`); + return nl === -1 ? s.length : s.length - nl - 1; +} +function pp(value, col, maxWidth) { + const flat = printString(value); + if (col + flat.length <= maxWidth) + return flat; + switch (value.kind) { + case valueKeywords.list: + return ppList(value.value, col, maxWidth); + case valueKeywords.vector: + return ppVec(value.value, col, maxWidth, false); + case valueKeywords.map: + return ppMap(value.entries, col, maxWidth); + case valueKeywords.set: + return ppSet(value.values, col, maxWidth); + case valueKeywords.lazySeq: + case valueKeywords.cons: + return flat; + default: + return flat; + } +} +function ppList(items, col, maxWidth) { + if (items.length === 0) + return "()"; + const [head, ...args] = items; + const headStr = printString(head); + const name = head.kind === valueKeywords.symbol ? head.name : null; + if (name !== null && name in BODY_FORM_HEADER_COUNT) { + const hCount = BODY_FORM_HEADER_COUNT[name]; + const headerArgs = args.slice(0, hCount); + const bodyArgs = args.slice(hCount); + const bodyIndent = col + 2; + let result = "(" + headStr; + let curCol = col + 1 + headStr.length; + for (let i = 0;i < headerArgs.length; i++) { + const arg = headerArgs[i]; + const argCol = curCol + 1; + const isPairVec = BINDING_FORMS.has(name) && i === 0 && arg.kind === valueKeywords.vector; + const argStr = isPairVec ? ppVec(arg.value, argCol, maxWidth, true) : pp(arg, argCol, maxWidth); + result += " " + argStr; + curCol = argStr.includes(` +`) ? lastLineLen(argStr) : argCol + argStr.length - 1; + } + if (bodyArgs.length === 0) + return result + ")"; + const bodyStr = PAIR_BODY_FORMS.has(name) ? ppPairs(bodyArgs, bodyIndent, maxWidth) : bodyArgs.map((a) => sp(bodyIndent) + pp(a, bodyIndent, maxWidth)).join(` +`); + return result + ` +` + bodyStr + ")"; + } + if (args.length === 0) + return "(" + headStr + ")"; + const firstArgCol = col + 1 + headStr.length + 1; + if (args.length === 1) { + return "(" + headStr + " " + pp(args[0], firstArgCol, maxWidth) + ")"; + } + const argIndent = headStr.length <= 10 ? firstArgCol : col + 2; + const argStrs = args.map((a) => pp(a, argIndent, maxWidth)); + if (argIndent === firstArgCol) { + return "(" + headStr + " " + argStrs[0] + ` +` + argStrs.slice(1).map((s) => sp(argIndent) + s).join(` +`) + ")"; + } + return "(" + headStr + ` +` + argStrs.map((s) => sp(argIndent) + s).join(` +`) + ")"; +} +function ppVec(items, col, maxWidth, pairMode) { + if (items.length === 0) + return "[]"; + const innerCol = col + 1; + if (pairMode) { + const lines = []; + for (let i = 0;i < items.length; i += 2) { + const prefix = i === 0 ? "" : sp(innerCol); + const keyFlat = printString(items[i]); + if (i + 1 >= items.length) { + lines.push(prefix + keyFlat); + continue; + } + const val = items[i + 1]; + const pairFlat = keyFlat + " " + printString(val); + if (innerCol + pairFlat.length <= maxWidth) { + lines.push(prefix + pairFlat); + } else { + const valStr = pp(val, innerCol + keyFlat.length + 1, maxWidth); + lines.push(prefix + keyFlat + " " + valStr); + } + } + return "[" + lines.join(` +`) + "]"; + } + const strs = items.map((item, i) => { + const s = pp(item, innerCol, maxWidth); + return (i === 0 ? "" : sp(innerCol)) + s; + }); + return "[" + strs.join(` +`) + "]"; +} +function ppMap(entries, col, maxWidth) { + if (entries.length === 0) + return "{}"; + const innerCol = col + 1; + const pairs = entries.map(([k, v2], i) => { + const kStr = printString(k); + const vStr = pp(v2, innerCol + kStr.length + 1, maxWidth); + return (i === 0 ? "" : sp(innerCol)) + kStr + " " + vStr; + }); + return "{" + pairs.join(` +`) + "}"; +} +function ppSet(items, col, maxWidth) { + if (items.length === 0) + return "#{}"; + const innerCol = col + 2; + const strs = items.map((item, i) => { + const s = pp(item, innerCol, maxWidth); + return (i === 0 ? "" : sp(innerCol)) + s; + }); + return "#{" + strs.join(` +`) + "}"; +} +function ppPairs(items, indent, maxWidth) { + const lines = []; + for (let i = 0;i < items.length; i += 2) { + const testStr = pp(items[i], indent, maxWidth); + if (i + 1 >= items.length) { + lines.push(sp(indent) + testStr); + continue; + } + const exprFlat = printString(items[i + 1]); + const pairFlat = testStr + " " + exprFlat; + if (indent + pairFlat.length <= maxWidth) { + lines.push(sp(indent) + pairFlat); + } else { + lines.push(sp(indent) + testStr + ` +` + sp(indent + 2) + pp(items[i + 1], indent + 2, maxWidth)); + } + } + return lines.join(` +`); +} +function prettyPrintString(value, maxWidth = 80) { + return pp(value, 0, maxWidth); +} -(defmacro doc [sym] - \`(let [v# ~sym - m# (meta v#) - d# (:doc m#) - args# (:arglists m#) - args-str# (when args# - (str "(" - (reduce - (fn [acc# a#] - (if (= acc# "") - (str a#) - (str acc# " " a#))) - "" - args#) - ")"))] - (println (str "-------------------------\\n" - ~(str sym) "\\n" - (if args-str# (str args-str# "\\n") "") - " " (or d# "No documentation available."))))) +// src/core/transformations.ts +function valueToString(value) { + switch (value.kind) { + case valueKeywords.string: + return value.value; + case valueKeywords.number: + return value.value.toString(); + case valueKeywords.boolean: + return value.value ? "true" : "false"; + case valueKeywords.keyword: + return value.name; + case valueKeywords.symbol: + return value.name; + case valueKeywords.list: { + const { printLength } = getPrintContext(); + const items = printLength !== null ? value.value.slice(0, printLength) : value.value; + const suffix = printLength !== null && value.value.length > printLength ? " ..." : ""; + return `(${items.map(valueToString).join(" ")}${suffix})`; + } + case valueKeywords.vector: { + const { printLength } = getPrintContext(); + const items = printLength !== null ? value.value.slice(0, printLength) : value.value; + const suffix = printLength !== null && value.value.length > printLength ? " ..." : ""; + return `[${items.map(valueToString).join(" ")}${suffix}]`; + } + case valueKeywords.map: { + const { printLength } = getPrintContext(); + const entries = printLength !== null ? value.entries.slice(0, printLength) : value.entries; + const suffix = printLength !== null && value.entries.length > printLength ? " ..." : ""; + return `{${entries.map(([key, v2]) => `${valueToString(key)} ${valueToString(v2)}`).join(" ")}${suffix}}`; + } + case valueKeywords.set: { + const { printLength } = getPrintContext(); + const items = printLength !== null ? value.values.slice(0, printLength) : value.values; + const suffix = printLength !== null && value.values.length > printLength ? " ..." : ""; + return `#{${items.map(valueToString).join(" ")}${suffix}}`; + } + case valueKeywords.function: { + if (value.arities.length === 1) { + const a = value.arities[0]; + const params = a.restParam ? [...a.params, { kind: "symbol", name: "&" }, a.restParam] : a.params; + return `(fn [${params.map(valueToString).join(" ")}] ${a.body.map(valueToString).join(" ")})`; + } + const clauses = value.arities.map((a) => { + const params = a.restParam ? [...a.params, { kind: "symbol", name: "&" }, a.restParam] : a.params; + return `([${params.map(valueToString).join(" ")}] ${a.body.map(valueToString).join(" ")})`; + }); + return `(fn ${clauses.join(" ")})`; + } + case valueKeywords.nativeFunction: + return `(native-fn ${value.name})`; + case valueKeywords.nil: + return "nil"; + case valueKeywords.regex: { + const prefix = value.flags ? `(?${value.flags})` : ""; + return `${prefix}${value.pattern}`; + } + case valueKeywords.delay: + return value.realized ? `#` : "#"; + case valueKeywords.lazySeq: { + const realized = realizeLazySeq(value); + if (isNil(realized)) + return "()"; + return valueToString(realized); + } + case valueKeywords.cons: { + const items = consToArray(value); + const { printLength } = getPrintContext(); + const visible = printLength !== null ? items.slice(0, printLength) : items; + const suffix = printLength !== null && items.length > printLength ? " ..." : ""; + return `(${visible.map(valueToString).join(" ")}${suffix})`; + } + case valueKeywords.namespace: + return `#namespace[${value.name}]`; + case "pending": + if (value.resolved && value.resolvedValue !== undefined) + return `#`; + return "#"; + default: + throw new EvaluationError(`unhandled value type: ${value.kind}`, { + value + }); + } +} +function realizeDelay(d) { + if (d.realized) + return d.value; + d.value = d.thunk(); + d.realized = true; + return d.value; +} +function realizeLazySeq(ls) { + let current = ls; + while (current.kind === "lazy-seq") { + const lazy = current; + if (lazy.realized) { + current = lazy.value; + continue; + } + if (lazy.thunk) { + lazy.value = lazy.thunk(); + lazy.thunk = null; + lazy.realized = true; + current = lazy.value; + } else { + return { kind: "nil", value: null }; + } + } + return current; +} +var toSeq = (collection) => { + if (isList(collection)) { + return collection.value; + } + if (isVector(collection)) { + return collection.value; + } + if (isMap(collection)) { + return collection.entries.map(([k, v2]) => cljVector([k, v2])); + } + if (isSet(collection)) { + return collection.values; + } + if (collection.kind === "string") { + return [...collection.value].map(cljString); + } + if (isLazySeq(collection)) { + const realized = realizeLazySeq(collection); + if (isNil(realized)) + return []; + return toSeq(realized); + } + if (isCons(collection)) { + return consToArray(collection); + } + throw new EvaluationError(`toSeq expects a collection or string, got ${printString(collection)}`, { collection }); +}; +function consToArray(c) { + const result = [c.head]; + let tail = c.tail; + while (true) { + if (isNil(tail)) + break; + if (isCons(tail)) { + result.push(tail.head); + tail = tail.tail; + continue; + } + if (isLazySeq(tail)) { + tail = realizeLazySeq(tail); + continue; + } + if (isList(tail)) { + result.push(...tail.value); + break; + } + if (isVector(tail)) { + result.push(...tail.value); + break; + } + result.push(...toSeq(tail)); + break; + } + return result; +} -(defn err - "Creates an error map with type, message, data and optionally cause" - ([type message] (err type message nil nil)) - ([type message data] (err type message data nil)) - ([type message data cause] {:type type :message message :data data :cause cause}))`; +// src/core/evaluator/destructure.ts +function toSeqSafe(value) { + if (is.nil(value)) + return []; + if (is.list(value)) + return value.value; + if (is.vector(value)) + return value.value; + if (is.lazySeq(value)) { + const realized = realizeLazySeq(value); + return toSeqSafe(realized); + } + if (is.cons(value)) + return consToArray(value); + throw new EvaluationError(`Cannot destructure ${value.kind} as a sequential collection`, { value }); +} +function seqFirst(value) { + if (is.nil(value)) + return cljNil(); + if (is.lazySeq(value)) { + const realized = realizeLazySeq(value); + return is.nil(realized) ? v.nil() : seqFirst(realized); + } + if (is.cons(value)) + return value.head; + if (is.list(value) || is.vector(value)) + return value.value.length > 0 ? value.value[0] : v.nil(); + return v.nil(); +} +function seqRest(value) { + if (is.nil(value)) + return v.list([]); + if (is.lazySeq(value)) { + const realized = realizeLazySeq(value); + return is.nil(realized) ? v.list([]) : seqRest(realized); + } + if (is.cons(value)) + return value.tail; + if (is.list(value)) + return v.list(value.value.slice(1)); + if (is.vector(value)) + return v.list(value.value.slice(1)); + return v.list([]); +} +function seqIsEmpty(value) { + if (is.nil(value)) + return true; + if (is.lazySeq(value)) { + const realized = realizeLazySeq(value); + return seqIsEmpty(realized); + } + if (is.cons(value)) + return false; + if (is.list(value) || is.vector(value)) + return value.value.length === 0; + return true; +} +function isLazy(value) { + return is.lazySeq(value) || is.cons(value); +} +function findMapEntry(map, key) { + const entry = map.entries.find(([k]) => is.equal(k, key)); + return entry ? entry[1] : undefined; +} +function mapContainsKey(map, key) { + return map.entries.some(([k]) => is.equal(k, key)); +} +function destructureVector(pattern, value, ctx, env) { + const pairs = []; + const elems = [...pattern]; + const asIdx = elems.findIndex((e) => is.keyword(e) && e.kind === "keyword" && e.name === ":as"); + if (asIdx !== -1) { + const asSym = elems[asIdx + 1]; + if (!asSym || !is.symbol(asSym)) { + throw new EvaluationError(":as must be followed by a symbol", { pattern }); + } + pairs.push([asSym.name, value]); + elems.splice(asIdx, 2); + } + const ampIdx = elems.findIndex((e) => is.symbol(e) && e.name === "&"); + let restPattern = null; + let positionalCount; + if (ampIdx !== -1) { + restPattern = elems[ampIdx + 1]; + if (!restPattern) { + throw new EvaluationError("& must be followed by a binding pattern", { + pattern + }); + } + positionalCount = ampIdx; + elems.splice(ampIdx); + } else { + positionalCount = elems.length; + } + if (isLazy(value)) { + let current = value; + for (let i = 0;i < positionalCount; i++) { + pairs.push(...destructureBindings(elems[i], seqFirst(current), ctx, env)); + current = seqRest(current); + } + if (restPattern !== null) { + if (is.map(restPattern) && !seqIsEmpty(current)) { + const restArgs = toSeqSafe(current); + const entries = []; + for (let i = 0;i < restArgs.length; i += 2) { + entries.push([restArgs[i], restArgs[i + 1] ?? cljNil()]); + } + pairs.push(...destructureBindings(restPattern, { kind: "map", entries }, ctx, env)); + } else { + const restValue = seqIsEmpty(current) ? cljNil() : current; + pairs.push(...destructureBindings(restPattern, restValue, ctx, env)); + } + } + } else { + const seq = toSeqSafe(value); + for (let i = 0;i < positionalCount; i++) { + pairs.push(...destructureBindings(elems[i], seq[i] ?? cljNil(), ctx, env)); + } + if (restPattern !== null) { + const restArgs = seq.slice(positionalCount); + let restValue; + if (is.map(restPattern) && restArgs.length > 0) { + const entries = []; + for (let i = 0;i < restArgs.length; i += 2) { + entries.push([restArgs[i], restArgs[i + 1] ?? cljNil()]); + } + restValue = { kind: "map", entries }; + } else { + restValue = restArgs.length > 0 ? cljList(restArgs) : cljNil(); + } + pairs.push(...destructureBindings(restPattern, restValue, ctx, env)); + } + } + return pairs; +} +function destructureMap(pattern, value, ctx, env) { + const pairs = []; + const orMapVal = findMapEntry(pattern, cljKeyword(":or")); + const orMap = orMapVal && is.map(orMapVal) ? orMapVal : null; + const asVal = findMapEntry(pattern, cljKeyword(":as")); + if (!is.map(value) && value.kind !== "nil") { + throw new EvaluationError(`Cannot destructure ${value.kind} as a map`, { + value, + pattern + }); + } + const targetMap = value.kind === "nil" ? { kind: "map", entries: [] } : value; + for (const [k, v2] of pattern.entries) { + if (is.keyword(k) && k.name === ":or") + continue; + if (is.keyword(k) && k.name === ":as") + continue; + if (is.keyword(k) && k.name === ":keys") { + if (!is.vector(v2)) { + throw new EvaluationError(":keys must be followed by a vector of symbols", { pattern }); + } + for (const sym of v2.value) { + if (!is.symbol(sym)) { + throw new EvaluationError(":keys vector must contain symbols", { + pattern, + sym + }); + } + const slashIdx = sym.name.indexOf("/"); + const localName = slashIdx !== -1 ? sym.name.slice(slashIdx + 1) : sym.name; + const lookupKey = cljKeyword(":" + sym.name); + const present2 = mapContainsKey(targetMap, lookupKey); + const entry2 = present2 ? findMapEntry(targetMap, lookupKey) : undefined; + let result; + if (present2) { + result = entry2; + } else if (orMap) { + const orDefault = findMapEntry(orMap, cljSymbol(localName)); + result = orDefault !== undefined ? ctx.evaluate(orDefault, env) : cljNil(); + } else { + result = cljNil(); + } + pairs.push([localName, result]); + } + continue; + } + if (is.keyword(k) && k.name === ":strs") { + if (!is.vector(v2)) { + throw new EvaluationError(":strs must be followed by a vector of symbols", { pattern }); + } + for (const sym of v2.value) { + if (!is.symbol(sym)) { + throw new EvaluationError(":strs vector must contain symbols", { + pattern, + sym + }); + } + const lookupKey = cljString(sym.name); + const present2 = mapContainsKey(targetMap, lookupKey); + const entry2 = present2 ? findMapEntry(targetMap, lookupKey) : undefined; + let result; + if (present2) { + result = entry2; + } else if (orMap) { + const orDefault = findMapEntry(orMap, cljSymbol(sym.name)); + result = orDefault !== undefined ? ctx.evaluate(orDefault, env) : cljNil(); + } else { + result = cljNil(); + } + pairs.push([sym.name, result]); + } + continue; + } + if (is.keyword(k) && k.name === ":syms") { + if (!is.vector(v2)) { + throw new EvaluationError(":syms must be followed by a vector of symbols", { pattern }); + } + for (const sym of v2.value) { + if (!is.symbol(sym)) { + throw new EvaluationError(":syms vector must contain symbols", { + pattern, + sym + }); + } + const lookupKey = cljSymbol(sym.name); + const present2 = mapContainsKey(targetMap, lookupKey); + const entry2 = present2 ? findMapEntry(targetMap, lookupKey) : undefined; + let result; + if (present2) { + result = entry2; + } else if (orMap) { + const orDefault = findMapEntry(orMap, cljSymbol(sym.name)); + result = orDefault !== undefined ? ctx.evaluate(orDefault, env) : cljNil(); + } else { + result = cljNil(); + } + pairs.push([sym.name, result]); + } + continue; + } + const entry = findMapEntry(targetMap, v2); + const present = mapContainsKey(targetMap, v2); + let boundVal; + if (present) { + boundVal = entry; + } else if (orMap && is.symbol(k)) { + const orDefault = findMapEntry(orMap, cljSymbol(k.name)); + boundVal = orDefault !== undefined ? ctx.evaluate(orDefault, env) : cljNil(); + } else { + boundVal = cljNil(); + } + pairs.push(...destructureBindings(k, boundVal, ctx, env)); + } + if (asVal && is.symbol(asVal)) { + pairs.push([asVal.name, value]); + } + return pairs; +} +function destructureBindings(pattern, value, ctx, env) { + if (is.symbol(pattern)) { + return [[pattern.name, value]]; + } + if (is.vector(pattern)) { + return destructureVector(pattern.value, value, ctx, env); + } + if (is.map(pattern)) { + return destructureMap(pattern, value, ctx, env); + } + throw new EvaluationError(`Invalid destructuring pattern: expected symbol, vector, or map, got ${pattern.kind}`, { pattern }); +} -// src/clojure/generated/clojure-string-source.ts -var clojure_stringSource = `(ns clojure.string) +// src/core/evaluator/arity.ts +class RecurSignal { + args; + constructor(args) { + this.args = args; + } +} +function parseParamVector(args, env) { + const ampIdx = args.value.findIndex((a) => is.symbol(a) && a.name === "&"); + let params = []; + let restParam = null; + if (ampIdx === -1) { + params = args.value; + } else { + const ampsCount = args.value.filter((a) => is.symbol(a) && a.name === "&").length; + if (ampsCount > 1) { + throw new EvaluationError("& can only appear once", { args, env }); + } + if (ampIdx !== args.value.length - 2) { + throw new EvaluationError("& must be second-to-last argument", { + args, + env + }); + } + params = args.value.slice(0, ampIdx); + restParam = args.value[ampIdx + 1]; + } + return { params, restParam }; +} +function parseArities(forms, env) { + if (forms.length === 0) { + throw new EvaluationError("fn/defmacro requires at least a parameter vector", { + forms, + env + }); + } + if (is.vector(forms[0])) { + const paramVec = forms[0]; + const { params, restParam } = parseParamVector(paramVec, env); + return [{ params, restParam, body: forms.slice(1) }]; + } + if (is.list(forms[0])) { + const arities = []; + for (const form of forms) { + if (!is.list(form) || form.value.length === 0) { + throw new EvaluationError("Multi-arity clause must be a list starting with a parameter vector", { form, env }); + } + const paramVec = form.value[0]; + if (!is.vector(paramVec)) { + throw new EvaluationError("First element of arity clause must be a parameter vector", { paramVec, env }); + } + const { params, restParam } = parseParamVector(paramVec, env); + arities.push({ params, restParam, body: form.value.slice(1) }); + } + const variadicCount = arities.filter((a) => a.restParam !== null).length; + if (variadicCount > 1) { + throw new EvaluationError("At most one variadic arity is allowed per function", { forms, env }); + } + return arities; + } + throw new EvaluationError("fn/defmacro expects a parameter vector or arity clauses", { forms, env }); +} +function bindParams(params, restParam, args, outerEnv, ctx, bindEnv) { + if (restParam === null) { + if (args.length !== params.length) { + throw new EvaluationError(`Arguments length mismatch: fn accepts ${params.length} arguments, but ${args.length} were provided`, { params, args, outerEnv }); + } + } else { + if (args.length < params.length) { + throw new EvaluationError(`Arguments length mismatch: fn expects at least ${params.length} arguments, but ${args.length} were provided`, { params, args, outerEnv }); + } + } + const allPairs = []; + for (let i = 0;i < params.length; i++) { + allPairs.push(...destructureBindings(params[i], args[i], ctx, bindEnv)); + } + if (restParam !== null) { + const restArgs = args.slice(params.length); + let restValue; + if (is.map(restParam) && restArgs.length > 0) { + const entries = []; + for (let i = 0;i < restArgs.length; i += 2) { + entries.push([restArgs[i], restArgs[i + 1] ?? cljNil()]); + } + restValue = { kind: "map", entries }; + } else { + restValue = restArgs.length > 0 ? cljList(restArgs) : cljNil(); + } + allPairs.push(...destructureBindings(restParam, restValue, ctx, bindEnv)); + } + return extend(allPairs.map(([n]) => n), allPairs.map(([, v2]) => v2), outerEnv); +} +function resolveArity(arities, argCount) { + const exactMatch = arities.find((a) => a.restParam === null && a.params.length === argCount); + if (exactMatch) + return exactMatch; + const variadicMatch = arities.find((a) => a.restParam !== null && argCount >= a.params.length); + if (variadicMatch) + return variadicMatch; + const counts = arities.map((a) => a.restParam ? `${a.params.length}+` : `${a.params.length}`); + throw new EvaluationError(`No matching arity for ${argCount} arguments. Available arities: ${counts.join(", ")}`, { arities, argCount }); +} -;; Runtime-injected native helpers. Declared here so clojure-lsp can resolve -;; them; the interpreter treats bare (def name) as a no-op and leaves the -;; native binding from coreEnv intact. -(def str-split*) -(def str-upper-case*) -(def str-lower-case*) -(def str-trim*) -(def str-triml*) -(def str-trimr*) -(def str-reverse*) -(def str-starts-with*) -(def str-ends-with*) -(def str-includes*) -(def str-index-of*) -(def str-last-index-of*) -(def str-replace*) -(def str-replace-first*) +// src/core/evaluator/async-evaluator.ts +function createAsyncEvalCtx(syncCtx) { + const asyncCtx = { + syncCtx, + evaluate: (expr, env) => evaluateFormAsync(expr, env, asyncCtx), + evaluateForms: (forms, env) => evaluateFormsAsync(forms, env, asyncCtx), + applyCallable: (fn, args, callEnv) => applyCallableAsync(fn, args, callEnv, asyncCtx) + }; + return asyncCtx; +} +async function evaluateFormAsync(expr, env, asyncCtx) { + switch (expr.kind) { + case "number": + case "string": + case "boolean": + case "keyword": + case "nil": + case "symbol": + case "function": + case "native-function": + case "macro": + case "multi-method": + case "atom": + case "reduced": + case "volatile": + case "regex": + case "var": + case "delay": + case "lazy-seq": + case "cons": + case "namespace": + case "pending": + return asyncCtx.syncCtx.evaluate(expr, env); + } + if (expr.kind === "vector") { + const elements = []; + for (const el of expr.value) { + elements.push(await evaluateFormAsync(el, env, asyncCtx)); + } + return { kind: "vector", value: elements }; + } + if (expr.kind === "map") { + const entries = []; + for (const [k, v2] of expr.entries) { + const ek = await evaluateFormAsync(k, env, asyncCtx); + const ev = await evaluateFormAsync(v2, env, asyncCtx); + entries.push([ek, ev]); + } + return { kind: "map", entries }; + } + if (expr.kind === "set") { + const elements = []; + for (const el of expr.values) { + elements.push(await evaluateFormAsync(el, env, asyncCtx)); + } + return { kind: "set", values: elements }; + } + if (expr.kind === "list") { + return evaluateListAsync(expr, env, asyncCtx); + } + return asyncCtx.syncCtx.evaluate(expr, env); +} +async function evaluateFormsAsync(forms, env, asyncCtx) { + let result = cljNil(); + for (const form of forms) { + const expanded = asyncCtx.syncCtx.expandAll(form, env); + result = await evaluateFormAsync(expanded, env, asyncCtx); + } + return result; +} +var ASYNC_SPECIAL_FORMS = new Set([ + "quote", + "def", + "if", + "do", + "let", + "let*", + "fn", + "fn*", + "loop", + "recur", + "binding", + "set!", + "try", + "var", + "defmacro", + "defmulti", + "defmethod", + "letfn", + "quasiquote", + "delay", + "lazy-seq", + "ns", + "async" +]); +async function evaluateListAsync(list, env, asyncCtx) { + if (list.value.length === 0) + return list; + const first = list.value[0]; + if (first.kind === "symbol" && ASYNC_SPECIAL_FORMS.has(first.name)) { + return evaluateSpecialFormAsync(first.name, list, env, asyncCtx); + } + const fn = await evaluateFormAsync(first, env, asyncCtx); + if (is.aFunction(fn) && fn.name === "deref" && list.value.length === 2) { + const val = await evaluateFormAsync(list.value[1], env, asyncCtx); + if (val.kind === "pending") { + return val.promise; + } + return asyncCtx.syncCtx.applyCallable(fn, [val], env); + } + const args = []; + for (const arg of list.value.slice(1)) { + args.push(await evaluateFormAsync(arg, env, asyncCtx)); + } + return applyCallableAsync(fn, args, env, asyncCtx); +} +async function evaluateSpecialFormAsync(name, list, env, asyncCtx) { + switch (name) { + case "quote": + case "var": + case "ns": + case "fn": + case "fn*": + return asyncCtx.syncCtx.evaluate(list, env); + case "recur": { + const args = []; + for (const arg of list.value.slice(1)) { + args.push(await evaluateFormAsync(arg, env, asyncCtx)); + } + throw new RecurSignal(args); + } + case "do": + return evaluateFormsAsync(list.value.slice(1), env, asyncCtx); + case "def": + throw new EvaluationError("def inside (async ...) is not supported. Define vars outside the async block.", { list, env }); + case "if": { + const condition = await evaluateFormAsync(list.value[1], env, asyncCtx); + const isTruthy = condition.kind !== "nil" && !(condition.kind === "boolean" && !condition.value); + if (isTruthy) { + return evaluateFormAsync(list.value[2], env, asyncCtx); + } + return list.value[3] !== undefined ? evaluateFormAsync(list.value[3], env, asyncCtx) : cljNil(); + } + case "let": + case "let*": + return evaluateLetAsync(list, env, asyncCtx); + case "loop": + return evaluateLoopAsync(list, env, asyncCtx); + case "binding": + return evaluateBindingAsync(list, env, asyncCtx); + case "try": + return evaluateTryAsync(list, env, asyncCtx); + case "set!": { + const newVal = await evaluateFormAsync(list.value[2], env, asyncCtx); + const quotedVal = { + kind: "list", + value: [{ kind: "symbol", name: "quote" }, newVal] + }; + const newList = { + kind: "list", + value: [list.value[0], list.value[1], quotedVal] + }; + return asyncCtx.syncCtx.evaluate(newList, env); + } + case "quasiquote": + return asyncCtx.syncCtx.evaluate(list, env); + default: + return asyncCtx.syncCtx.evaluate(list, env); + } +} +async function evaluateLetAsync(list, env, asyncCtx) { + const bindings = list.value[1]; + if (!is.vector(bindings)) { + throw new EvaluationError("let bindings must be a vector", { list, env }); + } + if (bindings.value.length % 2 !== 0) { + throw new EvaluationError("let bindings must have an even number of forms", { list, env }); + } + let currentEnv = env; + const pairs = bindings.value; + for (let i = 0;i < pairs.length; i += 2) { + const pattern = pairs[i]; + const valueForm = pairs[i + 1]; + const value = await evaluateFormAsync(valueForm, currentEnv, asyncCtx); + const boundPairs = destructureBindings(pattern, value, asyncCtx.syncCtx, currentEnv); + currentEnv = extend(boundPairs.map(([n]) => n), boundPairs.map(([, v2]) => v2), currentEnv); + } + return evaluateFormsAsync(list.value.slice(2), currentEnv, asyncCtx); +} +async function evaluateLoopAsync(list, env, asyncCtx) { + const loopBindings = list.value[1]; + if (!is.vector(loopBindings)) { + throw new EvaluationError("loop bindings must be a vector", { list, env }); + } + if (loopBindings.value.length % 2 !== 0) { + throw new EvaluationError("loop bindings must have an even number of forms", { list, env }); + } + const loopBody = list.value.slice(2); + const patterns = []; + let currentValues = []; + let initEnv = env; + for (let i = 0;i < loopBindings.value.length; i += 2) { + const pattern = loopBindings.value[i]; + const value = await evaluateFormAsync(loopBindings.value[i + 1], initEnv, asyncCtx); + patterns.push(pattern); + currentValues.push(value); + const boundPairs = destructureBindings(pattern, value, asyncCtx.syncCtx, initEnv); + initEnv = extend(boundPairs.map(([n]) => n), boundPairs.map(([, v2]) => v2), initEnv); + } + while (true) { + let loopEnv = env; + for (let i = 0;i < patterns.length; i++) { + const boundPairs = destructureBindings(patterns[i], currentValues[i], asyncCtx.syncCtx, loopEnv); + loopEnv = extend(boundPairs.map(([n]) => n), boundPairs.map(([, v2]) => v2), loopEnv); + } + try { + return await evaluateFormsAsync(loopBody, loopEnv, asyncCtx); + } catch (e) { + if (e instanceof RecurSignal) { + if (e.args.length !== patterns.length) { + throw new EvaluationError(`recur expects ${patterns.length} arguments but got ${e.args.length}`, { list, env }); + } + currentValues = e.args; + continue; + } + throw e; + } + } +} +async function evaluateBindingAsync(list, env, asyncCtx) { + return asyncCtx.syncCtx.evaluate(list, env); +} +async function evaluateTryAsync(list, env, asyncCtx) { + const forms = list.value.slice(1); + const bodyForms = []; + const catchClauses = []; + let finallyForms = null; + for (let i = 0;i < forms.length; i++) { + const form = forms[i]; + if (form.kind === "list" && form.value.length > 0 && form.value[0].kind === "symbol") { + const head = form.value[0].name; + if (head === "catch") { + catchClauses.push({ + discriminator: form.value[1], + binding: form.value[2].name, + body: form.value.slice(3) + }); + continue; + } + if (head === "finally") { + finallyForms = form.value.slice(1); + continue; + } + } + bodyForms.push(form); + } + let result = cljNil(); + let pendingThrow = null; + try { + result = await evaluateFormsAsync(bodyForms, env, asyncCtx); + } catch (e) { + if (e instanceof RecurSignal) + throw e; + let thrownValue; + if (e instanceof CljThrownSignal) { + thrownValue = e.value; + } else if (e instanceof EvaluationError) { + thrownValue = { + kind: "map", + entries: [ + [ + { kind: "keyword", name: ":type" }, + { kind: "keyword", name: ":error/runtime" } + ], + [ + { kind: "keyword", name: ":message" }, + { kind: "string", value: e.message } + ] + ] + }; + } else { + throw e; + } + let handled = false; + for (const clause of catchClauses) { + const catchEnv = extend([clause.binding], [thrownValue], env); + result = await evaluateFormsAsync(clause.body, catchEnv, asyncCtx); + handled = true; + break; + } + if (!handled) { + pendingThrow = e; + } + } finally { + if (finallyForms) { + await evaluateFormsAsync(finallyForms, env, asyncCtx); + } + } + if (pendingThrow !== null) + throw pendingThrow; + return result; +} +async function applyCallableAsync(fn, args, callEnv, asyncCtx) { + if (fn.kind === "native-function") { + if (fn.fnWithContext) { + return fn.fnWithContext(asyncCtx.syncCtx, callEnv, ...args); + } + return fn.fn(...args); + } + if (fn.kind === "function") { + const arity = resolveArity(fn.arities, args.length); + let currentArgs = args; + while (true) { + const localEnv = bindParams(arity.params, arity.restParam, currentArgs, fn.env, asyncCtx.syncCtx, callEnv); + try { + return await evaluateFormsAsync(arity.body, localEnv, asyncCtx); + } catch (e) { + if (e instanceof RecurSignal) { + currentArgs = e.args; + continue; + } + throw e; + } + } + } + return asyncCtx.syncCtx.applyCallable(fn, args, callEnv); +} -;; --------------------------------------------------------------------------- -;; Joining / splitting -;; --------------------------------------------------------------------------- +// src/core/positions.ts +function setPos(val, pos) { + Object.defineProperty(val, "_pos", { + value: pos, + enumerable: false, + writable: true, + configurable: true + }); +} +function getPos(val) { + return val._pos; +} +function getLineCol(source, offset) { + const lines = source.split(` +`); + let pos = 0; + for (let i = 0;i < lines.length; i++) { + const lineEnd = pos + lines[i].length; + if (offset <= lineEnd) { + return { line: i + 1, col: offset - pos, lineText: lines[i] }; + } + pos = lineEnd + 1; + } + const last = lines[lines.length - 1]; + return { line: lines.length, col: last.length, lineText: last }; +} +function formatErrorContext(source, pos, opts) { + const { line, col, lineText } = getLineCol(source, pos.start); + const absLine = line + (opts?.lineOffset ?? 0); + const absCol = line === 1 ? col + (opts?.colOffset ?? 0) : col; + const span = Math.max(1, pos.end - pos.start); + const caret = " ".repeat(col) + "^".repeat(span); + return ` + at line ${absLine}, col ${absCol + 1}: + ${lineText} + ${caret}`; +} -(defn join - "Returns a string of all elements in coll, as returned by (str), separated - by an optional separator." - ([coll] (join "" coll)) - ([separator coll] - (if (nil? coll) - "" - (reduce - (fn [acc x] - (if (= acc "") - (str x) - (str acc separator x))) - "" - coll)))) +// src/core/evaluator/js-interop.ts +function jsToClj(raw) { + if (raw === null) + return cljNil(); + if (raw === undefined) + return cljJsValue(undefined); + if (typeof raw === "number") + return cljNumber(raw); + if (typeof raw === "string") + return cljString(raw); + if (typeof raw === "boolean") + return cljBoolean(raw); + return cljJsValue(raw); +} +function mapKeyToString(key) { + if (key.kind === "string") + return key.value; + if (key.kind === "keyword") + return key.name.slice(1); + if (key.kind === "number") + return String(key.value); + if (key.kind === "boolean") + return String(key.value); + throw new EvaluationError(`cljToJs: map key must be a string, keyword, number, or boolean — ` + `got ${key.kind} (rich keys are not allowed as JS object keys; reduce to a primitive first)`, { key }); +} +function cljToJs(val, ctx, callEnv) { + switch (val.kind) { + case "js-value": + return val.value; + case "number": + return val.value; + case "string": + return val.value; + case "boolean": + return val.value; + case "nil": + return null; + case "keyword": + return val.name.slice(1); + case "function": + case "native-function": { + const fn = val; + return (...jsArgs) => { + const cljArgs = jsArgs.map(jsToClj); + const result = ctx.applyCallable(fn, cljArgs, callEnv); + return cljToJs(result, ctx, callEnv); + }; + } + case "list": + case "vector": + return val.value.map((v2) => cljToJs(v2, ctx, callEnv)); + case "map": { + const obj = {}; + for (const [key, value] of val.entries) { + obj[mapKeyToString(key)] = cljToJs(value, ctx, callEnv); + } + return obj; + } + default: + throw new EvaluationError(`cannot convert ${val.kind} to JS value — no coercion defined`, { val }); + } +} +function extractRawTarget(target) { + switch (target.kind) { + case "js-value": + return target.value; + case "string": + case "number": + case "boolean": + return target.value; + default: + throw new EvaluationError(`cannot use . on ${target.kind}`, { target }); + } +} +function evaluateDot(list, env, ctx) { + if (list.value.length < 3) { + throw new EvaluationError(". requires at least 2 arguments: (. obj prop)", { list }); + } + const target = ctx.evaluate(list.value[1], env); + const rawTarget = extractRawTarget(target); + if (rawTarget === null || rawTarget === undefined) { + const label = rawTarget === null ? "null" : "undefined"; + throw new EvaluationError(`cannot use . on ${label} js value — check for nil/undefined before accessing properties`, { target }); + } + const propForm = list.value[2]; + if (!is.symbol(propForm)) { + throw new EvaluationError(`. expects a symbol for property name, got: ${propForm.kind}`, { propForm }); + } + const propName = propForm.name; + const rawObj = rawTarget; + if (list.value.length === 3) { + const rawProp = rawObj[propName]; + if (typeof rawProp === "function") { + return cljJsValue(rawProp.bind(rawObj)); + } + return jsToClj(rawProp); + } + const method = rawObj[propName]; + if (typeof method !== "function") { + throw new EvaluationError(`method '${propName}' is not callable on ${String(rawObj)}`, { propName, rawObj }); + } + const cljArgs = list.value.slice(3).map((a) => ctx.evaluate(a, env)); + const jsArgs = cljArgs.map((a) => cljToJs(a, ctx, env)); + const rawResult = method.apply(rawObj, jsArgs); + return jsToClj(rawResult); +} +function evaluateNew(list, env, ctx) { + if (list.value.length < 2) { + throw new EvaluationError("js/new requires a constructor argument", { list }); + } + const cls = ctx.evaluate(list.value[1], env); + if (!is.jsValue(cls) || typeof cls.value !== "function") { + throw new EvaluationError(`js/new: expected js-value constructor, got ${cls.kind}`, { cls }); + } + const cljArgs = list.value.slice(2).map((a) => ctx.evaluate(a, env)); + const jsArgs = cljArgs.map((a) => cljToJs(a, ctx, env)); + const ctor = cls.value; + return cljJsValue(new ctor(...jsArgs)); +} -(defn split - "Splits string on a regular expression. Optional limit is the maximum number - of parts returned. Trailing empty strings are not returned by default; pass - a limit of -1 to return all." - ([s sep] (str-split* s sep)) - ([s sep limit] (str-split* s sep limit))) +// src/core/gensym.ts +var _counter = 0; +function makeGensym(prefix = "G") { + return `${prefix}__${_counter++}`; +} -(defn split-lines - "Splits s on \\\\n or \\\\r\\\\n. Trailing empty lines are not returned." - [s] - (split s #"\\r?\\n")) +// src/core/evaluator/quasiquote.ts +function evaluateQuasiquote(form, env, autoGensyms = new Map, ctx) { + switch (form.kind) { + case valueKeywords.vector: + case valueKeywords.list: { + const isAList = is.list(form); + if (isAList && form.value.length === 2 && is.symbol(form.value[0]) && form.value[0].name === "unquote") { + return ctx.evaluate(form.value[1], env); + } + const elements = []; + for (const elem of form.value) { + if (is.list(elem) && elem.value.length === 2 && is.symbol(elem.value[0]) && elem.value[0].name === "unquote-splicing") { + const toSplice = ctx.evaluate(elem.value[1], env); + if (is.list(toSplice) || is.vector(toSplice)) { + elements.push(...toSplice.value); + } else if (is.lazySeq(toSplice) || is.cons(toSplice)) { + elements.push(...toSeq(toSplice)); + } else if (is.nil(toSplice)) {} else { + throw new EvaluationError("Unquote-splicing must evaluate to a seqable", { elem, env }); + } + continue; + } + elements.push(evaluateQuasiquote(elem, env, autoGensyms, ctx)); + } + return isAList ? cljList(elements) : cljVector(elements); + } + case valueKeywords.map: { + const entries = []; + for (const [key, value] of form.entries) { + const evaluatedKey = evaluateQuasiquote(key, env, autoGensyms, ctx); + const evaluatedValue = evaluateQuasiquote(value, env, autoGensyms, ctx); + entries.push([evaluatedKey, evaluatedValue]); + } + return cljMap(entries); + } + case valueKeywords.number: + case valueKeywords.string: + case valueKeywords.boolean: + case valueKeywords.keyword: + case valueKeywords.nil: + return form; + case valueKeywords.symbol: { + if (form.name.endsWith("#")) { + if (!autoGensyms.has(form.name)) { + autoGensyms.set(form.name, makeGensym(form.name.slice(0, -1))); + } + return { kind: "symbol", name: autoGensyms.get(form.name) }; + } + return form; + } + default: + throw new EvaluationError(`Unexpected form: ${form.kind}`, { form, env }); + } +} -;; --------------------------------------------------------------------------- -;; Case conversion -;; --------------------------------------------------------------------------- +// src/core/evaluator/recur-check.ts +function assertRecurInTailPosition(body) { + validateForms(body, true); +} +function isRecurForm(form) { + return is.list(form) && form.value.length >= 1 && is.symbol(form.value[0]) && form.value[0].name === specialFormKeywords.recur; +} +function validateForms(forms, inTail) { + for (let i = 0;i < forms.length; i++) { + validateForm(forms[i], inTail && i === forms.length - 1); + } +} +function validateForm(form, inTail) { + if (!is.list(form)) + return; + if (isRecurForm(form)) { + if (!inTail) { + throw new EvaluationError("Can only recur from tail position", { form }); + } + return; + } + if (form.value.length === 0) + return; + const first = form.value[0]; + if (!is.symbol(first)) { + for (const sub of form.value) + validateForm(sub, false); + return; + } + const name = first.name; + if (name === specialFormKeywords.fn || name === specialFormKeywords.loop || name === specialFormKeywords.quote || name === specialFormKeywords.quasiquote) { + return; + } + if (name === specialFormKeywords.if) { + if (form.value[1]) + validateForm(form.value[1], false); + if (form.value[2]) + validateForm(form.value[2], inTail); + if (form.value[3]) + validateForm(form.value[3], inTail); + return; + } + if (name === specialFormKeywords.do) { + validateForms(form.value.slice(1), inTail); + return; + } + if (name === specialFormKeywords.let) { + const bindings = form.value[1]; + if (is.vector(bindings)) { + for (let i = 1;i < bindings.value.length; i += 2) { + validateForm(bindings.value[i], false); + } + } + validateForms(form.value.slice(2), inTail); + return; + } + for (const sub of form.value.slice(1)) { + validateForm(sub, false); + } +} -(defn upper-case - "Converts string to all upper-case." - [s] - (str-upper-case* s)) +// src/core/evaluator/special-forms.ts +function hasDynamicMeta(meta) { + if (!meta) + return false; + for (const [k, v2] of meta.entries) { + if (is.keyword(k) && k.name === ":dynamic" && is.boolean(v2) && v2.value === true) { + return true; + } + } + return false; +} +var specialFormKeywords = { + quote: "quote", + def: "def", + if: "if", + do: "do", + let: "let", + fn: "fn", + defmacro: "defmacro", + quasiquote: "quasiquote", + ns: "ns", + loop: "loop", + recur: "recur", + defmulti: "defmulti", + defmethod: "defmethod", + try: "try", + var: "var", + binding: "binding", + "set!": "set!", + letfn: "letfn", + delay: "delay", + "lazy-seq": "lazy-seq", + async: "async", + ".": ".", + "js/new": "js/new" +}; +function keywordToDispatchFn(kw) { + return v.nativeFn(`kw:${kw.name}`, (...args) => { + const target = args[0]; + if (!is.map(target)) + return v.nil(); + const entry = target.entries.find(([k]) => is.equal(k, kw)); + return entry ? entry[1] : v.nil(); + }); +} +function evaluateTry(list, env, ctx) { + const forms = list.value.slice(1); + const bodyForms = []; + const catchClauses = []; + let finallyForms = null; + for (let i = 0;i < forms.length; i++) { + const form = forms[i]; + if (is.list(form) && form.value.length > 0 && is.symbol(form.value[0])) { + const head = form.value[0].name; + if (head === "catch") { + if (form.value.length < 3) { + throw new EvaluationError("catch requires a discriminator and a binding symbol", { form, env }); + } + const discriminator = form.value[1]; + const bindingSym = form.value[2]; + if (!is.symbol(bindingSym)) { + throw new EvaluationError("catch binding must be a symbol", { + form, + env + }); + } + catchClauses.push({ + discriminator, + binding: bindingSym.name, + body: form.value.slice(3) + }); + continue; + } + if (head === "finally") { + if (i !== forms.length - 1) { + throw new EvaluationError("finally clause must be the last in try expression", { + form, + env + }); + } + finallyForms = form.value.slice(1); + continue; + } + } + bodyForms.push(form); + } + function matchesDiscriminator(discriminator, thrown) { + let disc; + try { + disc = ctx.evaluate(discriminator, env); + } catch { + return true; + } + if (disc.kind === "symbol") + return true; + if (is.keyword(disc)) { + if (disc.name === ":default") + return true; + if (!is.map(thrown)) + return false; + const typeEntry = thrown.entries.find(([k]) => is.keyword(k) && k.name === ":type"); + if (!typeEntry) + return false; + return is.equal(typeEntry[1], disc); + } + if (is.aFunction(disc)) { + const result2 = ctx.applyFunction(disc, [thrown], env); + return is.truthy(result2); + } + throw new EvaluationError("catch discriminator must be a keyword or a predicate function", { discriminator: disc, env }); + } + let result = v.nil(); + let pendingThrow = null; + try { + result = ctx.evaluateForms(bodyForms, env); + } catch (e) { + if (e instanceof RecurSignal) + throw e; + let thrownValue; + if (e instanceof CljThrownSignal) { + thrownValue = e.value; + } else if (e instanceof EvaluationError) { + thrownValue = v.map([ + [v.keyword(":type"), v.keyword(":error/runtime")], + [v.keyword(":message"), v.string(e.message)] + ]); + } else { + throw e; + } + let handled = false; + for (const clause of catchClauses) { + if (matchesDiscriminator(clause.discriminator, thrownValue)) { + const catchEnv = extend([clause.binding], [thrownValue], env); + result = ctx.evaluateForms(clause.body, catchEnv); + handled = true; + break; + } + } + if (!handled) { + pendingThrow = e; + } + } finally { + if (finallyForms) { + ctx.evaluateForms(finallyForms, env); + } + } + if (pendingThrow !== null) + throw pendingThrow; + return result; +} +function evaluateQuote(list, _env, _ctx) { + return list.value[1]; +} +function evalQuasiquote(list, env, ctx) { + return evaluateQuasiquote(list.value[1], env, new Map, ctx); +} +function buildVarMeta(symMeta, ctx, nameVal) { + const pos = nameVal ? getPos(nameVal) : undefined; + const hasPosInfo = pos && ctx.currentSource; + if (!symMeta && !hasPosInfo) + return; + const posEntries = []; + if (hasPosInfo) { + const { line, col } = getLineCol(ctx.currentSource, pos.start); + const lineOffset = ctx.currentLineOffset ?? 0; + const colOffset = ctx.currentColOffset ?? 0; + posEntries.push([v.keyword(":line"), v.number(line + lineOffset)]); + posEntries.push([ + v.keyword(":column"), + v.number(line === 1 ? col + colOffset : col) + ]); + if (ctx.currentFile) { + posEntries.push([v.keyword(":file"), v.string(ctx.currentFile)]); + } + } + const POS_KEYS = new Set([":line", ":column", ":file"]); + const baseEntries = (symMeta?.entries ?? []).filter(([k]) => !(k.kind === "keyword" && POS_KEYS.has(k.name))); + const allEntries = [...baseEntries, ...posEntries]; + return allEntries.length > 0 ? v.map(allEntries) : undefined; +} +function evaluateDef(list, env, ctx) { + const name = list.value[1]; + if (name.kind !== "symbol") { + throw new EvaluationError("First element of list must be a symbol", { + name, + list, + env + }); + } + if (list.value[2] === undefined) + return v.nil(); + const nsEnv = getNamespaceEnv(env); + const cljNs = nsEnv.ns; + const newValue = ctx.evaluate(list.value[2], env); + const varMeta = buildVarMeta(name.meta, ctx, name); + const existing = cljNs.vars.get(name.name); + if (existing) { + existing.value = newValue; + if (varMeta) { + existing.meta = varMeta; + if (hasDynamicMeta(varMeta)) + existing.dynamic = true; + } + } else { + const newVar = v.var(cljNs.name, name.name, newValue, varMeta); + if (hasDynamicMeta(varMeta)) + newVar.dynamic = true; + cljNs.vars.set(name.name, newVar); + } + return v.nil(); +} +var evaluateNs = (_list, _env, _ctx) => { + return v.nil(); +}; +function evaluateIf(list, env, ctx) { + const condition = ctx.evaluate(list.value[1], env); + if (!is.falsy(condition)) { + return ctx.evaluate(list.value[2], env); + } + if (!list.value[3]) { + return v.nil(); + } + return ctx.evaluate(list.value[3], env); +} +function evaluateDo(list, env, ctx) { + return ctx.evaluateForms(list.value.slice(1), env); +} +function evaluateLet(list, env, ctx) { + const bindings = list.value[1]; + if (!is.vector(bindings)) { + throw new EvaluationError("Bindings must be a vector", { + bindings, + env + }); + } + if (bindings.value.length % 2 !== 0) { + throw new EvaluationError("Bindings must be a balanced pair of keys and values", { bindings, env }); + } + const body = list.value.slice(2); + let localEnv = env; + for (let i = 0;i < bindings.value.length; i += 2) { + const pattern = bindings.value[i]; + const value = ctx.evaluate(bindings.value[i + 1], localEnv); + const pairs = destructureBindings(pattern, value, ctx, localEnv); + localEnv = extend(pairs.map(([n]) => n), pairs.map(([, v2]) => v2), localEnv); + } + return ctx.evaluateForms(body, localEnv); +} +function evaluateFn(list, env, _ctx) { + const rest = list.value.slice(1); + let fnName; + let arityForms = rest; + if (rest[0]?.kind === "symbol") { + fnName = rest[0].name; + arityForms = rest.slice(1); + } + const arities = parseArities(arityForms, env); + for (const arity of arities) { + assertRecurInTailPosition(arity.body); + } + const fn = v.multiArityFunction(arities, env); + if (fnName) { + fn.name = fnName; + const selfEnv = makeEnv(env); + selfEnv.bindings.set(fnName, fn); + fn.env = selfEnv; + } + return fn; +} +function evaluateLetfn(list, env, ctx) { + const fnSpecs = list.value[1]; + if (!is.vector(fnSpecs)) { + throw new EvaluationError("letfn binding specs must be a vector", { + fnSpecs, + env + }); + } + const body = list.value.slice(2); + const sharedEnv = makeEnv(env); + for (const spec of fnSpecs.value) { + if (!is.list(spec) || spec.value.length < 2 || !is.symbol(spec.value[0])) { + throw new EvaluationError("letfn specs must be (name [params] body...) forms", { spec }); + } + const name = spec.value[0].name; + const arityForms = spec.value.slice(1); + const arities = parseArities(arityForms, sharedEnv); + for (const arity of arities) { + assertRecurInTailPosition(arity.body); + } + const fn = v.multiArityFunction(arities, sharedEnv); + fn.name = name; + sharedEnv.bindings.set(name, fn); + } + for (const spec of fnSpecs.value) { + const name = spec.value[0]; + const fn = sharedEnv.bindings.get(name.name); + fn.env = sharedEnv; + } + return ctx.evaluateForms(body, sharedEnv); +} +function mergeDocIntoMeta(base, docstring) { + const docEntry = [ + v.keyword(":doc"), + v.string(docstring) + ]; + const existing = (base?.entries ?? []).filter(([k]) => !(k.kind === "keyword" && k.name === ":doc")); + return { kind: "map", entries: [...existing, docEntry] }; +} +function evaluateDefmacro(list, env, ctx) { + const name = list.value[1]; + if (!is.symbol(name)) { + throw new EvaluationError("First element of defmacro must be a symbol", { + name, + list, + env + }); + } + const rest = list.value.slice(2); + const docstring = rest[0]?.kind === "string" ? rest[0].value : undefined; + const arityForms = docstring ? rest.slice(1) : rest; + const arities = parseArities(arityForms, env); + const macro = v.multiArityMacro(arities, env); + macro.name = name.name; + const varMeta = buildVarMeta(name.meta, ctx, name); + const finalMeta = docstring ? mergeDocIntoMeta(varMeta, docstring) : varMeta; + internVar(name.name, macro, getNamespaceEnv(env), finalMeta); + return v.nil(); +} +function evaluateLoop(list, env, ctx) { + const loopBindings = list.value[1]; + if (!is.vector(loopBindings)) { + throw new EvaluationError("loop bindings must be a vector", { + loopBindings, + env + }); + } + if (loopBindings.value.length % 2 !== 0) { + throw new EvaluationError("loop bindings must be a balanced pair of keys and values", { loopBindings, env }); + } + const loopBody = list.value.slice(2); + assertRecurInTailPosition(loopBody); + const patterns = []; + const initValues = []; + let initEnv = env; + for (let i = 0;i < loopBindings.value.length; i += 2) { + const pattern = loopBindings.value[i]; + const value = ctx.evaluate(loopBindings.value[i + 1], initEnv); + patterns.push(pattern); + initValues.push(value); + const pairs = destructureBindings(pattern, value, ctx, initEnv); + initEnv = extend(pairs.map(([n]) => n), pairs.map(([, v2]) => v2), initEnv); + } + let currentValues = initValues; + while (true) { + let loopEnv = env; + for (let i = 0;i < patterns.length; i++) { + const pairs = destructureBindings(patterns[i], currentValues[i], ctx, loopEnv); + loopEnv = extend(pairs.map(([n]) => n), pairs.map(([, v2]) => v2), loopEnv); + } + try { + return ctx.evaluateForms(loopBody, loopEnv); + } catch (e) { + if (e instanceof RecurSignal) { + if (e.args.length !== patterns.length) { + throw new EvaluationError(`recur expects ${patterns.length} arguments but got ${e.args.length}`, { list, env }); + } + currentValues = e.args; + continue; + } + throw e; + } + } +} +function evaluateRecur(list, env, ctx) { + const args = list.value.slice(1).map((v2) => ctx.evaluate(v2, env)); + throw new RecurSignal(args); +} +function evaluateDefmulti(list, env, ctx) { + const mmName = list.value[1]; + if (!is.symbol(mmName)) { + throw new EvaluationError("defmulti: first argument must be a symbol", { + list, + env + }); + } + const dispatchFnExpr = list.value[2]; + let dispatchFn; + if (is.keyword(dispatchFnExpr)) { + dispatchFn = keywordToDispatchFn(dispatchFnExpr); + } else { + const evaluated = ctx.evaluate(dispatchFnExpr, env); + if (!is.aFunction(evaluated)) { + throw new EvaluationError("defmulti: dispatch-fn must be a function or keyword", { list, env }); + } + dispatchFn = evaluated; + } + const mm = v.multiMethod(mmName.name, dispatchFn, []); + internVar(mmName.name, mm, getNamespaceEnv(env)); + return v.nil(); +} +function evaluateDefmethod(list, env, ctx) { + const mmName = list.value[1]; + if (!is.symbol(mmName)) { + throw new EvaluationError("defmethod: first argument must be a symbol", { + list, + env + }); + } + const dispatchVal = ctx.evaluate(list.value[2], env); + const existing = lookup(mmName.name, env); + if (!is.multiMethod(existing)) { + throw new EvaluationError(`defmethod: ${mmName.name} is not a multimethod`, { list, env }); + } + const arities = parseArities([list.value[3], ...list.value.slice(4)], env); + const methodFn = v.multiArityFunction(arities, env); + const isDefault = is.keyword(dispatchVal) && dispatchVal.name === ":default"; + let updated; + if (isDefault) { + updated = v.multiMethod(existing.name, existing.dispatchFn, existing.methods, methodFn); + } else { + const filtered = existing.methods.filter((m) => !is.equal(m.dispatchVal, dispatchVal)); + updated = v.multiMethod(existing.name, existing.dispatchFn, [ + ...filtered, + { dispatchVal, fn: methodFn } + ]); + } + const eVar = lookupVar(mmName.name, env); + if (eVar) { + eVar.value = updated; + } else { + define(mmName.name, updated, getNamespaceEnv(env)); + } + return v.nil(); +} +function evaluateVar(list, env, ctx) { + const sym = list.value[1]; + if (!is.symbol(sym)) { + throw new EvaluationError("var expects a symbol", { list }); + } + const slashIdx = sym.name.indexOf("/"); + if (slashIdx > 0 && slashIdx < sym.name.length - 1) { + const alias = sym.name.slice(0, slashIdx); + const localName = sym.name.slice(slashIdx + 1); + const nsEnv = getNamespaceEnv(env); + const targetNs = nsEnv.ns?.aliases.get(alias) ?? ctx.resolveNs(alias) ?? null; + if (!targetNs) { + throw new EvaluationError(`No such namespace: ${alias}`, { sym }); + } + const v3 = targetNs.vars.get(localName); + if (!v3) + throw new EvaluationError(`Var ${sym.name} not found`, { sym }); + return v3; + } + const v2 = lookupVar(sym.name, env); + if (!v2) { + throw new EvaluationError(`Unable to resolve var: ${sym.name} in this context`, { sym }); + } + return v2; +} +function evaluateBinding(list, env, ctx) { + const bindings = list.value[1]; + if (!is.vector(bindings)) { + throw new EvaluationError("binding requires a vector of bindings", { + list, + env + }); + } + if (bindings.value.length % 2 !== 0) { + throw new EvaluationError("binding vector must have an even number of forms", { list, env }); + } + const body = list.value.slice(2); + const boundVars = []; + for (let i = 0;i < bindings.value.length; i += 2) { + const sym = bindings.value[i]; + if (!is.symbol(sym)) { + throw new EvaluationError("binding left-hand side must be a symbol", { + sym + }); + } + const newVal = ctx.evaluate(bindings.value[i + 1], env); + const v2 = lookupVar(sym.name, env); + if (!v2) { + throw new EvaluationError(`No var found for symbol '${sym.name}' in binding form`, { sym }); + } + if (!v2.dynamic) { + throw new EvaluationError(`Cannot use binding with non-dynamic var ${v2.ns}/${v2.name}. Mark it dynamic with (def ^:dynamic ${sym.name} ...)`, { sym }); + } + v2.bindingStack ??= []; + v2.bindingStack.push(newVal); + boundVars.push(v2); + } + try { + return ctx.evaluateForms(body, env); + } finally { + for (const v2 of boundVars) { + v2.bindingStack.pop(); + } + } +} +function evaluateSet(list, env, ctx) { + if (list.value.length !== 3) { + throw new EvaluationError(`set! requires exactly 2 arguments, got ${list.value.length - 1}`, { list, env }); + } + const symForm = list.value[1]; + if (!is.symbol(symForm)) { + throw new EvaluationError(`set! first argument must be a symbol, got ${symForm.kind}`, { symForm, env }); + } + const v2 = lookupVar(symForm.name, env); + if (!v2) { + throw new EvaluationError(`Unable to resolve var: ${symForm.name} in this context`, { symForm, env }); + } + if (!v2.dynamic) { + throw new EvaluationError(`Cannot set! non-dynamic var ${v2.ns}/${v2.name}. Mark it with ^:dynamic.`, { symForm, env }); + } + if (!v2.bindingStack || v2.bindingStack.length === 0) { + throw new EvaluationError(`Cannot set! ${v2.ns}/${v2.name} — no active binding. Use set! only inside a (binding [...] ...) form.`, { symForm, env }); + } + const newVal = ctx.evaluate(list.value[2], env); + v2.bindingStack[v2.bindingStack.length - 1] = newVal; + return newVal; +} +function evaluateDelay(list, env, ctx) { + const body = list.value.slice(1); + return v.delay(() => ctx.evaluateForms(body, env)); +} +function evaluateLazySeqForm(list, env, ctx) { + const body = list.value.slice(1); + return v.lazySeq(() => ctx.evaluateForms(body, env)); +} +function evaluateAsyncBlock(list, env, ctx) { + const body = list.value.slice(1); + if (body.length === 0) + return v.pending(Promise.resolve(v.nil())); + const asyncCtx = createAsyncEvalCtx(ctx); + const promise = asyncCtx.evaluateForms(body, env); + return v.pending(promise); +} +var specialFormEvaluatorEntries = { + try: evaluateTry, + quote: evaluateQuote, + quasiquote: evalQuasiquote, + def: evaluateDef, + ns: evaluateNs, + if: evaluateIf, + do: evaluateDo, + let: evaluateLet, + fn: evaluateFn, + defmacro: evaluateDefmacro, + loop: evaluateLoop, + recur: evaluateRecur, + defmulti: evaluateDefmulti, + defmethod: evaluateDefmethod, + var: evaluateVar, + binding: evaluateBinding, + "set!": evaluateSet, + letfn: evaluateLetfn, + delay: evaluateDelay, + "lazy-seq": evaluateLazySeqForm, + async: evaluateAsyncBlock, + ".": evaluateDot, + "js/new": evaluateNew +}; +function evaluateSpecialForm(symbol, list, env, ctx) { + const evalFn = specialFormEvaluatorEntries[symbol]; + if (evalFn) { + return evalFn(list, env, ctx); + } + throw new EvaluationError(`Unknown special form: ${symbol}`, { + symbol, + list, + env + }); +} -(defn lower-case - "Converts string to all lower-case." - [s] - (str-lower-case* s)) +// src/core/assertions.ts +var isNil = (value) => value.kind === "nil"; +var isBoolean = (value) => value.kind === "boolean"; +var isFalsy = (value) => { + if (value.kind === "nil") + return true; + if (isBoolean(value)) + return !value.value; + return false; +}; +var isTruthy = (value) => { + return !isFalsy(value); +}; +var isSpecialForm = (value) => value.kind === "symbol" && (value.name in specialFormKeywords); +var isSymbol = (value) => value.kind === "symbol"; +var isVector = (value) => value.kind === "vector"; +var isList = (value) => value.kind === "list"; +var isFunction = (value) => value.kind === "function"; +var isNativeFunction = (value) => value.kind === "native-function"; +var isMacro = (value) => value.kind === "macro"; +var isMap = (value) => value.kind === "map"; +var isKeyword = (value) => value.kind === "keyword"; +var isAFunction = (value) => isFunction(value) || isNativeFunction(value); +var isJsValue = (value) => value.kind === "js-value"; +var isCallable = (value) => isAFunction(value) || isKeyword(value) || isMap(value) || isJsValue(value) && typeof value.value === "function"; +var isMultiMethod = (value) => value.kind === "multi-method"; +var isAtom = (value) => value.kind === "atom"; +var isReduced = (value) => value.kind === "reduced"; +var isVolatile = (value) => value.kind === "volatile"; +var isRegex = (value) => value.kind === "regex"; +var isVar = (value) => value.kind === "var"; +var isSet = (value) => value.kind === valueKeywords.set; +var isDelay = (value) => value.kind === "delay"; +var isLazySeq = (value) => value.kind === "lazy-seq"; +var isCons = (value) => value.kind === "cons"; +var isNamespace = (value) => value.kind === "namespace"; +var isCollection = (value) => isVector(value) || isMap(value) || isList(value) || isSet(value) || isCons(value); +var isSeqable = (value) => isCollection(value) || value.kind === "string" || isLazySeq(value); +var isCljValue = (value) => { + return typeof value === "object" && value !== null && "kind" in value && value.kind in valueKeywords; +}; +function realizeLazySeqForEquality(ls) { + let current = ls; + while (current.kind === "lazy-seq") { + const lazy = current; + if (lazy.realized) { + current = lazy.value; + } else if (lazy.thunk) { + lazy.value = lazy.thunk(); + lazy.thunk = null; + lazy.realized = true; + current = lazy.value; + } else { + return { kind: "nil", value: null }; + } + } + return current; +} +var equalityHandlers = { + [valueKeywords.number]: (a, b) => a.value === b.value, + [valueKeywords.string]: (a, b) => a.value === b.value, + [valueKeywords.boolean]: (a, b) => a.value === b.value, + [valueKeywords.nil]: () => true, + [valueKeywords.symbol]: (a, b) => a.name === b.name, + [valueKeywords.keyword]: (a, b) => a.name === b.name, + [valueKeywords.vector]: (a, b) => { + if (a.value.length !== b.value.length) + return false; + return a.value.every((value, index) => isEqual(value, b.value[index])); + }, + [valueKeywords.map]: (a, b) => { + if (a.entries.length !== b.entries.length) + return false; + const uniqueKeys = new Set([ + ...a.entries.map(([key]) => key), + ...b.entries.map(([key]) => key) + ]); + for (const key of uniqueKeys) { + const aEntry = a.entries.find(([k]) => isEqual(k, key)); + if (!aEntry) + return false; + const bEntry = b.entries.find(([k]) => isEqual(k, key)); + if (!bEntry) + return false; + if (!isEqual(aEntry[1], bEntry[1])) + return false; + } + return true; + }, + [valueKeywords.list]: (a, b) => { + if (a.value.length !== b.value.length) + return false; + return a.value.every((value, index) => isEqual(value, b.value[index])); + }, + [valueKeywords.atom]: (a, b) => a === b, + [valueKeywords.reduced]: (a, b) => isEqual(a.value, b.value), + [valueKeywords.volatile]: (a, b) => a === b, + [valueKeywords.regex]: (a, b) => a === b, + [valueKeywords.var]: (a, b) => a === b, + [valueKeywords.set]: (a, b) => { + if (a.values.length !== b.values.length) + return false; + return a.values.every((av) => b.values.some((bv) => isEqual(av, bv))); + }, + [valueKeywords.delay]: (a, b) => a === b, + [valueKeywords.lazySeq]: (a, b) => { + const aVal = realizeLazySeqForEquality(a); + const bVal = realizeLazySeqForEquality(b); + return isEqual(aVal, bVal); + }, + [valueKeywords.cons]: (a, b) => isEqual(a.head, b.head) && isEqual(a.tail, b.tail), + [valueKeywords.namespace]: (a, b) => a === b +}; +var isString = (value) => value.kind === "string"; +var isEqual = (a, b) => { + if (a.kind !== b.kind) + return false; + const handler = equalityHandlers[a.kind]; + if (!handler) + return false; + return handler(a, b); +}; +var isNumber = (value) => value.kind === "number"; +var is = { + nil: isNil, + number: isNumber, + string: isString, + boolean: isBoolean, + falsy: isFalsy, + truthy: isTruthy, + specialForm: isSpecialForm, + symbol: isSymbol, + vector: isVector, + list: isList, + function: isFunction, + nativeFunction: isNativeFunction, + macro: isMacro, + map: isMap, + keyword: isKeyword, + aFunction: isAFunction, + callable: isCallable, + multiMethod: isMultiMethod, + atom: isAtom, + reduced: isReduced, + volatile: isVolatile, + regex: isRegex, + var: isVar, + set: isSet, + delay: isDelay, + lazySeq: isLazySeq, + cons: isCons, + namespace: isNamespace, + collection: isCollection, + seqable: isSeqable, + cljValue: isCljValue, + equal: isEqual, + jsValue: isJsValue +}; -(defn capitalize - "Converts first character of the string to upper-case, all other - characters to lower-case." - [s] - (if (< (count s) 2) - (upper-case s) - (str (upper-case (subs s 0 1)) (lower-case (subs s 1))))) +// src/core/evaluator/apply.ts +function applyFunctionWithContext(fn, args, ctx, callEnv) { + if (fn.kind === "native-function") { + if (fn.fnWithContext) { + return fn.fnWithContext(ctx, callEnv, ...args); + } + return fn.fn(...args); + } + if (fn.kind === "function") { + const arity = resolveArity(fn.arities, args.length); + let currentArgs = args; + while (true) { + const localEnv = bindParams(arity.params, arity.restParam, currentArgs, fn.env, ctx, callEnv); + try { + return ctx.evaluateForms(arity.body, localEnv); + } catch (e) { + if (e instanceof RecurSignal) { + currentArgs = e.args; + continue; + } + throw e; + } + } + } + throw new EvaluationError(`${fn.kind} is not a callable function`, { + fn, + args + }); +} +function applyMacroWithContext(macro, rawArgs, ctx) { + const arity = resolveArity(macro.arities, rawArgs.length); + const localEnv = bindParams(arity.params, arity.restParam, rawArgs, macro.env, ctx, macro.env); + return ctx.evaluateForms(arity.body, localEnv); +} +function applyCallableWithContext(fn, args, ctx, callEnv) { + if (is.aFunction(fn)) { + return applyFunctionWithContext(fn, args, ctx, callEnv); + } + if (is.jsValue(fn)) { + if (typeof fn.value !== "function") { + throw new EvaluationError(`js-value is not callable: ${typeof fn.value}`, { fn, args }); + } + const jsArgs = args.map((a) => cljToJs(a, ctx, callEnv)); + const rawResult = fn.value(...jsArgs); + return jsToClj(rawResult); + } + if (is.keyword(fn)) { + const target = args[0]; + const defaultVal = args.length > 1 ? args[1] : cljNil(); + if (is.map(target)) { + const entry = target.entries.find(([k]) => is.equal(k, fn)); + return entry ? entry[1] : defaultVal; + } + return defaultVal; + } + if (is.map(fn)) { + if (args.length === 0) { + throw new EvaluationError("Map used as function requires at least one argument", { fn, args }); + } + const key = args[0]; + const defaultVal = args.length > 1 ? args[1] : cljNil(); + const entry = fn.entries.find(([k]) => is.equal(k, key)); + return entry ? entry[1] : defaultVal; + } + throw new EvaluationError(`${printString(fn)} is not a callable value`, { + fn, + args + }); +} -;; --------------------------------------------------------------------------- -;; Trimming -;; --------------------------------------------------------------------------- +// src/core/evaluator/expand.ts +function macroExpandAllWithContext(form, env, ctx) { + if (is.vector(form)) { + const expanded2 = form.value.map((sub) => macroExpandAllWithContext(sub, env, ctx)); + return expanded2.every((e, i) => e === form.value[i]) ? form : cljVector(expanded2); + } + if (is.map(form)) { + const expanded2 = form.entries.map(([k, v2]) => [ + macroExpandAllWithContext(k, env, ctx), + macroExpandAllWithContext(v2, env, ctx) + ]); + return expanded2.every(([k, v2], i) => k === form.entries[i][0] && v2 === form.entries[i][1]) ? form : cljMap(expanded2); + } + if (!is.list(form)) + return form; + if (form.value.length === 0) + return form; + const first = form.value[0]; + if (!is.symbol(first)) { + const expanded2 = form.value.map((sub) => macroExpandAllWithContext(sub, env, ctx)); + return expanded2.every((e, i) => e === form.value[i]) ? form : cljList(expanded2); + } + const name = first.name; + if (name === "quote" || name === "quasiquote") + return form; + let macroOrUnknown; + const slashIdx = name.indexOf("/"); + if (slashIdx > 0 && slashIdx < name.length - 1) { + const nsPrefix = name.slice(0, slashIdx); + const localName = name.slice(slashIdx + 1); + const nsEnv = getNamespaceEnv(env); + const targetNs = nsEnv.ns?.aliases.get(nsPrefix) ?? ctx.resolveNs(nsPrefix) ?? null; + if (targetNs) { + const v2 = targetNs.vars.get(localName); + macroOrUnknown = v2 !== undefined ? derefValue(v2) : undefined; + } + } else { + macroOrUnknown = tryLookup(name, env); + } + if (macroOrUnknown !== undefined && is.macro(macroOrUnknown)) { + const expanded2 = ctx.applyMacro(macroOrUnknown, form.value.slice(1)); + return macroExpandAllWithContext(expanded2, env, ctx); + } + const expanded = form.value.map((sub) => macroExpandAllWithContext(sub, env, ctx)); + return expanded.every((e, i) => e === form.value[i]) ? form : cljList(expanded); +} -(defn trim - "Removes whitespace from both ends of string." - [s] - (str-trim* s)) +// src/core/evaluator/collections.ts +function evaluateVector(vector, env, ctx) { + const evaluated = vector.value.map((v2) => ctx.evaluate(v2, env)); + if (vector.meta) + return { kind: "vector", value: evaluated, meta: vector.meta }; + return v.vector(evaluated); +} +function evaluateSet2(set, env, ctx) { + const evaluated = []; + for (const v2 of set.values) { + const ev = ctx.evaluate(v2, env); + if (!evaluated.some((existing) => is.equal(existing, ev))) { + evaluated.push(ev); + } + } + return v.set(evaluated); +} +function evaluateMap(map, env, ctx) { + let entries = []; + for (const [key, value] of map.entries) { + const evaluatedKey = ctx.evaluate(key, env); + const evaluatedValue = ctx.evaluate(value, env); + entries.push([evaluatedKey, evaluatedValue]); + } + if (map.meta) + return { kind: "map", entries, meta: map.meta }; + return v.map(entries); +} -(defn triml - "Removes whitespace from the left side of string." - [s] - (str-triml* s)) +// src/core/evaluator/dispatch.ts +function dispatchMultiMethod(mm, args, ctx, env) { + const dispatchVal = ctx.applyFunction(mm.dispatchFn, args, env); + const method = mm.methods.find(({ dispatchVal: dv }) => is.equal(dv, dispatchVal)); + if (method) + return ctx.applyFunction(method.fn, args, env); + if (mm.defaultMethod) + return ctx.applyFunction(mm.defaultMethod, args, env); + throw new EvaluationError(`No method in multimethod '${mm.name}' for dispatch value ${printString(dispatchVal)}`, { mm, dispatchVal }); +} +function evaluateList(list, env, ctx) { + if (list.value.length === 0) { + return list; + } + const first = list.value[0]; + if (is.specialForm(first)) { + return evaluateSpecialForm(first.name, list, env, ctx); + } + const evaledFirst = ctx.evaluate(first, env); + if (is.multiMethod(evaledFirst)) { + const args2 = list.value.slice(1).map((v2) => ctx.evaluate(v2, env)); + return dispatchMultiMethod(evaledFirst, args2, ctx, env); + } + if (!is.callable(evaledFirst)) { + const name = is.symbol(first) ? first.name : printString(first); + throw new EvaluationError(`${name} is not callable`, { list, env }); + } + const args = list.value.slice(1).map((v2) => ctx.evaluate(v2, env)); + try { + return ctx.applyCallable(evaledFirst, args, env); + } catch (e) { + if (e instanceof EvaluationError && e.data?.argIndex !== undefined && !e.pos) { + const argForm = list.value[e.data.argIndex + 1]; + if (argForm) { + const pos = getPos(argForm); + if (pos) + e.pos = pos; + } + } + throw e; + } +} -(defn trimr - "Removes whitespace from the right side of string." - [s] - (str-trimr* s)) +// src/core/evaluator/evaluate.ts +function evaluateWithContext(expr, env, ctx) { + try { + switch (expr.kind) { + case valueKeywords.number: + case valueKeywords.string: + case valueKeywords.keyword: + case valueKeywords.nil: + case valueKeywords.function: + case valueKeywords.multiMethod: + case valueKeywords.boolean: + case valueKeywords.regex: + case valueKeywords.delay: + case valueKeywords.lazySeq: + case valueKeywords.cons: + case valueKeywords.namespace: + return expr; + case valueKeywords.symbol: { + const slashIdx = expr.name.indexOf("/"); + if (slashIdx > 0 && slashIdx < expr.name.length - 1) { + const alias = expr.name.slice(0, slashIdx); + const sym = expr.name.slice(slashIdx + 1); + const nsEnv = getNamespaceEnv(env); + const targetNs = nsEnv.ns?.aliases.get(alias) ?? ctx.resolveNs(alias) ?? null; + if (!targetNs) { + throw new EvaluationError(`No such namespace or alias: ${alias}`, { + symbol: expr.name, + env + }); + } + const v2 = targetNs.vars.get(sym); + if (v2 === undefined) { + throw new EvaluationError(`Symbol ${expr.name} not found`, { + symbol: expr.name, + env + }); + } + return derefValue(v2); + } + return lookup(expr.name, env); + } + case valueKeywords.vector: + return evaluateVector(expr, env, ctx); + case valueKeywords.map: + return evaluateMap(expr, env, ctx); + case valueKeywords.set: + return evaluateSet2(expr, env, ctx); + case valueKeywords.list: + return evaluateList(expr, env, ctx); + default: + throw new EvaluationError("Unexpected value", { expr, env }); + } + } catch (e) { + if (e instanceof EvaluationError && !e.pos) { + const p = getPos(expr); + if (p) + e.pos = p; + } + throw e; + } +} +function evaluateFormsWithContext(forms, env, ctx) { + let result = v.nil(); + for (const form of forms) { + result = ctx.evaluate(form, env); + } + return result; +} -(defn trim-newline - "Removes all trailing newline \\\\n or return \\\\r characters from string. - Similar to Perl's chomp." - [s] - (replace s #"[\\r\\n]+$" "")) +// src/core/evaluator/index.ts +function createEvaluationContext() { + const ctx = { + evaluate: (expr, env) => evaluateWithContext(expr, env, ctx), + evaluateForms: (forms, env) => evaluateFormsWithContext(forms, env, ctx), + applyFunction: (fn, args, callEnv) => applyFunctionWithContext(fn, args, ctx, callEnv), + applyCallable: (fn, args, callEnv) => applyCallableWithContext(fn, args, ctx, callEnv), + applyMacro: (macro, rawArgs) => applyMacroWithContext(macro, rawArgs, ctx), + expandAll: (form, env) => macroExpandAllWithContext(form, env, ctx), + resolveNs: (_name) => null, + io: { + stdout: (text) => console.log(text), + stderr: (text) => console.error(text) + } + }; + return ctx; +} -;; --------------------------------------------------------------------------- -;; Predicates -;; --------------------------------------------------------------------------- +// src/core/conversions.ts +class ConversionError extends Error { + context; + constructor(message, context) { + super(message); + this.name = "ConversionError"; + this.context = context; + } +} +var richKeyKinds = new Set(["list", "vector", "map"]); +var _throwingApplier = { + applyFunction: () => { + throw new ConversionError("Cannot convert a CLJ function to JS in this context — use session.cljToJs() instead."); + } +}; +function cljToJs2(value, applier) { + switch (value.kind) { + case "number": + return value.value; + case "string": + return value.value; + case "boolean": + return value.value; + case "nil": + return null; + case "keyword": + return value.name.startsWith(":") ? value.name.slice(1) : value.name; + case "symbol": + return value.name; + case "list": + case "vector": + return value.value.map((item) => cljToJs2(item, applier)); + case "map": { + const obj = {}; + for (const [k, val] of value.entries) { + if (richKeyKinds.has(k.kind)) { + throw new ConversionError(`Rich key types (${k.kind}) are not supported in JS object conversion. Restructure your map to use string, keyword, or number keys.`, { key: k, value: val }); + } + const jsKey = String(cljToJs2(k, applier)); + obj[jsKey] = cljToJs2(val, applier); + } + return obj; + } + case "function": + case "native-function": { + const fn = value; + return (...jsArgs) => { + const cljArgs = jsArgs.map((a) => jsToClj2(a)); + const result = applier.applyFunction(fn, cljArgs); + return cljToJs2(result, applier); + }; + } + case "macro": + throw new ConversionError("Macros cannot be exported to JavaScript. Macros are compile-time constructs.", { macro: value }); + } +} +function jsToClj2(value, opts = {}) { + const { keywordizeKeys = true } = opts; + if (value === null) + return v.nil(); + if (value === undefined) + return v.jsValue(undefined); + if (isCljValue(value)) + return value; + switch (typeof value) { + case "number": + return v.number(value); + case "string": + return v.string(value); + case "boolean": + return v.boolean(value); + case "function": { + const jsFn = value; + return v.nativeFn("js-fn", (...cljArgs) => { + const jsArgs = cljArgs.map((a) => cljToJs2(a, _throwingApplier)); + const result = jsFn(...jsArgs); + return jsToClj2(result, opts); + }); + } + case "object": { + if (Array.isArray(value)) { + return v.vector(value.map((item) => jsToClj2(item, opts))); + } + const entries = Object.entries(value).map(([k, val]) => [ + keywordizeKeys ? v.keyword(`:${k}`) : v.string(k), + jsToClj2(val, opts) + ]); + return v.map(entries); + } + default: + throw new ConversionError(`Cannot convert JS value of type ${typeof value} to CljValue`, { value }); + } +} -(defn blank? - "True if s is nil, empty, or contains only whitespace." - [s] - (or (nil? s) (not (nil? (re-matches #"\\s*" s))))) +// src/core/scanners.ts +var createCursor = (line, col, offset) => ({ + line, + col, + offset +}); +var makeScannerPrimitives = (input, cursor) => { + return { + peek: (ahead = 0) => { + const idx = cursor.offset + ahead; + if (idx >= input.length) + return null; + return input[idx]; + }, + isAtEnd: () => { + return cursor.offset >= input.length; + }, + position: () => { + return { + offset: cursor.offset, + line: cursor.line, + col: cursor.col + }; + } + }; +}; +function makeCharScanner(input) { + const cursor = createCursor(0, 0, 0); + const api = { + ...makeScannerPrimitives(input, cursor), + advance: () => { + if (cursor.offset >= input.length) + return null; + const ch = input[cursor.offset]; + cursor.offset++; + if (ch === ` +`) { + cursor.line++; + cursor.col = 0; + } else { + cursor.col++; + } + return ch; + }, + consumeWhile(predicate) { + const buffer = []; + while (!api.isAtEnd() && predicate(api.peek())) { + buffer.push(api.advance()); + } + return buffer.join(""); + } + }; + return api; +} +function makeTokenScanner(input) { + const cursor = createCursor(0, 0, 0); + const api = { + ...makeScannerPrimitives(input, cursor), + advance: () => { + if (cursor.offset >= input.length) + return null; + const token = input[cursor.offset]; + cursor.offset++; + cursor.col = token.end.col; + cursor.line = token.end.line; + return token; + }, + consumeWhile(predicate) { + const buffer = []; + while (!api.isAtEnd() && predicate(api.peek())) { + buffer.push(api.advance()); + } + return buffer; + }, + consumeN(n) { + for (let i = 0;i < n; i++) { + api.advance(); + } + } + }; + return api; +} -(defn starts-with? - "True if s starts with substr." - [s substr] - (str-starts-with* s substr)) +// src/core/tokenizer.ts +var isNewline = (char) => char === ` +`; +var isWhitespace = (char) => [" ", ",", ` +`, "\r", "\t"].includes(char); +var isComment = (char) => char === ";"; +var isLParen = (char) => char === "("; +var isRParen = (char) => char === ")"; +var isLBracket = (char) => char === "["; +var isRBracket = (char) => char === "]"; +var isLBrace = (char) => char === "{"; +var isRBrace = (char) => char === "}"; +var isDoubleQuote = (char) => char === '"'; +var isSingleQuote = (char) => char === "'"; +var isBacktick = (char) => char === "`"; +var isTilde = (char) => char === "~"; +var isAt = (char) => char === "@"; +var isNumber2 = (char) => { + const parsed = parseInt(char); + if (isNaN(parsed)) { + return false; + } + return parsed >= 0 && parsed <= 9; +}; +var isDot = (char) => char === "."; +var isKeywordStart = (char) => char === ":"; +var isHash = (char) => char === "#"; +var isCaret = (char) => char === "^"; +var isDelimiter = (char) => isLParen(char) || isRParen(char) || isLBracket(char) || isRBracket(char) || isLBrace(char) || isRBrace(char) || isBacktick(char) || isSingleQuote(char) || isAt(char) || isCaret(char); +var parseWhitespace = (ctx) => { + const scanner = ctx.scanner; + const start = scanner.position(); + scanner.consumeWhile(isWhitespace); + return { + kind: tokenKeywords.Whitespace, + start, + end: scanner.position() + }; +}; +var parseComment = (ctx) => { + const scanner = ctx.scanner; + const start = scanner.position(); + scanner.advance(); + const value = scanner.consumeWhile((char) => !isNewline(char)); + if (!scanner.isAtEnd() && scanner.peek() === ` +`) { + scanner.advance(); + } + return { + kind: tokenKeywords.Comment, + value, + start, + end: scanner.position() + }; +}; +var parseString = (ctx) => { + const scanner = ctx.scanner; + const start = scanner.position(); + scanner.advance(); + const buffer = []; + let foundClosingQuote = false; + while (!scanner.isAtEnd()) { + const ch = scanner.peek(); + if (ch === "\\") { + scanner.advance(); + const nextChar = scanner.peek(); + switch (nextChar) { + case '"': + buffer.push('"'); + break; + case "\\": + buffer.push("\\"); + break; + case "n": + buffer.push(` +`); + break; + case "r": + buffer.push("\r"); + break; + case "t": + buffer.push("\t"); + break; + default: + buffer.push(nextChar); + } + if (!scanner.isAtEnd()) { + scanner.advance(); + } + continue; + } + if (ch === '"') { + scanner.advance(); + foundClosingQuote = true; + break; + } + buffer.push(scanner.advance()); + } + if (!foundClosingQuote) { + throw new TokenizerError(`Unterminated string detected at ${start.offset}`, scanner.position()); + } + return { + kind: tokenKeywords.String, + value: buffer.join(""), + start, + end: scanner.position() + }; +}; +var parseKeyword = (ctx) => { + const scanner = ctx.scanner; + const start = scanner.position(); + const value = scanner.consumeWhile((char) => isKeywordStart(char) || !isWhitespace(char) && !isDelimiter(char) && !isComment(char)); + return { + kind: tokenKeywords.Keyword, + value, + start, + end: scanner.position() + }; +}; +function isNumberToken(char, ctx) { + const scanner = ctx.scanner; + const next = scanner.peek(1); + return isNumber2(char) || char === "-" && next !== null && isNumber2(next); +} +var parseNumber = (ctx) => { + const scanner = ctx.scanner; + const start = scanner.position(); + let value = ""; + if (scanner.peek() === "-") { + value += scanner.advance(); + } + value += scanner.consumeWhile(isNumber2); + if (!scanner.isAtEnd() && scanner.peek() === "." && scanner.peek(1) !== null && isNumber2(scanner.peek(1))) { + value += scanner.advance(); + value += scanner.consumeWhile(isNumber2); + } + if (!scanner.isAtEnd() && (scanner.peek() === "e" || scanner.peek() === "E")) { + value += scanner.advance(); + if (!scanner.isAtEnd() && (scanner.peek() === "+" || scanner.peek() === "-")) { + value += scanner.advance(); + } + const exponentDigits = scanner.consumeWhile(isNumber2); + if (exponentDigits.length === 0) { + throw new TokenizerError(`Invalid number format at line ${start.line} column ${start.col}: "${value}"`, { start, end: scanner.position() }); + } + value += exponentDigits; + } + if (!scanner.isAtEnd() && isDot(scanner.peek())) { + throw new TokenizerError(`Invalid number format at line ${start.line} column ${start.col}: "${value}${scanner.consumeWhile((ch) => !isWhitespace(ch) && !isDelimiter(ch))}"`, { start, end: scanner.position() }); + } + return { + kind: tokenKeywords.Number, + value: Number(value), + start, + end: scanner.position() + }; +}; +var parseSymbol = (ctx) => { + const scanner = ctx.scanner; + const start = scanner.position(); + const value = scanner.consumeWhile((char) => !isWhitespace(char) && !isDelimiter(char) && !isComment(char)); + return { + kind: tokenKeywords.Symbol, + value, + start, + end: scanner.position() + }; +}; +var parseDerefToken = (ctx) => { + const scanner = ctx.scanner; + const start = scanner.position(); + scanner.advance(); + return { kind: "Deref", start, end: scanner.position() }; +}; +var parseMetaToken = (ctx) => { + const scanner = ctx.scanner; + const start = scanner.position(); + scanner.advance(); + return { kind: "Meta", start, end: scanner.position() }; +}; +var parseRegexLiteral = (ctx, start) => { + const scanner = ctx.scanner; + scanner.advance(); + const buffer = []; + let foundClosingQuote = false; + while (!scanner.isAtEnd()) { + const ch = scanner.peek(); + if (ch === "\\") { + scanner.advance(); + const next = scanner.peek(); + if (next === null) { + throw new TokenizerError(`Unterminated regex literal at ${start.offset}`, scanner.position()); + } + if (next === '"') { + buffer.push('"'); + } else { + buffer.push("\\"); + buffer.push(next); + } + scanner.advance(); + continue; + } + if (ch === '"') { + scanner.advance(); + foundClosingQuote = true; + break; + } + buffer.push(scanner.advance()); + } + if (!foundClosingQuote) { + throw new TokenizerError(`Unterminated regex literal at ${start.offset}`, scanner.position()); + } + return { + kind: tokenKeywords.Regex, + value: buffer.join(""), + start, + end: scanner.position() + }; +}; +function parseDispatch(ctx) { + const scanner = ctx.scanner; + const start = scanner.position(); + scanner.advance(); + const next = scanner.peek(); + if (next === "(") { + scanner.advance(); + return { kind: tokenKeywords.AnonFnStart, start, end: scanner.position() }; + } + if (next === '"') { + return parseRegexLiteral(ctx, start); + } + if (next === "'") { + scanner.advance(); + return { kind: tokenKeywords.VarQuote, start, end: scanner.position() }; + } + if (next === "{") { + scanner.advance(); + return { kind: tokenKeywords.SetStart, start, end: scanner.position() }; + } + throw new TokenizerError(`Unknown dispatch character: #${next ?? "EOF"}`, start); +} +function parseCharToken(kind, value) { + return (ctx) => { + const scanner = ctx.scanner; + const start = scanner.position(); + scanner.advance(); + return { + kind, + value, + start, + end: scanner.position() + }; + }; +} +function parseTilde(ctx) { + const scanner = ctx.scanner; + const start = scanner.position(); + scanner.advance(); + const nextChar = scanner.peek(); + if (!nextChar) { + throw new TokenizerError(`Unexpected end of input while parsing unquote at ${start.offset}`, start); + } + if (isAt(nextChar)) { + scanner.advance(); + return { + kind: tokenKeywords.UnquoteSplicing, + value: tokenSymbols.UnquoteSplicing, + start, + end: scanner.position() + }; + } + return { + kind: tokenKeywords.Unquote, + value: tokenSymbols.Unquote, + start, + end: scanner.position() + }; +} +var tokenParseEntries = [ + [isWhitespace, parseWhitespace], + [isComment, parseComment], + [isLParen, parseCharToken(tokenKeywords.LParen, tokenSymbols.LParen)], + [isRParen, parseCharToken(tokenKeywords.RParen, tokenSymbols.RParen)], + [isLBracket, parseCharToken(tokenKeywords.LBracket, tokenSymbols.LBracket)], + [isRBracket, parseCharToken(tokenKeywords.RBracket, tokenSymbols.RBracket)], + [isLBrace, parseCharToken(tokenKeywords.LBrace, tokenSymbols.LBrace)], + [isRBrace, parseCharToken(tokenKeywords.RBrace, tokenSymbols.RBrace)], + [isDoubleQuote, parseString], + [isKeywordStart, parseKeyword], + [isNumberToken, parseNumber], + [isSingleQuote, parseCharToken(tokenKeywords.Quote, tokenSymbols.Quote)], + [ + isBacktick, + parseCharToken(tokenKeywords.Quasiquote, tokenSymbols.Quasiquote) + ], + [isTilde, parseTilde], + [isAt, parseDerefToken], + [isCaret, parseMetaToken], + [isHash, parseDispatch] +]; +function parseNextToken(ctx) { + const scanner = ctx.scanner; + const char = scanner.peek(); + const entry = tokenParseEntries.find(([check]) => check(char, ctx)); + if (entry) { + const [, parse] = entry; + return parse(ctx); + } + return parseSymbol(ctx); +} +function parseAllTokens(ctx) { + const tokens = []; + let error = undefined; + try { + while (!ctx.scanner.isAtEnd()) { + const result = parseNextToken(ctx); + if (!result) { + break; + } + if (result.kind === tokenKeywords.Whitespace) { + continue; + } + tokens.push(result); + } + } catch (e) { + error = e; + } + const parsed = { + tokens, + scanner: ctx.scanner, + error + }; + return parsed; +} +function getTokenValue(token) { + if ("value" in token) { + return token.value; + } + return ""; +} +function tokenize(input) { + const inputLength = input.length; + const scanner = makeCharScanner(input); + const tokenizationContext = { + scanner + }; + const tokensResult = parseAllTokens(tokenizationContext); + if (tokensResult.error) { + throw tokensResult.error; + } + if (tokensResult.scanner.position().offset !== inputLength) { + throw new TokenizerError(`Unexpected end of input, expected ${inputLength} characters, got ${tokensResult.scanner.position().offset}`, tokensResult.scanner.position()); + } + return tokensResult.tokens; +} -(defn ends-with? - "True if s ends with substr." - [s substr] - (str-ends-with* s substr)) +// src/core/reader.ts +function readAtom(ctx) { + const scanner = ctx.scanner; + const token = scanner.peek(); + if (!token) { + throw new ReaderError("Unexpected end of input", scanner.position()); + } + switch (token.kind) { + case tokenKeywords.Symbol: + return readSymbol(scanner); + case tokenKeywords.String: { + scanner.advance(); + const val = { kind: "string", value: token.value }; + setPos(val, { start: token.start.offset, end: token.end.offset }); + return val; + } + case tokenKeywords.Number: { + scanner.advance(); + const val = { kind: "number", value: token.value }; + setPos(val, { start: token.start.offset, end: token.end.offset }); + return val; + } + case tokenKeywords.Keyword: { + scanner.advance(); + const kwName = token.value; + let val; + if (kwName.startsWith("::")) { + const rest = kwName.slice(2); + if (rest.includes("/")) { + const slashIdx = rest.indexOf("/"); + const alias = rest.slice(0, slashIdx); + const localName = rest.slice(slashIdx + 1); + const fullNs = ctx.aliases.get(alias); + if (!fullNs) { + throw new ReaderError(`No namespace alias '${alias}' found for ::${alias}/${localName}`, token, { start: token.start.offset, end: token.end.offset }); + } + val = { kind: "keyword", name: `:${fullNs}/${localName}` }; + } else { + val = { kind: "keyword", name: `:${ctx.namespace}/${rest}` }; + } + } else { + val = { kind: "keyword", name: kwName }; + } + setPos(val, { start: token.start.offset, end: token.end.offset }); + return val; + } + } + throw new ReaderError(`Unexpected token: ${token.kind}`, token, { + start: token.start.offset, + end: token.end.offset + }); +} +var readQuote = (ctx) => { + const scanner = ctx.scanner; + const token = scanner.peek(); + if (!token) { + throw new ReaderError("Unexpected end of input while parsing quote", scanner.position()); + } + scanner.advance(); + const value = readForm(ctx); + if (!value) { + throw new ReaderError(`Unexpected token: ${getTokenValue(token)}`, token); + } + return { kind: valueKeywords.list, value: [v.symbol("quote"), value] }; +}; +var readQuasiquote = (ctx) => { + const scanner = ctx.scanner; + const token = scanner.peek(); + if (!token) { + throw new ReaderError("Unexpected end of input while parsing quasiquote", scanner.position()); + } + scanner.advance(); + const value = readForm(ctx); + if (!value) { + throw new ReaderError(`Unexpected token: ${getTokenValue(token)}`, token); + } + return { kind: valueKeywords.list, value: [v.symbol("quasiquote"), value] }; +}; +var readUnquote = (ctx) => { + const scanner = ctx.scanner; + const token = scanner.peek(); + if (!token) { + throw new ReaderError("Unexpected end of input while parsing unquote", scanner.position()); + } + scanner.advance(); + const value = readForm(ctx); + if (!value) { + throw new ReaderError(`Unexpected token: ${getTokenValue(token)}`, token); + } + return { kind: valueKeywords.list, value: [v.symbol("unquote"), value] }; +}; +var readMeta = (ctx) => { + const scanner = ctx.scanner; + const token = scanner.peek(); + if (!token) { + throw new ReaderError("Unexpected end of input while parsing metadata", scanner.position()); + } + scanner.advance(); + const metaForm = readForm(ctx); + const target = readForm(ctx); + let metaEntries; + if (metaForm.kind === "keyword") { + metaEntries = [[metaForm, v.boolean(true)]]; + } else if (metaForm.kind === "map") { + metaEntries = metaForm.entries; + } else if (metaForm.kind === "symbol") { + metaEntries = [[v.keyword(":tag"), metaForm]]; + } else { + throw new ReaderError("Metadata must be a keyword, map, or symbol", token); + } + if (target.kind === "symbol" || target.kind === "list" || target.kind === "vector" || target.kind === "map") { + const existingEntries = target.meta ? target.meta.entries : []; + const result = { + ...target, + meta: v.map([...existingEntries, ...metaEntries]) + }; + const pos = getPos(target); + if (pos) + setPos(result, pos); + return result; + } + return target; +}; +var readVarQuote = (ctx) => { + const scanner = ctx.scanner; + const token = scanner.peek(); + if (!token) { + throw new ReaderError("Unexpected end of input while parsing var quote", scanner.position()); + } + scanner.advance(); + const value = readForm(ctx); + return v.list([v.symbol("var"), value]); +}; +var readDeref = (ctx) => { + const scanner = ctx.scanner; + const token = scanner.peek(); + if (!token) { + throw new ReaderError("Unexpected end of input while parsing deref", scanner.position()); + } + scanner.advance(); + const value = readForm(ctx); + if (!value) { + throw new ReaderError(`Unexpected token: ${getTokenValue(token)}`, token); + } + return { kind: valueKeywords.list, value: [v.symbol("deref"), value] }; +}; +var readUnquoteSplicing = (ctx) => { + const scanner = ctx.scanner; + const token = scanner.peek(); + if (!token) { + throw new ReaderError("Unexpected end of input while parsing unquote splicing", scanner.position()); + } + scanner.advance(); + const value = readForm(ctx); + if (!value) { + throw new ReaderError(`Unexpected token: ${getTokenValue(token)}`, token); + } + return { + kind: valueKeywords.list, + value: [v.symbol("unquote-splicing"), value] + }; +}; +var isClosingToken = (token) => { + return [ + tokenKeywords.RParen, + tokenKeywords.RBracket, + tokenKeywords.RBrace + ].includes(token.kind); +}; +var collectionReader = (valueType, closeToken) => { + return function(ctx) { + const scanner = ctx.scanner; + const startToken = scanner.peek(); + if (!startToken) { + throw new ReaderError("Unexpected end of input while parsing collection", scanner.position()); + } + scanner.advance(); + const values = []; + let pairMatched = false; + let closingEnd; + while (!scanner.isAtEnd()) { + const token = scanner.peek(); + if (!token) { + break; + } + if (isClosingToken(token) && token.kind !== closeToken) { + throw new ReaderError(`Expected '${closeToken}' to close ${valueType} started at line ${startToken.start.line} column ${startToken.start.col}, but got '${getTokenValue(token)}' at line ${token.start.line} column ${token.start.col}`, token, { start: token.start.offset, end: token.end.offset }); + } + if (token.kind === closeToken) { + closingEnd = token.end.offset; + scanner.advance(); + pairMatched = true; + break; + } + const value = readForm(ctx); + values.push(value); + } + if (!pairMatched) { + throw new ReaderError(`Unmatched ${valueType} started at line ${startToken.start.line} column ${startToken.start.col}`, scanner.peek()); + } + const result = { kind: valueType, value: values }; + if (closingEnd !== undefined) { + setPos(result, { start: startToken.start.offset, end: closingEnd }); + } + return result; + }; +}; +var readList = collectionReader("list", tokenKeywords.RParen); +var readVector = collectionReader("vector", tokenKeywords.RBracket); +var readSet = (ctx) => { + const scanner = ctx.scanner; + const startToken = scanner.peek(); + if (!startToken) { + throw new ReaderError("Unexpected end of input while parsing set", scanner.position()); + } + scanner.advance(); + const values = []; + let pairMatched = false; + let closingEnd; + while (!scanner.isAtEnd()) { + const token = scanner.peek(); + if (!token) + break; + if (isClosingToken(token) && token.kind !== tokenKeywords.RBrace) { + throw new ReaderError(`Expected '}' to close set started at line ${startToken.start.line} column ${startToken.start.col}, but got '${getTokenValue(token)}' at line ${token.start.line} column ${token.start.col}`, token, { start: token.start.offset, end: token.end.offset }); + } + if (token.kind === tokenKeywords.RBrace) { + closingEnd = token.end.offset; + scanner.advance(); + pairMatched = true; + break; + } + values.push(readForm(ctx)); + } + if (!pairMatched) { + throw new ReaderError(`Unmatched set started at line ${startToken.start.line} column ${startToken.start.col}`, scanner.peek()); + } + const deduped = []; + for (const v2 of values) { + if (!deduped.some((existing) => is.equal(existing, v2))) { + deduped.push(v2); + } + } + const result = v.set(deduped); + if (closingEnd !== undefined) { + setPos(result, { start: startToken.start.offset, end: closingEnd }); + } + return result; +}; +var readSymbol = (scanner) => { + const token = scanner.peek(); + if (!token) { + throw new ReaderError("Unexpected end of input", scanner.position()); + } + if (token.kind !== tokenKeywords.Symbol) { + throw new ReaderError(`Unexpected token: ${getTokenValue(token)}`, token, { + start: token.start.offset, + end: token.end.offset + }); + } + scanner.advance(); + let val; + switch (token.value) { + case "true": + case "false": + val = v.boolean(token.value === "true"); + break; + case "nil": + val = v.nil(); + break; + default: + val = v.symbol(token.value); + } + setPos(val, { start: token.start.offset, end: token.end.offset }); + return val; +}; +var readMap = (ctx) => { + const scanner = ctx.scanner; + const startToken = scanner.peek(); + if (!startToken) { + throw new ReaderError("Unexpected end of input while parsing map", scanner.position()); + } + let pairMatched = false; + let closingEnd; + scanner.advance(); + const entries = []; + while (!scanner.isAtEnd()) { + const token = scanner.peek(); + if (!token) { + break; + } + if (isClosingToken(token) && token.kind !== tokenKeywords.RBrace) { + throw new ReaderError(`Expected '}' to close map started at line ${startToken.start.line} column ${startToken.start.col}, but got '${token.kind}' at line ${token.start.line} column ${token.start.col}`, token, { start: token.start.offset, end: token.end.offset }); + } + if (token.kind === tokenKeywords.RBrace) { + closingEnd = token.end.offset; + scanner.advance(); + pairMatched = true; + break; + } + const key = readForm(ctx); + const nextToken = scanner.peek(); + if (!nextToken) { + throw new ReaderError(`Expected value in map started at line ${startToken.start.line} column ${startToken.start.col}, but got end of input`, scanner.position()); + } + if (nextToken.kind === tokenKeywords.RBrace) { + throw new ReaderError(`Map started at line ${startToken.start.line} column ${startToken.start.col} has key ${key.kind} but no value`, scanner.position()); + } + const value = readForm(ctx); + if (!value) { + break; + } + entries.push([key, value]); + } + if (!pairMatched) { + throw new ReaderError(`Unmatched map started at line ${startToken.start.line} column ${startToken.start.col}`, scanner.peek()); + } + const result = { kind: valueKeywords.map, entries }; + if (closingEnd !== undefined) { + setPos(result, { start: startToken.start.offset, end: closingEnd }); + } + return result; +}; +function collectAnonFnParams(forms) { + let maxIndex = 0; + let hasRest = false; + function walk(form) { + switch (form.kind) { + case "symbol": { + const name = form.name; + if (name === "%" || name === "%1") { + maxIndex = Math.max(maxIndex, 1); + } else if (/^%[2-9]$/.test(name)) { + maxIndex = Math.max(maxIndex, parseInt(name[1])); + } else if (name === "%&") { + hasRest = true; + } + break; + } + case "list": + case "vector": + for (const child of form.value) + walk(child); + break; + case "map": + for (const [k, v2] of form.entries) { + walk(k); + walk(v2); + } + break; + default: + break; + } + } + for (const form of forms) + walk(form); + return { maxIndex, hasRest }; +} +function substituteAnonFnParams(form) { + switch (form.kind) { + case "symbol": { + const name = form.name; + if (name === "%" || name === "%1") + return v.symbol("p1"); + if (/^%[2-9]$/.test(name)) + return v.symbol(`p${name[1]}`); + if (name === "%&") + return v.symbol("rest"); + return form; + } + case "list": + return { ...form, value: form.value.map(substituteAnonFnParams) }; + case "vector": + return { ...form, value: form.value.map(substituteAnonFnParams) }; + case "map": + return { + ...form, + entries: form.entries.map(([k, v2]) => [substituteAnonFnParams(k), substituteAnonFnParams(v2)]) + }; + default: + return form; + } +} +var readAnonFn = (ctx) => { + const scanner = ctx.scanner; + const startToken = scanner.peek(); + if (!startToken) { + throw new ReaderError("Unexpected end of input while parsing anonymous function", scanner.position()); + } + scanner.advance(); + const bodyForms = []; + let pairMatched = false; + let closingEnd; + while (!scanner.isAtEnd()) { + const token = scanner.peek(); + if (!token) + break; + if (isClosingToken(token) && token.kind !== tokenKeywords.RParen) { + throw new ReaderError(`Expected ')' to close anonymous function started at line ${startToken.start.line} column ${startToken.start.col}, but got '${getTokenValue(token)}' at line ${token.start.line} column ${token.start.col}`, token, { start: token.start.offset, end: token.end.offset }); + } + if (token.kind === tokenKeywords.RParen) { + closingEnd = token.end.offset; + scanner.advance(); + pairMatched = true; + break; + } + if (token.kind === tokenKeywords.AnonFnStart) { + throw new ReaderError("Nested anonymous functions (#(...)) are not allowed", token, { start: token.start.offset, end: token.end.offset }); + } + bodyForms.push(readForm(ctx)); + } + if (!pairMatched) { + throw new ReaderError(`Unmatched anonymous function started at line ${startToken.start.line} column ${startToken.start.col}`, scanner.peek()); + } + const bodyList = { kind: "list", value: bodyForms }; + const { maxIndex, hasRest } = collectAnonFnParams([bodyList]); + const paramSymbols = []; + for (let i = 1;i <= maxIndex; i++) { + paramSymbols.push(v.symbol(`p${i}`)); + } + if (hasRest) { + paramSymbols.push(v.symbol("&")); + paramSymbols.push(v.symbol("rest")); + } + const substitutedBody = substituteAnonFnParams(bodyList); + const result = v.list([ + v.symbol("fn"), + v.vector(paramSymbols), + substitutedBody + ]); + if (closingEnd !== undefined) { + setPos(result, { start: startToken.start.offset, end: closingEnd }); + } + return result; +}; +function extractInlineFlags(raw) { + let remaining = raw; + let flags = ""; + const flagGroupRe = /^\(\?([imsx]+)\)/; + let m; + while ((m = flagGroupRe.exec(remaining)) !== null) { + for (const f of m[1]) { + if (f === "x") { + throw new ReaderError("Regex flag (?x) (verbose mode) has no JavaScript equivalent and is not supported", null); + } + if (!flags.includes(f)) + flags += f; + } + remaining = remaining.slice(m[0].length); + } + return { pattern: remaining, flags }; +} +var readRegex = (ctx) => { + const scanner = ctx.scanner; + const token = scanner.peek(); + if (!token || token.kind !== tokenKeywords.Regex) { + throw new ReaderError("Expected regex token", scanner.position()); + } + scanner.advance(); + const { pattern, flags } = extractInlineFlags(token.value); + const val = v.regex(pattern, flags); + setPos(val, { start: token.start.offset, end: token.end.offset }); + return val; +}; +function readForm(ctx) { + const scanner = ctx.scanner; + const token = scanner.peek(); + if (!token) { + throw new ReaderError("Unexpected end of input", scanner.position()); + } + switch (token.kind) { + case tokenKeywords.String: + case tokenKeywords.Number: + case tokenKeywords.Keyword: + case tokenKeywords.Symbol: + return readAtom(ctx); + case tokenKeywords.LParen: + return readList(ctx); + case tokenKeywords.LBrace: + return readMap(ctx); + case tokenKeywords.LBracket: + return readVector(ctx); + case tokenKeywords.Quote: + return readQuote(ctx); + case tokenKeywords.Quasiquote: + return readQuasiquote(ctx); + case tokenKeywords.Unquote: + return readUnquote(ctx); + case tokenKeywords.UnquoteSplicing: + return readUnquoteSplicing(ctx); + case tokenKeywords.AnonFnStart: + return readAnonFn(ctx); + case tokenKeywords.SetStart: + return readSet(ctx); + case tokenKeywords.Deref: + return readDeref(ctx); + case tokenKeywords.VarQuote: + return readVarQuote(ctx); + case tokenKeywords.Meta: + return readMeta(ctx); + case tokenKeywords.Regex: + return readRegex(ctx); + default: + throw new ReaderError(`Unexpected token: ${getTokenValue(token)} at line ${token.start.line} column ${token.start.col}`, token, { start: token.start.offset, end: token.end.offset }); + } +} +function readForms(input, currentNs = "user", aliases = new Map) { + const withoutComments = input.filter((t) => t.kind !== tokenKeywords.Comment); + const scanner = makeTokenScanner(withoutComments); + const ctx = { + scanner, + namespace: currentNs, + aliases + }; + const values = []; + while (!scanner.isAtEnd()) { + values.push(readForm(ctx)); + } + return values; +} -(defn includes? - "True if s includes substr." - [s substr] - (str-includes* s substr)) +// src/core/module.ts +function resolveModuleOrder(modules, existingNamespaces) { + const byId = new Map; + for (const m of modules) { + if (byId.has(m.id)) { + throw new Error(`Duplicate module ID: '${m.id}'`); + } + byId.set(m.id, m); + } + const nsProviders = new Map; + for (const m of modules) { + for (const decl of m.declareNs) { + const providers = nsProviders.get(decl.name) ?? []; + providers.push(m.id); + nsProviders.set(decl.name, providers); + } + } + const graph = new Map; + const inDegree = new Map; + for (const m of modules) { + graph.set(m.id, []); + inDegree.set(m.id, 0); + } + for (const m of modules) { + for (const depNs of m.dependsOn ?? []) { + if (existingNamespaces?.has(depNs)) + continue; + const providers = nsProviders.get(depNs); + if (!providers || providers.length === 0) { + throw new Error(`No module provides namespace '${depNs}' (required by '${m.id}')`); + } + for (const providerId of providers) { + if (providerId === m.id) + continue; + graph.get(providerId).push(m.id); + inDegree.set(m.id, inDegree.get(m.id) + 1); + } + } + } + const queue = []; + for (const [id, degree] of inDegree) { + if (degree === 0) + queue.push(id); + } + const result = []; + while (queue.length > 0) { + const id = queue.shift(); + result.push(byId.get(id)); + for (const dependentId of graph.get(id)) { + const newDegree = inDegree.get(dependentId) - 1; + inDegree.set(dependentId, newDegree); + if (newDegree === 0) + queue.push(dependentId); + } + } + if (result.length !== modules.length) { + const unprocessed = modules.map((m) => m.id).filter((id) => !result.some((m) => m.id === id)); + throw new Error(`Circular dependency detected in module system. Modules in cycle: ${unprocessed.join(", ")}`); + } + return result; +} -;; --------------------------------------------------------------------------- -;; Search -;; --------------------------------------------------------------------------- +// src/core/stdlib/arithmetic.ts +var arithmeticFunctions = { + "+": v.nativeFn("+", function add(...nums) { + if (nums.length === 0) { + return v.number(0); + } + const badIdx = nums.findIndex(function isNotNumber(a) { + return a.kind !== "number"; + }); + if (badIdx !== -1) { + throw EvaluationError.atArg("+ expects all arguments to be numbers", { args: nums }, badIdx); + } + return nums.reduce(function sumNumbers(acc, arg) { + return v.number(acc.value + arg.value); + }, v.number(0)); + }).doc("Returns the sum of the arguments. Throws on non-number arguments.", [ + ["&", "nums"] + ]), + "-": v.nativeFn("-", function subtract(...nums) { + if (nums.length === 0) { + throw new EvaluationError("- expects at least one argument", { + args: nums + }); + } + const badIdx = nums.findIndex(function isNotNumber(a) { + return a.kind !== "number"; + }); + if (badIdx !== -1) { + throw EvaluationError.atArg("- expects all arguments to be numbers", { args: nums }, badIdx); + } + return nums.slice(1).reduce(function subtractNumbers(acc, arg) { + return v.number(acc.value - arg.value); + }, nums[0]); + }).doc("Returns the difference of the arguments. Throws on non-number arguments.", [["&", "nums"]]), + "*": v.nativeFn("*", function multiply(...nums) { + if (nums.length === 0) { + return v.number(1); + } + const badIdx = nums.findIndex(function isNotNumber(a) { + return a.kind !== "number"; + }); + if (badIdx !== -1) { + throw EvaluationError.atArg("* expects all arguments to be numbers", { args: nums }, badIdx); + } + return nums.slice(1).reduce(function multiplyNumbers(acc, arg) { + return v.number(acc.value * arg.value); + }, nums[0]); + }).doc("Returns the product of the arguments. Throws on non-number arguments.", [["&", "nums"]]), + "/": v.nativeFn("/", function divide(...nums) { + if (nums.length === 0) { + throw new EvaluationError("/ expects at least one argument", { + args: nums + }); + } + const badIdx = nums.findIndex(function isNotNumber(a) { + return a.kind !== "number"; + }); + if (badIdx !== -1) { + throw EvaluationError.atArg("/ expects all arguments to be numbers", { args: nums }, badIdx); + } + return nums.slice(1).reduce(function divideNumbers(acc, arg, reduceIdx) { + if (arg.value === 0) { + const err = new EvaluationError("division by zero", { args: nums }); + err.data = { argIndex: reduceIdx + 1 }; + throw err; + } + return v.number(acc.value / arg.value); + }, nums[0]); + }).doc("Returns the quotient of the arguments. Throws on non-number arguments or division by zero.", [["&", "nums"]]), + ">": v.nativeFn(">", function greaterThan(...nums) { + if (nums.length < 2) { + throw new EvaluationError("> expects at least two arguments", { + args: nums + }); + } + const badIdx = nums.findIndex(function isNotNumber(a) { + return a.kind !== "number"; + }); + if (badIdx !== -1) { + throw EvaluationError.atArg("> expects all arguments to be numbers", { args: nums }, badIdx); + } + for (let i = 1;i < nums.length; i++) { + if (nums[i].value >= nums[i - 1].value) { + return v.boolean(false); + } + } + return v.boolean(true); + }).doc("Compares adjacent arguments left to right, returns true if all values are in ascending order, false otherwise.", [["&", "nums"]]), + "<": v.nativeFn("<", function lessThan(...nums) { + if (nums.length < 2) { + throw new EvaluationError("< expects at least two arguments", { + args: nums + }); + } + const badIdx = nums.findIndex(function isNotNumber(a) { + return a.kind !== "number"; + }); + if (badIdx !== -1) { + throw EvaluationError.atArg("< expects all arguments to be numbers", { args: nums }, badIdx); + } + for (let i = 1;i < nums.length; i++) { + if (nums[i].value <= nums[i - 1].value) { + return v.boolean(false); + } + } + return v.boolean(true); + }).doc("Compares adjacent arguments left to right, returns true if all values are in descending order, false otherwise.", [["&", "nums"]]), + ">=": v.nativeFn(">=", function greaterThanOrEqual(...nums) { + if (nums.length < 2) { + throw new EvaluationError(">= expects at least two arguments", { + args: nums + }); + } + const badIdx = nums.findIndex(function isNotNumber(a) { + return a.kind !== "number"; + }); + if (badIdx !== -1) { + throw EvaluationError.atArg(">= expects all arguments to be numbers", { args: nums }, badIdx); + } + for (let i = 1;i < nums.length; i++) { + if (nums[i].value > nums[i - 1].value) { + return v.boolean(false); + } + } + return v.boolean(true); + }).doc("Compares adjacent arguments left to right, returns true if all comparisons returns true for greater than or equal to checks, false otherwise.", [["&", "nums"]]), + "<=": v.nativeFn("<=", function lessThanOrEqual(...nums) { + if (nums.length < 2) { + throw new EvaluationError("<= expects at least two arguments", { + args: nums + }); + } + const badIdx = nums.findIndex(function isNotNumber(a) { + return a.kind !== "number"; + }); + if (badIdx !== -1) { + throw EvaluationError.atArg("<= expects all arguments to be numbers", { args: nums }, badIdx); + } + for (let i = 1;i < nums.length; i++) { + if (nums[i].value < nums[i - 1].value) { + return v.boolean(false); + } + } + return v.boolean(true); + }).doc("Compares adjacent arguments left to right, returns true if all comparisons returns true for less than or equal to checks, false otherwise.", [["&", "nums"]]), + "=": v.nativeFn("=", function equals(...vals) { + if (vals.length < 2) { + throw new EvaluationError("= expects at least two arguments", { + args: vals + }); + } + for (let i = 1;i < vals.length; i++) { + if (!is.equal(vals[i], vals[i - 1])) { + return v.boolean(false); + } + } + return v.boolean(true); + }).doc("Compares adjacent arguments left to right, returns true if all values are structurally equal, false otherwise.", [["&", "vals"]]), + inc: v.nativeFn("inc", function increment(x) { + if (x === undefined || x.kind !== "number") { + throw EvaluationError.atArg(`inc expects a number${x !== undefined ? `, got ${printString(x)}` : ""}`, { x }, 0); + } + return v.number(x.value + 1); + }).doc("Returns the argument incremented by 1. Throws on non-number arguments.", [["x"]]), + dec: v.nativeFn("dec", function decrement(x) { + if (x === undefined || x.kind !== "number") { + throw EvaluationError.atArg(`dec expects a number${x !== undefined ? `, got ${printString(x)}` : ""}`, { x }, 0); + } + return v.number(x.value - 1); + }).doc("Returns the argument decremented by 1. Throws on non-number arguments.", [["x"]]), + max: v.nativeFn("max", function maximum(...nums) { + if (nums.length === 0) { + throw new EvaluationError("max expects at least one argument", { + args: nums + }); + } + const badIdx = nums.findIndex(function isNotNumber(a) { + return a.kind !== "number"; + }); + if (badIdx !== -1) { + throw EvaluationError.atArg("max expects all arguments to be numbers", { args: nums }, badIdx); + } + return nums.reduce(function findMax(best, arg) { + return arg.value > best.value ? arg : best; + }); + }).doc("Returns the largest of the arguments. Throws on non-number arguments.", [["&", "nums"]]), + min: v.nativeFn("min", function minimum(...nums) { + if (nums.length === 0) { + throw new EvaluationError("min expects at least one argument", { + args: nums + }); + } + const badIdx = nums.findIndex(function isNotNumber(a) { + return a.kind !== "number"; + }); + if (badIdx !== -1) { + throw EvaluationError.atArg("min expects all arguments to be numbers", { args: nums }, badIdx); + } + return nums.reduce(function findMin(best, arg) { + return arg.value < best.value ? arg : best; + }); + }).doc("Returns the smallest of the arguments. Throws on non-number arguments.", [["&", "nums"]]), + mod: v.nativeFn("mod", function modulo(n, d) { + if (n === undefined || n.kind !== "number") { + throw EvaluationError.atArg(`mod expects a number as first argument${n !== undefined ? `, got ${printString(n)}` : ""}`, { n }, 0); + } + if (d === undefined || d.kind !== "number") { + throw EvaluationError.atArg(`mod expects a number as second argument${d !== undefined ? `, got ${printString(d)}` : ""}`, { d }, 1); + } + if (d.value === 0) { + const err = new EvaluationError("mod: division by zero", { n, d }); + err.data = { argIndex: 1 }; + throw err; + } + const result = n.value % d.value; + return v.number(result < 0 ? result + Math.abs(d.value) : result); + }).doc("Returns the remainder of the first argument divided by the second argument. Throws on non-number arguments or division by zero.", [["n", "d"]]), + "even?": v.nativeFn("even?", function isEven(n) { + if (n === undefined || n.kind !== "number") { + throw EvaluationError.atArg(`even? expects a number${n !== undefined ? `, got ${printString(n)}` : ""}`, { n }, 0); + } + return v.boolean(n.value % 2 === 0); + }).doc("Returns true if the argument is an even number, false otherwise.", [ + ["n"] + ]), + "odd?": v.nativeFn("odd?", function isOdd(n) { + if (n === undefined || n.kind !== "number") { + throw EvaluationError.atArg(`odd? expects a number${n !== undefined ? `, got ${printString(n)}` : ""}`, { n }, 0); + } + return v.boolean(Math.abs(n.value) % 2 !== 0); + }).doc("Returns true if the argument is an odd number, false otherwise.", [ + ["n"] + ]), + "pos?": v.nativeFn("pos?", function isPositive(n) { + if (n === undefined || n.kind !== "number") { + throw EvaluationError.atArg(`pos? expects a number${n !== undefined ? `, got ${printString(n)}` : ""}`, { n }, 0); + } + return v.boolean(n.value > 0); + }).doc("Returns true if the argument is a positive number, false otherwise.", [["n"]]), + "neg?": v.nativeFn("neg?", function isNegative(n) { + if (n === undefined || n.kind !== "number") { + throw EvaluationError.atArg(`neg? expects a number${n !== undefined ? `, got ${printString(n)}` : ""}`, { n }, 0); + } + return v.boolean(n.value < 0); + }).doc("Returns true if the argument is a negative number, false otherwise.", [["n"]]), + "zero?": v.nativeFn("zero?", function isZero(n) { + if (n === undefined || n.kind !== "number") { + throw EvaluationError.atArg(`zero? expects a number${n !== undefined ? `, got ${printString(n)}` : ""}`, { n }, 0); + } + return v.boolean(n.value === 0); + }).doc("Returns true if the argument is zero, false otherwise.", [["n"]]), + abs: v.nativeFn("abs", function absImpl(n) { + if (n === undefined || n.kind !== "number") { + throw EvaluationError.atArg(`abs expects a number${n !== undefined ? `, got ${printString(n)}` : ""}`, { n }, 0); + } + return v.number(Math.abs(n.value)); + }).doc("Returns the absolute value of a.", [["a"]]), + quot: v.nativeFn("quot", function quotImpl(num, div) { + if (num === undefined || num.kind !== "number") { + throw EvaluationError.atArg(`quot expects a number as first argument`, { num }, 0); + } + if (div === undefined || div.kind !== "number") { + throw EvaluationError.atArg(`quot expects a number as second argument`, { div }, 1); + } + if (div.value === 0) { + throw new EvaluationError("quot: division by zero", { num, div }); + } + return v.number(Math.trunc(num.value / div.value)); + }).doc("quot[ient] of dividing numerator by denominator.", [["num", "div"]]), + rem: v.nativeFn("rem", function remImpl(num, div) { + if (num === undefined || num.kind !== "number") { + throw EvaluationError.atArg(`rem expects a number as first argument`, { num }, 0); + } + if (div === undefined || div.kind !== "number") { + throw EvaluationError.atArg(`rem expects a number as second argument`, { div }, 1); + } + if (div.value === 0) { + throw new EvaluationError("rem: division by zero", { num, div }); + } + return v.number(num.value % div.value); + }).doc("remainder of dividing numerator by denominator.", [["num", "div"]]), + rand: v.nativeFn("rand", function randImpl(...args) { + if (args.length === 0) + return v.number(Math.random()); + if (args[0].kind !== "number") { + throw EvaluationError.atArg(`rand expects a number`, { n: args[0] }, 0); + } + return v.number(Math.random() * args[0].value); + }).doc("Returns a random floating point number between 0 (inclusive) and n (default 1) (exclusive).", [[], ["n"]]), + "rand-int": v.nativeFn("rand-int", function randIntImpl(n) { + if (n === undefined || n.kind !== "number") { + throw EvaluationError.atArg(`rand-int expects a number`, { n }, 0); + } + return v.number(Math.floor(Math.random() * n.value)); + }).doc("Returns a random integer between 0 (inclusive) and n (exclusive).", [ + ["n"] + ]), + "rand-nth": v.nativeFn("rand-nth", function randNthImpl(coll) { + if (coll === undefined || !is.list(coll) && !is.vector(coll)) { + throw EvaluationError.atArg(`rand-nth expects a list or vector`, { coll }, 0); + } + const items = coll.value; + if (items.length === 0) { + throw new EvaluationError("rand-nth called on empty collection", { + coll + }); + } + return items[Math.floor(Math.random() * items.length)]; + }).doc("Return a random element of the (sequential) collection.", [["coll"]]), + shuffle: v.nativeFn("shuffle", function shuffleImpl(coll) { + if (coll === undefined || coll.kind === "nil") + return v.vector([]); + if (!is.seqable(coll)) { + throw EvaluationError.atArg(`shuffle expects a collection, got ${printString(coll)}`, { coll }, 0); + } + const arr = [...toSeq(coll)]; + for (let i = arr.length - 1;i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [arr[i], arr[j]] = [arr[j], arr[i]]; + } + return v.vector(arr); + }).doc("Return a random permutation of coll.", [["coll"]]), + "bit-and": v.nativeFn("bit-and", function bitAndImpl(x, y) { + if (x?.kind !== "number") + throw EvaluationError.atArg("bit-and expects numbers", { x }, 0); + if (y?.kind !== "number") + throw EvaluationError.atArg("bit-and expects numbers", { y }, 1); + return v.number(x.value & y.value); + }).doc("Bitwise and", [["x", "y"]]), + "bit-or": v.nativeFn("bit-or", function bitOrImpl(x, y) { + if (x?.kind !== "number") + throw EvaluationError.atArg("bit-or expects numbers", { x }, 0); + if (y?.kind !== "number") + throw EvaluationError.atArg("bit-or expects numbers", { y }, 1); + return v.number(x.value | y.value); + }).doc("Bitwise or", [["x", "y"]]), + "bit-xor": v.nativeFn("bit-xor", function bitXorImpl(x, y) { + if (x?.kind !== "number") + throw EvaluationError.atArg("bit-xor expects numbers", { x }, 0); + if (y?.kind !== "number") + throw EvaluationError.atArg("bit-xor expects numbers", { y }, 1); + return v.number(x.value ^ y.value); + }).doc("Bitwise exclusive or", [["x", "y"]]), + "bit-not": v.nativeFn("bit-not", function bitNotImpl(x) { + if (x?.kind !== "number") + throw EvaluationError.atArg("bit-not expects a number", { x }, 0); + return v.number(~x.value); + }).doc("Bitwise complement", [["x"]]), + "bit-shift-left": v.nativeFn("bit-shift-left", function bitShiftLeftImpl(x, n) { + if (x?.kind !== "number") + throw EvaluationError.atArg("bit-shift-left expects numbers", { x }, 0); + if (n?.kind !== "number") + throw EvaluationError.atArg("bit-shift-left expects numbers", { n }, 1); + return v.number(x.value << n.value); + }).doc("Bitwise shift left", [["x", "n"]]), + "bit-shift-right": v.nativeFn("bit-shift-right", function bitShiftRightImpl(x, n) { + if (x?.kind !== "number") + throw EvaluationError.atArg("bit-shift-right expects numbers", { x }, 0); + if (n?.kind !== "number") + throw EvaluationError.atArg("bit-shift-right expects numbers", { n }, 1); + return v.number(x.value >> n.value); + }).doc("Bitwise shift right", [["x", "n"]]), + "unsigned-bit-shift-right": v.nativeFn("unsigned-bit-shift-right", function unsignedBitShiftRightImpl(x, n) { + if (x?.kind !== "number") + throw EvaluationError.atArg("unsigned-bit-shift-right expects numbers", { x }, 0); + if (n?.kind !== "number") + throw EvaluationError.atArg("unsigned-bit-shift-right expects numbers", { n }, 1); + return v.number(x.value >>> n.value); + }).doc("Bitwise shift right, without sign-extension", [["x", "n"]]) +}; -(defn index-of - "Return index of value (string) in s, optionally searching forward from - from-index. Return nil if value not found." - ([s value] (str-index-of* s value)) - ([s value from-index] (str-index-of* s value from-index))) +// src/core/stdlib/atoms.ts +function validateAtom(a, newVal, ctx, callEnv) { + if (a.validator && is.aFunction(a.validator)) { + const result = ctx.applyFunction(a.validator, [newVal], callEnv); + if (is.falsy(result)) { + throw new EvaluationError("Invalid reference state", { newVal }); + } + } +} +function notifyWatches(a, oldVal, newVal) { + if (a.watches) { + for (const [, { key, fn, ctx, callEnv }] of a.watches) { + ctx.applyFunction(fn, [key, { kind: "atom", value: newVal }, oldVal, newVal], callEnv); + } + } +} +var atomFunctions = { + atom: v.nativeFn("atom", function atom(value) { + return v.atom(value); + }).doc("Returns a new atom holding the given value.", [["value"]]), + deref: v.nativeFn("deref", function deref(value) { + if (is.atom(value)) + return value.value; + if (is.volatile(value)) + return value.value; + if (is.reduced(value)) + return value.value; + if (is.delay(value)) + return realizeDelay(value); + if (value.kind === "pending") { + throw EvaluationError.atArg("@ on a pending value requires an (async ...) context. Use (async @x) or compose with then/catch.", { value }, 0); + } + throw EvaluationError.atArg(`deref expects an atom, volatile, reduced, or delay value, got ${value.kind}`, { value }, 0); + }).doc("Returns the wrapped value from an atom, volatile, reduced, or delay value.", [["value"]]), + "swap!": v.nativeFnCtx("swap!", function swap(ctx, callEnv, atomVal, fn, ...extraArgs) { + if (!is.atom(atomVal)) { + throw EvaluationError.atArg(`swap! expects an atom as its first argument, got ${atomVal.kind}`, { atomVal }, 0); + } + if (!is.aFunction(fn)) { + throw EvaluationError.atArg(`swap! expects a function as its second argument, got ${fn.kind}`, { fn }, 1); + } + const a = atomVal; + const oldVal = a.value; + const newVal = ctx.applyFunction(fn, [oldVal, ...extraArgs], callEnv); + validateAtom(a, newVal, ctx, callEnv); + a.value = newVal; + notifyWatches(a, oldVal, newVal); + return newVal; + }).doc("Applies fn to the current value of the atom, replacing the current value with the result. Returns the new value.", [["atomVal", "fn", "&", "extraArgs"]]), + "reset!": v.nativeFnCtx("reset!", function reset(ctx, callEnv, atomVal, newVal) { + if (!is.atom(atomVal)) { + throw EvaluationError.atArg(`reset! expects an atom as its first argument, got ${atomVal.kind}`, { atomVal }, 0); + } + const a = atomVal; + const oldVal = a.value; + validateAtom(a, newVal, ctx, callEnv); + a.value = newVal; + notifyWatches(a, oldVal, newVal); + return newVal; + }).doc("Sets the value of the atom to newVal and returns the new value.", [ + ["atomVal", "newVal"] + ]), + "atom?": v.nativeFn("atom?", function isAtomPredicate(value) { + return v.boolean(is.atom(value)); + }).doc("Returns true if the value is an atom, false otherwise.", [["value"]]), + "swap-vals!": v.nativeFnCtx("swap-vals!", function swapVals(ctx, callEnv, atomVal, fn, ...extraArgs) { + if (!is.atom(atomVal)) { + throw EvaluationError.atArg(`swap-vals! expects an atom, got ${printString(atomVal)}`, { atomVal }, 0); + } + if (!is.aFunction(fn)) { + throw EvaluationError.atArg(`swap-vals! expects a function, got ${printString(fn)}`, { fn }, 1); + } + const oldVal = atomVal.value; + const newVal = ctx.applyFunction(fn, [oldVal, ...extraArgs], callEnv); + atomVal.value = newVal; + return v.vector([oldVal, newVal]); + }).doc("Atomically swaps the value of atom to be (apply f current-value-of-atom args). Returns [old new].", [["atom", "f", "&", "args"]]), + "reset-vals!": v.nativeFn("reset-vals!", function resetVals(atomVal, newVal) { + if (!is.atom(atomVal)) { + throw EvaluationError.atArg(`reset-vals! expects an atom, got ${printString(atomVal)}`, { atomVal }, 0); + } + const oldVal = atomVal.value; + atomVal.value = newVal; + return v.vector([oldVal, newVal]); + }).doc("Sets the value of atom to newVal. Returns [old new].", [ + ["atom", "newval"] + ]), + "compare-and-set!": v.nativeFn("compare-and-set!", function compareAndSet(atomVal, oldv, newv) { + if (!is.atom(atomVal)) { + throw EvaluationError.atArg(`compare-and-set! expects an atom, got ${printString(atomVal)}`, { atomVal }, 0); + } + if (is.equal(atomVal.value, oldv)) { + atomVal.value = newv; + return v.boolean(true); + } + return v.boolean(false); + }).doc("Atomically sets the value of atom to newval if and only if the current value of the atom is identical to oldval. Returns true if set happened, else false.", [["atom", "oldval", "newval"]]), + "add-watch": v.nativeFnCtx("add-watch", function addWatch(ctx, callEnv, atomVal, key, fn) { + if (!is.atom(atomVal)) { + throw EvaluationError.atArg(`add-watch expects an atom, got ${printString(atomVal)}`, { atomVal }, 0); + } + if (!is.aFunction(fn)) { + throw EvaluationError.atArg(`add-watch expects a function, got ${printString(fn)}`, { fn }, 2); + } + const a = atomVal; + if (!a.watches) + a.watches = new Map; + a.watches.set(printString(key), { key, fn, ctx, callEnv }); + return atomVal; + }).doc("Adds a watch function to an atom. The watch fn must be a fn of 4 args: a key, the atom, its old-state, its new-state.", [["atom", "key", "fn"]]), + "remove-watch": v.nativeFn("remove-watch", function removeWatch(atomVal, key) { + if (!is.atom(atomVal)) { + throw EvaluationError.atArg(`remove-watch expects an atom, got ${printString(atomVal)}`, { atomVal }, 0); + } + const a = atomVal; + if (a.watches) + a.watches.delete(printString(key)); + return atomVal; + }).doc("Removes a watch (set by add-watch) from an atom.", [["atom", "key"]]), + "set-validator!": v.nativeFnCtx("set-validator!", function setValidator(_ctx, _callEnv, atomVal, fn) { + if (!is.atom(atomVal)) { + throw EvaluationError.atArg(`set-validator! expects an atom, got ${printString(atomVal)}`, { atomVal }, 0); + } + if (fn.kind === "nil") { + atomVal.validator = undefined; + return v.nil(); + } + if (!is.aFunction(fn)) { + throw EvaluationError.atArg(`set-validator! expects a function or nil, got ${printString(fn)}`, { fn }, 1); + } + atomVal.validator = fn; + return v.nil(); + }).doc("Sets the validator-fn for an atom. fn must be nil or a side-effect-free fn of one argument.", [["atom", "fn"]]) +}; -(defn last-index-of - "Return last index of value (string) in s, optionally searching backward - from from-index. Return nil if value not found." - ([s value] (str-last-index-of* s value)) - ([s value from-index] (str-last-index-of* s value from-index))) +// src/core/stdlib/collections.ts +var collectionFunctions = { + list: v.nativeFn("list", function listImpl(...args) { + if (args.length === 0) { + return v.list([]); + } + return v.list(args); + }).doc("Returns a new list containing the given values.", [["&", "args"]]), + vector: v.nativeFn("vector", function vectorImpl(...args) { + if (args.length === 0) { + return v.vector([]); + } + return v.vector(args); + }).doc("Returns a new vector containing the given values.", [["&", "args"]]), + "hash-map": v.nativeFn("hash-map", function hashMapImpl(...kvals) { + if (kvals.length === 0) { + return v.map([]); + } + if (kvals.length % 2 !== 0) { + throw new EvaluationError(`hash-map expects an even number of arguments, got ${kvals.length}`, { args: kvals }); + } + const entries = []; + for (let i = 0;i < kvals.length; i += 2) { + const key = kvals[i]; + const value = kvals[i + 1]; + entries.push([key, value]); + } + return v.map(entries); + }).doc("Returns a new hash-map containing the given key-value pairs.", [ + ["&", "kvals"] + ]), + seq: v.nativeFn("seq", function seqImpl(coll) { + if (coll.kind === "nil") + return v.nil(); + if (is.lazySeq(coll)) { + const realized = realizeLazySeq(coll); + if (is.nil(realized)) + return v.nil(); + return seqImpl(realized); + } + if (is.cons(coll)) + return coll; + if (!is.seqable(coll)) { + throw EvaluationError.atArg(`seq expects a collection, string, or nil, got ${printString(coll)}`, { collection: coll }, 0); + } + const items = toSeq(coll); + return items.length === 0 ? v.nil() : v.list(items); + }).doc("Returns a sequence of the given collection or string. Strings yield a sequence of single-character strings.", [["coll"]]), + first: v.nativeFn("first", function firstImpl(collection) { + if (collection.kind === "nil") + return v.nil(); + if (is.lazySeq(collection)) { + const realized = realizeLazySeq(collection); + if (is.nil(realized)) + return v.nil(); + return firstImpl(realized); + } + if (is.cons(collection)) + return collection.head; + if (!is.seqable(collection)) { + throw EvaluationError.atArg("first expects a collection or string", { collection }, 0); + } + const entries = toSeq(collection); + return entries.length === 0 ? v.nil() : entries[0]; + }).doc("Returns the first element of the given collection or string.", [ + ["coll"] + ]), + rest: v.nativeFn("rest", function restImpl(collection) { + if (collection.kind === "nil") + return v.list([]); + if (is.lazySeq(collection)) { + const realized = realizeLazySeq(collection); + if (is.nil(realized)) + return v.list([]); + return restImpl(realized); + } + if (is.cons(collection)) + return collection.tail; + if (!is.seqable(collection)) { + throw EvaluationError.atArg("rest expects a collection or string", { collection }, 0); + } + if (is.list(collection)) { + if (collection.value.length === 0) { + return collection; + } + return v.list(collection.value.slice(1)); + } + if (is.vector(collection)) { + return v.vector(collection.value.slice(1)); + } + if (is.map(collection)) { + if (collection.entries.length === 0) { + return collection; + } + return v.map(collection.entries.slice(1)); + } + if (collection.kind === "string") { + const chars = toSeq(collection); + return v.list(chars.slice(1)); + } + throw EvaluationError.atArg(`rest expects a collection or string, got ${printString(collection)}`, { collection }, 0); + }).doc("Returns a sequence of the given collection or string excluding the first element.", [["coll"]]), + conj: v.nativeFn("conj", function conjImpl(collection, ...args) { + if (!collection) { + throw new EvaluationError("conj expects a collection as first argument", { collection }); + } + if (args.length === 0) { + return collection; + } + if (!is.collection(collection)) { + throw EvaluationError.atArg(`conj expects a collection, got ${printString(collection)}`, { collection }, 0); + } + if (is.list(collection)) { + const newItems = []; + for (let i = args.length - 1;i >= 0; i--) { + newItems.push(args[i]); + } + return v.list([...newItems, ...collection.value]); + } + if (is.vector(collection)) { + return v.vector([...collection.value, ...args]); + } + if (is.map(collection)) { + const newEntries = [...collection.entries]; + for (let i = 0;i < args.length; i += 1) { + const pair = args[i]; + const pairArgIndex = i + 1; + if (pair.kind !== "vector") { + throw EvaluationError.atArg(`conj on maps expects each argument to be a vector key-pair for maps, got ${printString(pair)}`, { pair }, pairArgIndex); + } + if (pair.value.length !== 2) { + throw EvaluationError.atArg(`conj on maps expects each argument to be a vector key-pair for maps, got ${printString(pair)}`, { pair }, pairArgIndex); + } + const key = pair.value[0]; + const keyIdx = newEntries.findIndex(function findKeyEntry(entry) { + return is.equal(entry[0], key); + }); + if (keyIdx === -1) { + newEntries.push([key, pair.value[1]]); + } else { + newEntries[keyIdx] = [key, pair.value[1]]; + } + } + return v.map([...newEntries]); + } + if (is.set(collection)) { + const newValues = [...collection.values]; + for (const v2 of args) { + if (!newValues.some((existing) => is.equal(existing, v2))) { + newValues.push(v2); + } + } + return v.set(newValues); + } + throw new EvaluationError(`unhandled collection type, got ${printString(collection)}`, { collection }); + }).doc("Appends args to the given collection. Lists append in reverse order to the head, vectors append to the tail, sets add unique elements.", [["collection", "&", "args"]]), + cons: v.nativeFn("cons", function consImpl(x, xs) { + if (is.lazySeq(xs) || is.cons(xs)) { + return v.cons(x, xs); + } + if (is.nil(xs)) { + return v.list([x]); + } + if (!is.collection(xs)) { + throw EvaluationError.atArg(`cons expects a collection as second argument, got ${printString(xs)}`, { xs }, 1); + } + if (is.map(xs) || is.set(xs)) { + throw EvaluationError.atArg("cons on maps and sets is not supported, use vectors instead", { xs }, 1); + } + const wrap = is.list(xs) ? v.list : v.vector; + const newItems = [x, ...xs.value]; + return wrap(newItems); + }).doc("Returns a new collection with x prepended to the head of xs.", [ + ["x", "xs"] + ]), + assoc: v.nativeFn("assoc", function assocImpl(collection, ...args) { + if (!collection) { + throw new EvaluationError("assoc expects a collection as first argument", { collection }); + } + if (is.nil(collection)) { + collection = v.map([]); + } + if (is.list(collection)) { + throw new EvaluationError("assoc on lists is not supported, use vectors instead", { collection }); + } + if (!is.collection(collection)) { + throw EvaluationError.atArg(`assoc expects a collection, got ${printString(collection)}`, { collection }, 0); + } + if (args.length < 2) { + throw new EvaluationError("assoc expects at least two arguments", { + args + }); + } + if (args.length % 2 !== 0) { + throw new EvaluationError("assoc expects an even number of binding arguments", { + args + }); + } + if (is.vector(collection)) { + const newValues = [...collection.value]; + for (let i = 0;i < args.length; i += 2) { + const index = args[i]; + if (index.kind !== "number") { + throw EvaluationError.atArg(`assoc on vectors expects each key argument to be a index (number), got ${printString(index)}`, { index }, i + 1); + } + if (index.value > newValues.length) { + throw EvaluationError.atArg(`assoc index ${index.value} is out of bounds for vector of length ${newValues.length}`, { index, collection }, i + 1); + } + newValues[index.value] = args[i + 1]; + } + return v.vector(newValues); + } + if (is.map(collection)) { + const newEntries = [...collection.entries]; + for (let i = 0;i < args.length; i += 2) { + const key = args[i]; + const value = args[i + 1]; + const entryIdx = newEntries.findIndex(function findEntryByKey(entry) { + return is.equal(entry[0], key); + }); + if (entryIdx === -1) { + newEntries.push([key, value]); + } else { + newEntries[entryIdx] = [key, value]; + } + } + return v.map(newEntries); + } + throw new EvaluationError(`unhandled collection type, got ${printString(collection)}`, { collection }); + }).doc("Associates the value val with the key k in collection. If collection is a map, returns a new map with the same mappings, otherwise returns a vector with the new value at index k.", [["collection", "&", "kvals"]]), + dissoc: v.nativeFn("dissoc", function dissocImpl(collection, ...args) { + if (!collection) { + throw new EvaluationError("dissoc expects a collection as first argument", { collection }); + } + if (is.list(collection)) { + throw EvaluationError.atArg("dissoc on lists is not supported, use vectors instead", { collection }, 0); + } + if (!is.collection(collection)) { + throw EvaluationError.atArg(`dissoc expects a collection, got ${printString(collection)}`, { collection }, 0); + } + if (is.vector(collection)) { + if (collection.value.length === 0) { + return collection; + } + const newValues = [...collection.value]; + for (let i = 0;i < args.length; i += 1) { + const index = args[i]; + if (index.kind !== "number") { + throw EvaluationError.atArg(`dissoc on vectors expects each key argument to be a index (number), got ${printString(index)}`, { index }, i + 1); + } + if (index.value >= newValues.length) { + throw EvaluationError.atArg(`dissoc index ${index.value} is out of bounds for vector of length ${newValues.length}`, { index, collection }, i + 1); + } + newValues.splice(index.value, 1); + } + return v.vector(newValues); + } + if (is.map(collection)) { + if (collection.entries.length === 0) { + return collection; + } + const newEntries = [...collection.entries]; + for (let i = 0;i < args.length; i += 1) { + const key = args[i]; + const entryIdx = newEntries.findIndex(function findEntryByKey(entry) { + return is.equal(entry[0], key); + }); + if (entryIdx === -1) { + return collection; + } + newEntries.splice(entryIdx, 1); + } + return v.map(newEntries); + } + throw new EvaluationError(`unhandled collection type, got ${printString(collection)}`, { collection }); + }).doc("Dissociates the key k from collection. If collection is a map, returns a new map with the same mappings, otherwise returns a vector with the value at index k removed.", [["collection", "&", "keys"]]), + get: v.nativeFn("get", function getImpl(target, key, notFound) { + const defaultValue = notFound ?? v.nil(); + switch (target.kind) { + case valueKeywords.map: { + const entries = target.entries; + for (const [k, v2] of entries) { + if (is.equal(k, key)) { + return v2; + } + } + return defaultValue; + } + case valueKeywords.vector: { + const values = target.value; + if (key.kind !== "number") { + throw new EvaluationError("get on vectors expects a 0-based index as parameter", { key }); + } + if (key.value < 0 || key.value >= values.length) { + return defaultValue; + } + return values[key.value]; + } + default: + return defaultValue; + } + }).doc("Returns the value associated with key in target. If target is a map, returns the value associated with key, otherwise returns the value at index key in target. If not-found is provided, it is returned if the key is not found, otherwise nil is returned.", [ + ["target", "key"], + ["target", "key", "not-found"] + ]), + nth: v.nativeFn("nth", function nthImpl(coll, n, notFound) { + if (coll === undefined || !is.list(coll) && !is.vector(coll)) { + throw new EvaluationError(`nth expects a list or vector${coll !== undefined ? `, got ${printString(coll)}` : ""}`, { coll }); + } + if (n === undefined || n.kind !== "number") { + throw new EvaluationError(`nth expects a number index${n !== undefined ? `, got ${printString(n)}` : ""}`, { n }); + } + const index = n.value; + const items = coll.value; + if (index < 0 || index >= items.length) { + if (notFound !== undefined) + return notFound; + const err = new EvaluationError(`nth index ${index} is out of bounds for collection of length ${items.length}`, { coll, n }); + err.data = { argIndex: 1 }; + throw err; + } + return items[index]; + }).doc("Returns the nth element of the given collection. If not-found is provided, it is returned if the index is out of bounds, otherwise an error is thrown.", [["coll", "n", "not-found"]]), + zipmap: v.nativeFn("zipmap", function zipmapImpl(ks, vs) { + if (ks === undefined || !is.seqable(ks)) { + throw new EvaluationError(`zipmap expects a collection or string as first argument${ks !== undefined ? `, got ${printString(ks)}` : ""}`, { ks }); + } + if (vs === undefined || !is.seqable(vs)) { + throw new EvaluationError(`zipmap expects a collection or string as second argument${vs !== undefined ? `, got ${printString(vs)}` : ""}`, { vs }); + } + const keys = toSeq(ks); + const vals = toSeq(vs); + const len = Math.min(keys.length, vals.length); + const entries = []; + for (let i = 0;i < len; i++) { + entries.push([keys[i], vals[i]]); + } + return v.map(entries); + }).doc("Returns a new map with the keys and values of the given collections.", [["ks", "vs"]]), + last: v.nativeFn("last", function lastImpl(coll) { + if (coll === undefined || !is.list(coll) && !is.vector(coll)) { + throw new EvaluationError(`last expects a list or vector${coll !== undefined ? `, got ${printString(coll)}` : ""}`, { coll }); + } + const items = coll.value; + return items.length === 0 ? v.nil() : items[items.length - 1]; + }).doc("Returns the last element of the given collection.", [["coll"]]), + reverse: v.nativeFn("reverse", function reverseImpl(coll) { + if (coll === undefined || !is.list(coll) && !is.vector(coll)) { + throw EvaluationError.atArg(`reverse expects a list or vector${coll !== undefined ? `, got ${printString(coll)}` : ""}`, { coll }, 0); + } + return v.list([...coll.value].reverse()); + }).doc("Returns a new sequence with the elements of the given collection in reverse order.", [["coll"]]), + "empty?": v.nativeFn("empty?", function emptyPredImpl(coll) { + if (coll === undefined) { + throw EvaluationError.atArg("empty? expects one argument", {}, 0); + } + if (coll.kind === "nil") + return v.boolean(true); + if (!is.seqable(coll)) { + throw EvaluationError.atArg(`empty? expects a collection, string, or nil, got ${printString(coll)}`, { coll }, 0); + } + return v.boolean(toSeq(coll).length === 0); + }).doc("Returns true if coll has no items. Accepts collections, strings, and nil.", [["coll"]]), + "contains?": v.nativeFn("contains?", function containsPredImpl(coll, key) { + if (coll === undefined) { + throw EvaluationError.atArg("contains? expects a collection as first argument", {}, 0); + } + if (key === undefined) { + throw EvaluationError.atArg("contains? expects a key as second argument", {}, 1); + } + if (coll.kind === "nil") + return v.boolean(false); + if (is.map(coll)) { + return v.boolean(coll.entries.some(function checkKeyMatch([k]) { + return is.equal(k, key); + })); + } + if (is.vector(coll)) { + if (key.kind !== "number") + return v.boolean(false); + return v.boolean(key.value >= 0 && key.value < coll.value.length); + } + if (is.set(coll)) { + return v.boolean(coll.values.some((v2) => is.equal(v2, key))); + } + throw EvaluationError.atArg(`contains? expects a map, set, vector, or nil, got ${printString(coll)}`, { coll }, 0); + }).doc("Returns true if key is present in coll. For maps checks key existence (including keys with nil values). For vectors checks index bounds.", [["coll", "key"]]), + "repeat*": v.nativeFn("repeat*", function repeatImpl(n, x) { + if (n === undefined || n.kind !== "number") { + throw EvaluationError.atArg(`repeat expects a number as first argument${n !== undefined ? `, got ${printString(n)}` : ""}`, { n }, 0); + } + return v.list(Array(n.value).fill(x)); + }).doc("Returns a finite sequence of n copies of x (native helper).", [ + ["n", "x"] + ]), + "range*": v.nativeFn("range*", function rangeImpl(...args) { + if (args.length === 0 || args.length > 3) { + throw new EvaluationError("range expects 1, 2, or 3 arguments: (range n), (range start end), or (range start end step)", { args }); + } + const badIdx = args.findIndex(function checkIsNumber(a) { + return a.kind !== "number"; + }); + if (badIdx !== -1) { + throw EvaluationError.atArg("range expects number arguments", { args }, badIdx); + } + let start; + let end; + let step; + if (args.length === 1) { + start = 0; + end = args[0].value; + step = 1; + } else if (args.length === 2) { + start = args[0].value; + end = args[1].value; + step = 1; + } else { + start = args[0].value; + end = args[1].value; + step = args[2].value; + } + if (step === 0) { + throw EvaluationError.atArg("range step cannot be zero", { args }, args.length - 1); + } + const result = []; + if (step > 0) { + for (let i = start;i < end; i += step) { + result.push(v.number(i)); + } + } else { + for (let i = start;i > end; i += step) { + result.push(v.number(i)); + } + } + return v.list(result); + }).doc("Returns a finite sequence of numbers (native helper).", [ + ["n"], + ["start", "end"], + ["start", "end", "step"] + ]), + keys: v.nativeFn("keys", function keysImpl(m) { + if (m === undefined || !is.map(m)) { + throw EvaluationError.atArg(`keys expects a map${m !== undefined ? `, got ${printString(m)}` : ""}`, { m }, 0); + } + return v.vector(m.entries.map(function extractKey([k]) { + return k; + })); + }).doc("Returns a vector of the keys of the given map.", [["m"]]), + vals: v.nativeFn("vals", function valsImpl(m) { + if (m === undefined || !is.map(m)) { + throw EvaluationError.atArg(`vals expects a map${m !== undefined ? `, got ${printString(m)}` : ""}`, { m }, 0); + } + return v.vector(m.entries.map(function extractVal([, v2]) { + return v2; + })); + }).doc("Returns a vector of the values of the given map.", [["m"]]), + count: v.nativeFn("count", function countImpl(countable) { + if (countable.kind === "nil") + return v.number(0); + if (is.lazySeq(countable) || is.cons(countable)) { + return v.number(toSeq(countable).length); + } + if (![ + valueKeywords.list, + valueKeywords.vector, + valueKeywords.map, + valueKeywords.set, + valueKeywords.string + ].includes(countable.kind)) { + throw EvaluationError.atArg(`count expects a countable value, got ${printString(countable)}`, { countable }, 0); + } + switch (countable.kind) { + case valueKeywords.list: + return v.number(countable.value.length); + case valueKeywords.vector: + return v.number(countable.value.length); + case valueKeywords.map: + return v.number(countable.entries.length); + case valueKeywords.set: + return v.number(countable.values.length); + case valueKeywords.string: + return v.number(countable.value.length); + default: + throw new EvaluationError(`count expects a countable value, got ${printString(countable)}`, { countable }); + } + }).doc("Returns the number of elements in the given countable value.", [ + ["countable"] + ]), + "hash-set": v.nativeFn("hash-set", function hashSetImpl(...args) { + const deduped = []; + for (const v2 of args) { + if (!deduped.some((existing) => is.equal(existing, v2))) { + deduped.push(v2); + } + } + return v.set(deduped); + }).doc("Returns a set containing the given values.", [["&", "xs"]]), + set: v.nativeFn("set", function setImpl(coll) { + if (coll === undefined || coll.kind === "nil") + return v.set([]); + const items = toSeq(coll); + const deduped = []; + for (const v2 of items) { + if (!deduped.some((existing) => is.equal(existing, v2))) { + deduped.push(v2); + } + } + return v.set(deduped); + }).doc("Returns a set of the distinct elements of the given collection.", [ + ["coll"] + ]), + "set?": v.nativeFn("set?", function setPredicateImpl(x) { + return v.boolean(x !== undefined && x.kind === "set"); + }).doc("Returns true if x is a set.", [["x"]]), + disj: v.nativeFn("disj", function disjImpl(s, ...items) { + if (s === undefined || s.kind === "nil") + return v.set([]); + if (s.kind !== "set") { + throw EvaluationError.atArg(`disj expects a set, got ${printString(s)}`, { s }, 0); + } + const newValues = s.values.filter((v2) => !items.some((item) => is.equal(item, v2))); + return v.set(newValues); + }).doc("Returns a set with the given items removed.", [["s", "&", "items"]]), + vec: v.nativeFn("vec", function vecImpl(coll) { + if (coll === undefined || coll.kind === "nil") + return v.vector([]); + if (is.vector(coll)) + return coll; + if (!is.seqable(coll)) { + throw EvaluationError.atArg(`vec expects a collection or string, got ${printString(coll)}`, { coll }, 0); + } + return v.vector(toSeq(coll)); + }).doc("Creates a new vector containing the contents of coll.", [["coll"]]), + subvec: v.nativeFn("subvec", function subvecImpl(vector, start, end) { + if (vector === undefined || !is.vector(vector)) { + throw EvaluationError.atArg(`subvec expects a vector, got ${printString(vector)}`, { v: vector }, 0); + } + if (start === undefined || start.kind !== "number") { + throw EvaluationError.atArg(`subvec expects a number start index`, { start }, 1); + } + const s = start.value; + const e = end !== undefined && end.kind === "number" ? end.value : vector.value.length; + if (s < 0 || e > vector.value.length || s > e) { + throw new EvaluationError(`subvec index out of bounds: start=${s}, end=${e}, length=${vector.value.length}`, { v: vector, start, end }); + } + return v.vector(vector.value.slice(s, e)); + }).doc("Returns a persistent vector of the items in vector from start (inclusive) to end (exclusive).", [ + ["v", "start"], + ["v", "start", "end"] + ]), + peek: v.nativeFn("peek", function peekImpl(coll) { + if (coll === undefined || coll.kind === "nil") + return v.nil(); + if (is.vector(coll)) { + return coll.value.length === 0 ? v.nil() : coll.value[coll.value.length - 1]; + } + if (is.list(coll)) { + return coll.value.length === 0 ? v.nil() : coll.value[0]; + } + throw EvaluationError.atArg(`peek expects a list or vector, got ${printString(coll)}`, { coll }, 0); + }).doc("For a list, same as first. For a vector, same as last.", [["coll"]]), + pop: v.nativeFn("pop", function popImpl(coll) { + if (coll === undefined || coll.kind === "nil") { + throw EvaluationError.atArg("Can't pop empty list", { coll }, 0); + } + if (is.vector(coll)) { + if (coll.value.length === 0) + throw new EvaluationError("Can't pop empty vector", { coll }); + return v.vector(coll.value.slice(0, -1)); + } + if (is.list(coll)) { + if (coll.value.length === 0) + throw new EvaluationError("Can't pop empty list", { coll }); + return v.list(coll.value.slice(1)); + } + throw EvaluationError.atArg(`pop expects a list or vector, got ${printString(coll)}`, { coll }, 0); + }).doc("For a list, returns a new list without the first item. For a vector, returns a new vector without the last item.", [["coll"]]), + empty: v.nativeFn("empty", function emptyImpl(coll) { + if (coll === undefined || coll.kind === "nil") + return v.nil(); + switch (coll.kind) { + case "list": + return v.list([]); + case "vector": + return v.vector([]); + case "map": + return v.map([]); + case "set": + return v.set([]); + default: + return v.nil(); + } + }).doc("Returns an empty collection of the same category as coll, or nil.", [ + ["coll"] + ]) +}; -;; --------------------------------------------------------------------------- -;; Replacement -;; --------------------------------------------------------------------------- +// src/core/stdlib/errors.ts +var errorFunctions = { + throw: v.nativeFn("throw", function throwImpl(...args) { + if (args.length !== 1) { + throw new EvaluationError(`throw requires exactly 1 argument, got ${args.length}`, { args }); + } + throw new CljThrownSignal(args[0]); + }).doc("Throws a value as an exception. The value may be any CljValue; maps are idiomatic.", [["value"]]), + "ex-info": v.nativeFn("ex-info", function exInfoImpl(...args) { + if (args.length < 2) { + throw new EvaluationError(`ex-info requires at least 2 arguments, got ${args.length}`, { args }); + } + const [msg, data, cause] = args; + if (!is.string(msg)) { + throw new EvaluationError("ex-info: first argument must be a string", { + msg + }); + } + const entries = [ + [v.keyword(":message"), msg], + [v.keyword(":data"), data] + ]; + if (cause !== undefined) { + entries.push([v.keyword(":cause"), cause]); + } + return v.map(entries); + }).doc("Creates an error map with :message and :data keys. Optionally accepts a :cause.", [ + ["msg", "data"], + ["msg", "data", "cause"] + ]), + "ex-message": v.nativeFn("ex-message", function exMessageImpl(...args) { + const [e] = args; + if (!is.map(e)) + return v.nil(); + const entry = e.entries.find(function findMessageKey([k]) { + return is.keyword(k) && k.name === ":message"; + }); + return entry ? entry[1] : v.nil(); + }).doc("Returns the :message of an error map, or nil.", [["e"]]), + "ex-data": v.nativeFn("ex-data", function exDataImpl(...args) { + const [e] = args; + if (!is.map(e)) + return v.nil(); + const entry = e.entries.find(function findDataKey([k]) { + return is.keyword(k) && k.name === ":data"; + }); + return entry ? entry[1] : v.nil(); + }).doc("Returns the :data map of an error map, or nil.", [["e"]]), + "ex-cause": v.nativeFn("ex-cause", function exCauseImpl(...args) { + const [e] = args; + if (!is.map(e)) + return v.nil(); + const entry = e.entries.find(function findCauseKey([k]) { + return is.keyword(k) && k.name === ":cause"; + }); + return entry ? entry[1] : v.nil(); + }).doc("Returns the :cause of an error map, or nil.", [["e"]]) +}; -(defn replace - "Replaces all instances of match with replacement in s. +// src/core/stdlib/hof.ts +var hofFunctions = { + reduce: v.nativeFnCtx("reduce", function reduce(ctx, callEnv, fn, ...rest) { + if (fn === undefined || !is.aFunction(fn)) { + throw EvaluationError.atArg(`reduce expects a function as first argument${fn !== undefined ? `, got ${printString(fn)}` : ""}`, { fn }, 0); + } + if (rest.length === 0 || rest.length > 2) { + throw new EvaluationError("reduce expects 2 or 3 arguments: (reduce f coll) or (reduce f init coll)", { fn }); + } + const hasInit = rest.length === 2; + const init = hasInit ? rest[0] : undefined; + const collection = hasInit ? rest[1] : rest[0]; + if (collection.kind === "nil") { + if (!hasInit) { + throw new EvaluationError("reduce called on empty collection with no initial value", { fn }); + } + return init; + } + if (!is.seqable(collection)) { + throw EvaluationError.atArg(`reduce expects a collection or string, got ${printString(collection)}`, { collection }, rest.length); + } + const items = toSeq(collection); + if (!hasInit) { + if (items.length === 0) { + throw new EvaluationError("reduce called on empty collection with no initial value", { fn }); + } + if (items.length === 1) + return items[0]; + let acc2 = items[0]; + for (let i = 1;i < items.length; i++) { + const result = ctx.applyFunction(fn, [acc2, items[i]], callEnv); + if (is.reduced(result)) + return result.value; + acc2 = result; + } + return acc2; + } + let acc = init; + for (const item of items) { + const result = ctx.applyFunction(fn, [acc, item], callEnv); + if (is.reduced(result)) + return result.value; + acc = result; + } + return acc; + }).doc("Reduces a collection to a single value by iteratively applying f. (reduce f coll) or (reduce f init coll).", [ + ["f", "coll"], + ["f", "val", "coll"] + ]), + apply: v.nativeFnCtx("apply", (ctx, callEnv, fn, ...rest) => { + if (fn === undefined || !is.callable(fn)) { + throw EvaluationError.atArg(`apply expects a callable as first argument${fn !== undefined ? `, got ${printString(fn)}` : ""}`, { fn }, 0); + } + if (rest.length === 0) { + throw new EvaluationError("apply expects at least 2 arguments", { + fn + }); + } + const lastArg = rest[rest.length - 1]; + if (!is.nil(lastArg) && !is.seqable(lastArg)) { + throw EvaluationError.atArg(`apply expects a collection or string as last argument, got ${printString(lastArg)}`, { lastArg }, rest.length); + } + const args = [ + ...rest.slice(0, -1), + ...is.nil(lastArg) ? [] : toSeq(lastArg) + ]; + return ctx.applyCallable(fn, args, callEnv); + }).doc("Calls f with the elements of the last argument (a collection) as its arguments, optionally prepended by fixed args.", [ + ["f", "args"], + ["f", "&", "args"] + ]), + partial: v.nativeFn("partial", (fn, ...preArgs) => { + if (fn === undefined || !is.callable(fn)) { + throw EvaluationError.atArg(`partial expects a callable as first argument${fn !== undefined ? `, got ${printString(fn)}` : ""}`, { fn }, 0); + } + const capturedFn = fn; + return v.nativeFnCtx("partial", (ctx, callEnv, ...moreArgs) => { + return ctx.applyCallable(capturedFn, [...preArgs, ...moreArgs], callEnv); + }); + }).doc("Returns a function that calls f with pre-applied args prepended to any additional arguments.", [["f", "&", "args"]]), + comp: v.nativeFn("comp", (...fns) => { + if (fns.length === 0) { + return v.nativeFn("identity", (x) => x); + } + const badIdx = fns.findIndex((f) => !is.callable(f)); + if (badIdx !== -1) { + throw EvaluationError.atArg("comp expects functions or other callable values (keywords, maps)", { fns }, badIdx); + } + const capturedFns = fns; + return v.nativeFnCtx("composed", (ctx, callEnv, ...args) => { + let result = ctx.applyCallable(capturedFns[capturedFns.length - 1], args, callEnv); + for (let i = capturedFns.length - 2;i >= 0; i--) { + result = ctx.applyCallable(capturedFns[i], [result], callEnv); + } + return result; + }); + }).doc("Returns the composition of fns, applied right-to-left. (comp f g) is equivalent to (fn [x] (f (g x))). Accepts any callable: functions, keywords, and maps.", [[], ["f"], ["f", "g"], ["f", "g", "&", "fns"]]), + identity: v.nativeFn("identity", (x) => { + if (x === undefined) { + throw EvaluationError.atArg("identity expects one argument", {}, 0); + } + return x; + }).doc("Returns its single argument unchanged.", [["x"]]) +}; - match/replacement can be: - string / string — literal match, literal replacement - pattern / string — regex match; $1, $2, etc. substituted from groups - pattern / fn — regex match; fn called with match (string or vector - of [whole g1 g2 ...]), return value used as replacement. +// src/core/stdlib/meta.ts +var metaFunctions = { + meta: v.nativeFn("meta", function metaImpl(val) { + if (val === undefined) { + throw EvaluationError.atArg("meta expects one argument", {}, 0); + } + if (val.kind === "function" || val.kind === "native-function" || val.kind === "var" || val.kind === "list" || val.kind === "vector" || val.kind === "map" || val.kind === "symbol" || val.kind === "atom") { + return val.meta ?? v.nil(); + } + return v.nil(); + }).doc("Returns the metadata map of a value, or nil if the value has no metadata.", [["val"]]), + "with-meta": v.nativeFn("with-meta", function withMetaImpl(val, m) { + if (val === undefined) { + throw EvaluationError.atArg("with-meta expects two arguments", {}, 0); + } + if (m === undefined) { + throw EvaluationError.atArg("with-meta expects two arguments", {}, 1); + } + if (m.kind !== "map" && m.kind !== "nil") { + throw EvaluationError.atArg(`with-meta expects a map as second argument, got ${printString(m)}`, { m }, 1); + } + const metaSupported = val.kind === "function" || val.kind === "native-function" || val.kind === "list" || val.kind === "vector" || val.kind === "map" || val.kind === "symbol"; + if (!metaSupported) { + throw EvaluationError.atArg(`with-meta does not support ${val.kind}, got ${printString(val)}`, { val }, 0); + } + const meta = m.kind === "nil" ? undefined : m; + return { ...val, meta }; + }).doc("Returns a new value with the metadata map m applied to val.", [ + ["val", "m"] + ]), + "alter-meta!": v.nativeFnCtx("alter-meta!", function alterMetaImpl(ctx, callEnv, ref, f, ...args) { + if (ref === undefined) { + throw EvaluationError.atArg("alter-meta! expects at least two arguments", {}, 0); + } + if (f === undefined) { + throw EvaluationError.atArg("alter-meta! expects at least two arguments", {}, 1); + } + if (ref.kind !== "var" && ref.kind !== "atom") { + throw EvaluationError.atArg(`alter-meta! expects a Var or Atom as first argument, got ${ref.kind}`, {}, 0); + } + if (!is.aFunction(f)) { + throw EvaluationError.atArg(`alter-meta! expects a function as second argument, got ${f.kind}`, {}, 1); + } + const currentMeta = ref.meta ?? v.nil(); + const newMeta = ctx.applyCallable(f, [currentMeta, ...args], callEnv); + if (newMeta.kind !== "map" && newMeta.kind !== "nil") { + throw new EvaluationError(`alter-meta! function must return a map or nil, got ${newMeta.kind}`, {}); + } + ref.meta = newMeta.kind === "nil" ? undefined : newMeta; + return newMeta; + }).doc("Applies f to ref's current metadata (with optional args), sets the result as the new metadata, and returns it.", [["ref", "f", "&", "args"]]) +}; - See also replace-first." - [s match replacement] - (str-replace* s match replacement)) +// src/core/stdlib/predicates.ts +var predicateFunctions = { + "nil?": v.nativeFn("nil?", function nilPredImpl(arg) { + return v.boolean(arg.kind === "nil"); + }).doc("Returns true if the value is nil, false otherwise.", [["arg"]]), + "true?": v.nativeFn("true?", function truePredImpl(arg) { + if (arg.kind !== "boolean") { + return v.boolean(false); + } + return v.boolean(arg.value === true); + }).doc("Returns true if the value is a boolean and true, false otherwise.", [ + ["arg"] + ]), + "false?": v.nativeFn("false?", function falsePredImpl(arg) { + if (arg.kind !== "boolean") { + return v.boolean(false); + } + return v.boolean(arg.value === false); + }).doc("Returns true if the value is a boolean and false, false otherwise.", [ + ["arg"] + ]), + "truthy?": v.nativeFn("truthy?", function truthyPredImpl(arg) { + return v.boolean(is.truthy(arg)); + }).doc("Returns true if the value is not nil or false, false otherwise.", [ + ["arg"] + ]), + "falsy?": v.nativeFn("falsy?", function falsyPredImpl(arg) { + return v.boolean(is.falsy(arg)); + }).doc("Returns true if the value is nil or false, false otherwise.", [ + ["arg"] + ]), + "not=": v.nativeFn("not=", function notEqualImpl(...vals) { + if (vals.length < 2) { + throw new EvaluationError("not= expects at least two arguments", { + args: vals + }); + } + for (let i = 1;i < vals.length; i++) { + if (!is.equal(vals[i], vals[i - 1])) { + return v.boolean(true); + } + } + return v.boolean(false); + }).doc("Returns true if any two adjacent arguments are not equal, false otherwise.", [["&", "vals"]]), + "number?": v.nativeFn("number?", function numberPredImpl(x) { + return v.boolean(x !== undefined && x.kind === "number"); + }).doc("Returns true if the value is a number, false otherwise.", [["x"]]), + "string?": v.nativeFn("string?", function stringPredImpl(x) { + return v.boolean(x !== undefined && is.string(x)); + }).doc("Returns true if the value is a string, false otherwise.", [["x"]]), + "boolean?": v.nativeFn("boolean?", function booleanPredImpl(x) { + return v.boolean(x !== undefined && x.kind === "boolean"); + }).doc("Returns true if the value is a boolean, false otherwise.", [["x"]]), + "vector?": v.nativeFn("vector?", function vectorPredImpl(x) { + return v.boolean(x !== undefined && is.vector(x)); + }).doc("Returns true if the value is a vector, false otherwise.", [["x"]]), + "list?": v.nativeFn("list?", function listPredImpl(x) { + return v.boolean(x !== undefined && is.list(x)); + }).doc("Returns true if the value is a list, false otherwise.", [["x"]]), + "map?": v.nativeFn("map?", function mapPredImpl(x) { + return v.boolean(x !== undefined && is.map(x)); + }).doc("Returns true if the value is a map, false otherwise.", [["x"]]), + "keyword?": v.nativeFn("keyword?", function keywordPredImpl(x) { + return v.boolean(x !== undefined && is.keyword(x)); + }).doc("Returns true if the value is a keyword, false otherwise.", [["x"]]), + "qualified-keyword?": v.nativeFn("qualified-keyword?", function qualifiedKeywordPredImpl(x) { + return v.boolean(x !== undefined && is.keyword(x) && x.name.includes("/")); + }).doc("Returns true if the value is a qualified keyword, false otherwise.", [ + ["x"] + ]), + "symbol?": v.nativeFn("symbol?", function symbolPredImpl(x) { + return v.boolean(x !== undefined && is.symbol(x)); + }).doc("Returns true if the value is a symbol, false otherwise.", [["x"]]), + "namespace?": v.nativeFn("namespace?", function namespaceQImpl(x) { + return v.boolean(x !== undefined && x.kind === "namespace"); + }).doc("Returns true if x is a namespace.", [["x"]]), + "qualified-symbol?": v.nativeFn("qualified-symbol?", function qualifiedSymbolPredImpl(x) { + return v.boolean(x !== undefined && is.symbol(x) && x.name.includes("/")); + }).doc("Returns true if the value is a qualified symbol, false otherwise.", [ + ["x"] + ]), + "fn?": v.nativeFn("fn?", function fnPredImpl(x) { + return v.boolean(x !== undefined && is.aFunction(x)); + }).doc("Returns true if the value is a function, false otherwise.", [["x"]]), + "coll?": v.nativeFn("coll?", function collPredImpl(x) { + return v.boolean(x !== undefined && is.collection(x)); + }).doc("Returns true if the value is a collection, false otherwise.", [ + ["x"] + ]), + some: v.nativeFnCtx("some", function someImpl(ctx, callEnv, pred, coll) { + if (pred === undefined || !is.aFunction(pred)) { + throw EvaluationError.atArg(`some expects a function as first argument${pred !== undefined ? `, got ${printString(pred)}` : ""}`, { pred }, 0); + } + if (coll === undefined) { + return v.nil(); + } + if (!is.seqable(coll)) { + throw EvaluationError.atArg(`some expects a collection or string as second argument, got ${printString(coll)}`, { coll }, 1); + } + for (const item of toSeq(coll)) { + const result = ctx.applyFunction(pred, [item], callEnv); + if (is.truthy(result)) { + return result; + } + } + return v.nil(); + }).doc("Returns the first truthy result of applying pred to each item in coll, or nil if no item satisfies pred.", [["pred", "coll"]]), + "every?": v.nativeFnCtx("every?", function everyPredImpl(ctx, callEnv, pred, coll) { + if (pred === undefined || !is.aFunction(pred)) { + throw EvaluationError.atArg(`every? expects a function as first argument${pred !== undefined ? `, got ${printString(pred)}` : ""}`, { pred }, 0); + } + if (coll === undefined || !is.seqable(coll)) { + throw EvaluationError.atArg(`every? expects a collection or string as second argument${coll !== undefined ? `, got ${printString(coll)}` : ""}`, { coll }, 1); + } + for (const item of toSeq(coll)) { + if (is.falsy(ctx.applyFunction(pred, [item], callEnv))) { + return v.boolean(false); + } + } + return v.boolean(true); + }).doc("Returns true if all items in coll satisfy pred, false otherwise.", [ + ["pred", "coll"] + ]), + "identical?": v.nativeFn("identical?", function identicalPredImpl(x, y) { + return v.boolean(x === y); + }).doc("Tests if 2 arguments are the same object (reference equality).", [ + ["x", "y"] + ]), + "seqable?": v.nativeFn("seqable?", function seqablePredImpl(x) { + return v.boolean(x !== undefined && is.seqable(x)); + }).doc("Return true if the seq function is supported for x.", [["x"]]), + "sequential?": v.nativeFn("sequential?", function sequentialPredImpl(x) { + return v.boolean(x !== undefined && (is.list(x) || is.vector(x))); + }).doc("Returns true if coll is a sequential collection (list or vector).", [ + ["coll"] + ]), + "associative?": v.nativeFn("associative?", function associativePredImpl(x) { + return v.boolean(x !== undefined && (is.map(x) || is.vector(x))); + }).doc("Returns true if coll implements Associative (map or vector).", [ + ["coll"] + ]), + "counted?": v.nativeFn("counted?", function countedPredImpl(x) { + return v.boolean(x !== undefined && (is.list(x) || is.vector(x) || is.map(x) || x.kind === "set" || is.string(x))); + }).doc("Returns true if coll implements count in constant time.", [["coll"]]), + "int?": v.nativeFn("int?", function intPredImpl(x) { + return v.boolean(x !== undefined && x.kind === "number" && Number.isInteger(x.value)); + }).doc("Return true if x is a fixed precision integer.", [["x"]]), + "double?": v.nativeFn("double?", function doublePredImpl(x) { + return v.boolean(x !== undefined && x.kind === "number"); + }).doc("Return true if x is a Double (all numbers in JS are doubles).", [ + ["x"] + ]), + "NaN?": v.nativeFn("NaN?", function nanPredImpl(x) { + return v.boolean(x !== undefined && x.kind === "number" && isNaN(x.value)); + }).doc("Returns true if num is NaN, else false.", [["num"]]), + "infinite?": v.nativeFn("infinite?", function infinitePredImpl(x) { + return v.boolean(x !== undefined && x.kind === "number" && !isFinite(x.value)); + }).doc("Returns true if num is positive or negative infinity, else false.", [ + ["num"] + ]), + compare: v.nativeFn("compare", function compareImpl(x, y) { + if (is.nil(x) && is.nil(y)) + return v.number(0); + if (is.nil(x)) + return v.number(-1); + if (is.nil(y)) + return v.number(1); + if (is.number(x) && is.number(y)) { + return v.number(x.value < y.value ? -1 : x.value > y.value ? 1 : 0); + } + if (is.string(x) && is.string(y)) { + return v.number(x.value < y.value ? -1 : x.value > y.value ? 1 : 0); + } + if (is.keyword(x) && is.keyword(y)) { + return v.number(x.name < y.name ? -1 : x.name > y.name ? 1 : 0); + } + throw new EvaluationError(`compare: cannot compare ${printString(x)} to ${printString(y)}`, { x, y }); + }).doc("Comparator. Returns a negative number, zero, or a positive number.", [ + ["x", "y"] + ]), + hash: v.nativeFn("hash", function hashImpl(x) { + const s = printString(x); + let h = 0; + for (let i = 0;i < s.length; i++) { + h = Math.imul(31, h) + s.charCodeAt(i) | 0; + } + return v.number(h); + }).doc("Returns the hash code of its argument.", [["x"]]) +}; -(defn replace-first - "Replaces the first instance of match with replacement in s. - Same match/replacement semantics as replace." - [s match replacement] - (str-replace-first* s match replacement)) +// src/core/stdlib/regex.ts +function extractInlineFlags2(raw) { + let remaining = raw; + let flags = ""; + const flagGroupRe = /^\(\?([imsx]+)\)/; + let m; + while ((m = flagGroupRe.exec(remaining)) !== null) { + for (const f of m[1]) { + if (f === "x") { + throw new EvaluationError("Regex flag (?x) (verbose mode) has no JavaScript equivalent and is not supported", {}); + } + if (!flags.includes(f)) + flags += f; + } + remaining = remaining.slice(m[0].length); + } + return { pattern: remaining, flags }; +} +function assertRegex(val, fnName) { + if (!is.regex(val)) { + throw new EvaluationError(`${fnName} expects a regex as first argument, got ${printString(val)}`, { val }); + } + return val; +} +function assertStringArg(val, fnName) { + if (val.kind !== "string") { + throw new EvaluationError(`${fnName} expects a string as second argument, got ${printString(val)}`, { val }); + } + return val.value; +} +function matchToClj(match) { + if (match.length === 1) + return v.string(match[0]); + return v.vector(match.map(function mapMatchToClj(m) { + return m == null ? v.nil() : v.string(m); + })); +} +var regexFunctions = { + "regexp?": v.nativeFn("regexp?", function regexpPredImpl(x) { + return v.boolean(x !== undefined && is.regex(x)); + }).doc("Returns true if x is a regular expression pattern.", [["x"]]), + "re-pattern": v.nativeFn("re-pattern", function rePatternImpl(s) { + if (s === undefined || s.kind !== "string") { + throw new EvaluationError(`re-pattern expects a string argument${s !== undefined ? `, got ${printString(s)}` : ""}`, { s }); + } + const { pattern, flags } = extractInlineFlags2(s.value); + return v.regex(pattern, flags); + }).doc(`Returns an instance of java.util.regex.Pattern, for use, e.g. in re-matcher. + (re-pattern "\\\\d+") produces the same pattern as #"\\d+".`, [["s"]]), + "re-find": v.nativeFn("re-find", function reFindImpl(reVal, sVal) { + const re = assertRegex(reVal, "re-find"); + const s = assertStringArg(sVal, "re-find"); + const jsRe = new RegExp(re.pattern, re.flags); + const match = jsRe.exec(s); + if (!match) + return v.nil(); + return matchToClj(match); + }).doc(`Returns the next regex match, if any, of string to pattern, using + java.util.regex.Matcher.find(). Returns the match or nil. When there + are groups, returns a vector of the whole match and groups (nil for + unmatched optional groups).`, [["re", "s"]]), + "re-matches": v.nativeFn("re-matches", function reMatchesImpl(reVal, sVal) { + const re = assertRegex(reVal, "re-matches"); + const s = assertStringArg(sVal, "re-matches"); + const jsRe = new RegExp(re.pattern, re.flags); + const match = jsRe.exec(s); + if (!match || match.index !== 0 || match[0].length !== s.length) { + return v.nil(); + } + return matchToClj(match); + }).doc(`Returns the match, if any, of string to pattern, using + java.util.regex.Matcher.matches(). The entire string must match. + Returns the match or nil. When there are groups, returns a vector + of the whole match and groups (nil for unmatched optional groups).`, [["re", "s"]]), + "re-seq": v.nativeFn("re-seq", function reSeqImpl(reVal, sVal) { + const re = assertRegex(reVal, "re-seq"); + const s = assertStringArg(sVal, "re-seq"); + const jsRe = new RegExp(re.pattern, re.flags + "g"); + const results = []; + let match; + while ((match = jsRe.exec(s)) !== null) { + if (match[0].length === 0) { + jsRe.lastIndex++; + continue; + } + results.push(matchToClj(match)); + } + if (results.length === 0) + return v.nil(); + return { kind: "list", value: results }; + }).doc(`Returns a lazy sequence of successive matches of pattern in string, + using java.util.regex.Matcher.find(), each such match processed with + re-groups.`, [["re", "s"]]), + "str-split*": v.nativeFn("str-split*", function strSplitImpl(sVal, sepVal, limitVal) { + if (sVal === undefined || sVal.kind !== "string") { + throw new EvaluationError(`str-split* expects a string as first argument${sVal !== undefined ? `, got ${printString(sVal)}` : ""}`, { sVal }); + } + const s = sVal.value; + const hasLimit = limitVal !== undefined && limitVal.kind !== "nil"; + const limit = hasLimit && limitVal.kind === "number" ? limitVal.value : undefined; + let jsPattern; + let jsFlags; + if (sepVal.kind !== "regex") { + throw new EvaluationError(`str-split* expects a regex pattern as second argument, got ${printString(sepVal)}`, { sepVal }); + } + if (sepVal.pattern === "") { + const chars = [...s]; + if (limit === undefined || limit >= chars.length) { + return v.vector(chars.map(v.string)); + } + const parts = [ + ...chars.slice(0, limit - 1), + chars.slice(limit - 1).join("") + ]; + return v.vector(parts.map(function mapPartToString(p) { + return v.string(p); + })); + } + jsPattern = sepVal.pattern; + jsFlags = sepVal.flags; + const re = new RegExp(jsPattern, jsFlags + "g"); + const rawParts = splitWithRegex(s, re, limit); + return v.vector(rawParts.map(function mapRawPartToString(p) { + return v.string(p); + })); + }).doc(`Internal helper for clojure.string/split. Splits string s by a regex or + string separator. Optional limit keeps all parts when provided.`, [ + ["s", "sep"], + ["s", "sep", "limit"] + ]) +}; +function splitWithRegex(s, re, limit) { + const parts = []; + let lastIndex = 0; + let match; + let count = 0; + while ((match = re.exec(s)) !== null) { + if (match[0].length === 0) { + re.lastIndex++; + continue; + } + if (limit !== undefined && count >= limit - 1) + break; + parts.push(s.slice(lastIndex, match.index)); + lastIndex = match.index + match[0].length; + count++; + } + parts.push(s.slice(lastIndex)); + if (limit === undefined) { + while (parts.length > 0 && parts[parts.length - 1] === "") { + parts.pop(); + } + } + return parts; +} -(defn re-quote-replacement - "Given a replacement string that you wish to be a literal replacement for a - pattern match in replace or replace-first, escape any special replacement - characters ($ signs) so they are treated literally." - [s] - (replace s #"\\$" "$$$$")) +// src/core/stdlib/strings.ts +function assertStr(val, fnName) { + if (val === undefined || val.kind !== "string") { + throw new EvaluationError(`${fnName} expects a string as first argument${val !== undefined ? `, got ${printString(val)}` : ""}`, { val }); + } + return val.value; +} +function assertStrArg(val, pos, fnName) { + if (val === undefined || val.kind !== "string") { + throw new EvaluationError(`${fnName} expects a string as ${pos} argument${val !== undefined ? `, got ${printString(val)}` : ""}`, { val }); + } + return val.value; +} +function escapeRegex(s) { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} +function escapeDollarInReplacement(s) { + return s.replace(/\$/g, "$$$$"); +} +function buildMatchValue(whole, args) { + let offsetIdx = -1; + for (let i = args.length - 1;i >= 0; i--) { + if (typeof args[i] === "number") { + offsetIdx = i; + break; + } + } + const groups = offsetIdx > 0 ? args.slice(0, offsetIdx) : []; + if (groups.length === 0) + return v.string(whole); + return v.vector([ + v.string(whole), + ...groups.map(function mapGroupToClj(g) { + return g == null ? v.nil() : v.string(String(g)); + }) + ]); +} +function doReplace(ctx, callEnv, fnName, sVal, matchVal, replVal, global) { + const s = assertStr(sVal, fnName); + if (matchVal === undefined || replVal === undefined) { + throw new EvaluationError(`${fnName} expects 3 arguments`, {}); + } + if (matchVal.kind === "string") { + if (replVal.kind !== "string") { + throw new EvaluationError(`${fnName}: when match is a string, replacement must also be a string, got ${printString(replVal)}`, { replVal }); + } + const re = new RegExp(escapeRegex(matchVal.value), global ? "g" : ""); + return v.string(s.replace(re, escapeDollarInReplacement(replVal.value))); + } + if (matchVal.kind === "regex") { + const re = matchVal; + const flags = global ? re.flags + "g" : re.flags; + const jsRe = new RegExp(re.pattern, flags); + if (replVal.kind === "string") { + return v.string(s.replace(jsRe, replVal.value)); + } + if (is.aFunction(replVal)) { + const fn = replVal; + const result = s.replace(jsRe, function replaceCallback(whole, ...args) { + const matchClj = buildMatchValue(whole, args); + const replResult = ctx.applyFunction(fn, [matchClj], callEnv); + return valueToString(replResult); + }); + return v.string(result); + } + throw new EvaluationError(`${fnName}: replacement must be a string or function, got ${printString(replVal)}`, { replVal }); + } + throw new EvaluationError(`${fnName}: match must be a string or regex, got ${printString(matchVal)}`, { matchVal }); +} +var stringFunctions = { + "str-upper-case*": v.nativeFn("str-upper-case*", function strUpperCaseImpl(sVal) { + return v.string(assertStr(sVal, "str-upper-case*").toUpperCase()); + }).doc("Internal helper. Converts s to upper-case.", [["s"]]), + "str-lower-case*": v.nativeFn("str-lower-case*", function strLowerCaseImpl(sVal) { + return v.string(assertStr(sVal, "str-lower-case*").toLowerCase()); + }).doc("Internal helper. Converts s to lower-case.", [["s"]]), + "str-trim*": v.nativeFn("str-trim*", function strTrimImpl(sVal) { + return v.string(assertStr(sVal, "str-trim*").trim()); + }).doc("Internal helper. Removes whitespace from both ends of s.", [["s"]]), + "str-triml*": v.nativeFn("str-triml*", function strTrimlImpl(sVal) { + return v.string(assertStr(sVal, "str-triml*").trimStart()); + }).doc("Internal helper. Removes whitespace from the left of s.", [["s"]]), + "str-trimr*": v.nativeFn("str-trimr*", function strTrimrImpl(sVal) { + return v.string(assertStr(sVal, "str-trimr*").trimEnd()); + }).doc("Internal helper. Removes whitespace from the right of s.", [["s"]]), + "str-reverse*": v.nativeFn("str-reverse*", function strReverseImpl(sVal) { + return v.string([...assertStr(sVal, "str-reverse*")].reverse().join("")); + }).doc("Internal helper. Returns s with its characters reversed (Unicode-safe).", [["s"]]), + "str-starts-with*": v.nativeFn("str-starts-with*", function strStartsWithImpl(sVal, substrVal) { + const s = assertStr(sVal, "str-starts-with*"); + const substr = assertStrArg(substrVal, "second", "str-starts-with*"); + return v.boolean(s.startsWith(substr)); + }).doc("Internal helper. Returns true if s starts with substr.", [ + ["s", "substr"] + ]), + "str-ends-with*": v.nativeFn("str-ends-with*", function strEndsWithImpl(sVal, substrVal) { + const s = assertStr(sVal, "str-ends-with*"); + const substr = assertStrArg(substrVal, "second", "str-ends-with*"); + return v.boolean(s.endsWith(substr)); + }).doc("Internal helper. Returns true if s ends with substr.", [ + ["s", "substr"] + ]), + "str-includes*": v.nativeFn("str-includes*", function strIncludesImpl(sVal, substrVal) { + const s = assertStr(sVal, "str-includes*"); + const substr = assertStrArg(substrVal, "second", "str-includes*"); + return v.boolean(s.includes(substr)); + }).doc("Internal helper. Returns true if s contains substr.", [ + ["s", "substr"] + ]), + "str-index-of*": v.nativeFn("str-index-of*", function strIndexOfImpl(sVal, valVal, fromVal) { + const s = assertStr(sVal, "str-index-of*"); + const needle = assertStrArg(valVal, "second", "str-index-of*"); + let idx; + if (fromVal !== undefined && fromVal.kind !== "nil") { + if (fromVal.kind !== "number") { + throw new EvaluationError(`str-index-of* expects a number as third argument, got ${printString(fromVal)}`, { fromVal }); + } + idx = s.indexOf(needle, fromVal.value); + } else { + idx = s.indexOf(needle); + } + return idx === -1 ? v.nil() : v.number(idx); + }).doc("Internal helper. Returns index of value in s, or nil if not found.", [ + ["s", "value"], + ["s", "value", "from-index"] + ]), + "str-last-index-of*": v.nativeFn("str-last-index-of*", function strLastIndexOfImpl(sVal, valVal, fromVal) { + const s = assertStr(sVal, "str-last-index-of*"); + const needle = assertStrArg(valVal, "second", "str-last-index-of*"); + let idx; + if (fromVal !== undefined && fromVal.kind !== "nil") { + if (fromVal.kind !== "number") { + throw new EvaluationError(`str-last-index-of* expects a number as third argument, got ${printString(fromVal)}`, { fromVal }); + } + idx = s.lastIndexOf(needle, fromVal.value); + } else { + idx = s.lastIndexOf(needle); + } + return idx === -1 ? v.nil() : v.number(idx); + }).doc("Internal helper. Returns last index of value in s, or nil if not found.", [ + ["s", "value"], + ["s", "value", "from-index"] + ]), + "str-replace*": v.nativeFnCtx("str-replace*", function strReplaceImpl(ctx, callEnv, sVal, matchVal, replVal) { + return doReplace(ctx, callEnv, "str-replace*", sVal, matchVal, replVal, true); + }).doc("Internal helper. Replaces all occurrences of match with replacement in s.", [["s", "match", "replacement"]]), + "str-replace-first*": v.nativeFnCtx("str-replace-first*", function strReplaceFirstImpl(ctx, callEnv, sVal, matchVal, replVal) { + return doReplace(ctx, callEnv, "str-replace-first*", sVal, matchVal, replVal, false); + }).doc("Internal helper. Replaces the first occurrence of match with replacement in s.", [["s", "match", "replacement"]]) +}; -;; --------------------------------------------------------------------------- -;; Miscellaneous -;; --------------------------------------------------------------------------- +// src/core/stdlib/transducers.ts +var transducerFunctions = { + reduced: v.nativeFn("reduced", function reducedImpl(value) { + if (value === undefined) { + throw new EvaluationError("reduced expects one argument", {}); + } + return v.reduced(value); + }).doc("Returns a reduced value, indicating termination of the reduction process.", [["value"]]), + "reduced?": v.nativeFn("reduced?", function isReducedImpl(value) { + if (value === undefined) { + throw new EvaluationError("reduced? expects one argument", {}); + } + return v.boolean(is.reduced(value)); + }).doc("Returns true if the given value is a reduced value, false otherwise.", [["value"]]), + unreduced: v.nativeFn("unreduced", function unreducedImpl(value) { + if (value === undefined) { + throw new EvaluationError("unreduced expects one argument", {}); + } + return is.reduced(value) ? value.value : value; + }).doc("Returns the unreduced value of the given value. If the value is not a reduced value, it is returned unchanged.", [["value"]]), + "ensure-reduced": v.nativeFn("ensure-reduced", function ensureReducedImpl(value) { + if (value === undefined) { + throw new EvaluationError("ensure-reduced expects one argument", {}); + } + return is.reduced(value) ? value : v.reduced(value); + }).doc("Returns the given value if it is a reduced value, otherwise returns a reduced value with the given value as its value.", [["value"]]), + "volatile!": v.nativeFn("volatile!", function volatileImpl(value) { + if (value === undefined) { + throw new EvaluationError("volatile! expects one argument", {}); + } + return v.volatile(value); + }).doc("Returns a volatile value with the given value as its value.", [ + ["value"] + ]), + "volatile?": v.nativeFn("volatile?", function isVolatileImpl(value) { + if (value === undefined) { + throw new EvaluationError("volatile? expects one argument", {}); + } + return v.boolean(is.volatile(value)); + }).doc("Returns true if the given value is a volatile value, false otherwise.", [["value"]]), + "vreset!": v.nativeFn("vreset!", function vresetImpl(vol, newVal) { + if (!is.volatile(vol)) { + throw new EvaluationError(`vreset! expects a volatile as its first argument, got ${printString(vol)}`, { vol }); + } + if (newVal === undefined) { + throw new EvaluationError("vreset! expects two arguments", { vol }); + } + vol.value = newVal; + return newVal; + }).doc("Resets the value of the given volatile to the given new value and returns the new value.", [["vol", "newVal"]]), + "vswap!": v.nativeFnCtx("vswap!", function vswapImpl(ctx, callEnv, vol, fn, ...extraArgs) { + if (!is.volatile(vol)) { + throw new EvaluationError(`vswap! expects a volatile as its first argument, got ${printString(vol)}`, { vol }); + } + if (!is.aFunction(fn)) { + throw new EvaluationError(`vswap! expects a function as its second argument, got ${printString(fn)}`, { fn }); + } + const newVal = ctx.applyFunction(fn, [vol.value, ...extraArgs], callEnv); + vol.value = newVal; + return newVal; + }).doc("Applies fn to the current value of the volatile, replacing the current value with the result. Returns the new value.", [ + ["vol", "fn"], + ["vol", "fn", "&", "extraArgs"] + ]), + transduce: v.nativeFnCtx("transduce", function transduceImpl(ctx, callEnv, xform, f, init, coll) { + if (!is.aFunction(xform)) { + throw new EvaluationError(`transduce expects a transducer (function) as first argument, got ${printString(xform)}`, { xf: xform }); + } + if (!is.aFunction(f)) { + throw new EvaluationError(`transduce expects a reducing function as second argument, got ${printString(f)}`, { f }); + } + if (init === undefined) { + throw new EvaluationError("transduce expects 3 or 4 arguments: (transduce xf f coll) or (transduce xf f init coll)", {}); + } + let actualInit; + let actualColl; + if (coll === undefined) { + actualColl = init; + actualInit = ctx.applyFunction(f, [], callEnv); + } else { + actualInit = init; + actualColl = coll; + } + const rf = ctx.applyFunction(xform, [f], callEnv); + if (is.nil(actualColl)) { + return ctx.applyFunction(rf, [actualInit], callEnv); + } + if (!is.seqable(actualColl)) { + throw new EvaluationError(`transduce expects a collection or string as ${coll === undefined ? "third" : "fourth"} argument, got ${printString(actualColl)}`, { coll: actualColl }); + } + const items = toSeq(actualColl); + let acc = actualInit; + for (const item of items) { + const result = ctx.applyFunction(rf, [acc, item], callEnv); + if (is.reduced(result)) { + acc = result.value; + break; + } + acc = result; + } + return ctx.applyFunction(rf, [acc], callEnv); + }).doc(joinLines([ + "reduce with a transformation of f (xf). If init is not", + "supplied, (f) will be called to produce it. f should be a reducing", + "step function that accepts both 1 and 2 arguments, if it accepts", + "only 2 you can add the arity-1 with 'completing'. Returns the result", + "of applying (the transformed) xf to init and the first item in coll,", + "then applying xf to that result and the 2nd item, etc. If coll", + "contains no items, returns init and f is not called. Note that", + "certain transforms may inject or skip items." + ]), [ + ["xform", "f", "coll"], + ["xform", "f", "init", "coll"] + ]) +}; -(defn reverse - "Returns s with its characters reversed." - [s] - (str-reverse* s)) +// src/core/stdlib/utils.ts +var utilFunctions = { + str: v.nativeFn("str", function strImpl(...args) { + return v.string(args.map((v2) => v2.kind === "nil" ? "" : valueToString(v2)).join("")); + }).doc("Returns a concatenated string representation of the given values.", [ + ["&", "args"] + ]), + subs: v.nativeFn("subs", function subsImpl(s, start, end) { + if (s === undefined || s.kind !== "string") { + throw EvaluationError.atArg(`subs expects a string as first argument${s !== undefined ? `, got ${printString(s)}` : ""}`, { s }, 0); + } + if (start === undefined || start.kind !== "number") { + throw EvaluationError.atArg(`subs expects a number as second argument${start !== undefined ? `, got ${printString(start)}` : ""}`, { start }, 1); + } + if (end !== undefined && end.kind !== "number") { + throw EvaluationError.atArg(`subs expects a number as optional third argument${end !== undefined ? `, got ${printString(end)}` : ""}`, { end }, 2); + } + const from = start.value; + const to = end?.value; + return v.string(to === undefined ? s.value.slice(from) : s.value.slice(from, to)); + }).doc("Returns the substring of s beginning at start, and optionally ending before end.", [ + ["s", "start"], + ["s", "start", "end"] + ]), + type: v.nativeFn("type", function typeImpl(x) { + if (x === undefined) { + throw new EvaluationError("type expects an argument", { x }); + } + const kindToKeyword = { + number: ":number", + string: ":string", + boolean: ":boolean", + nil: ":nil", + keyword: ":keyword", + symbol: ":symbol", + list: ":list", + vector: ":vector", + map: ":map", + function: ":function", + regex: ":regex", + var: ":var", + "native-function": ":function" + }; + const name = kindToKeyword[x.kind]; + if (!name) { + throw new EvaluationError(`type: unhandled kind ${x.kind}`, { x }); + } + return v.keyword(name); + }).doc("Returns a keyword representing the type of the given value.", [ + ["x"] + ]), + gensym: v.nativeFn("gensym", function gensymImpl(...args) { + if (args.length > 1) { + throw new EvaluationError("gensym takes 0 or 1 arguments", { args }); + } + const prefix = args[0]; + if (prefix !== undefined && prefix.kind !== "string") { + throw EvaluationError.atArg(`gensym prefix must be a string${prefix !== undefined ? `, got ${printString(prefix)}` : ""}`, { prefix }, 0); + } + const p = prefix?.kind === "string" ? prefix.value : "G"; + return v.symbol(makeGensym(p)); + }).doc('Returns a unique symbol with the given prefix. Defaults to "G" if no prefix is provided.', [[], ["prefix"]]), + eval: v.nativeFnCtx("eval", function evalImpl(ctx, callEnv, form) { + if (form === undefined) { + throw new EvaluationError("eval expects a form as argument", { + form + }); + } + const expanded = ctx.expandAll(form, callEnv); + return ctx.evaluate(expanded, callEnv); + }).doc("Evaluates the given form in the global environment and returns the result.", [["form"]]), + "macroexpand-1": v.nativeFnCtx("macroexpand-1", function macroexpand1Impl(ctx, callEnv, form) { + if (!is.list(form) || form.value.length === 0) + return form; + const head = form.value[0]; + if (!is.symbol(head)) + return form; + const macroValue = tryLookup(head.name, callEnv); + if (macroValue === undefined) + return form; + if (!is.macro(macroValue)) + return form; + return ctx.applyMacro(macroValue, form.value.slice(1)); + }).doc("If the head of the form is a macro, expands it and returns the resulting forms. Otherwise, returns the form unchanged.", [["form"]]), + macroexpand: v.nativeFnCtx("macroexpand", function macroexpandImpl(ctx, callEnv, form) { + let current = form; + while (true) { + if (!is.list(current) || current.value.length === 0) + return current; + const head = current.value[0]; + if (!is.symbol(head)) + return current; + const macroValue = tryLookup(head.name, callEnv); + if (macroValue === undefined) + return current; + if (!is.macro(macroValue)) + return current; + current = ctx.applyMacro(macroValue, current.value.slice(1)); + } + }).doc(joinLines([ + "Expands all macros until the expansion is stable (head is no longer a macro)", + "", + "Note neither macroexpand-1 nor macroexpand will expand macros in sub-forms" + ]), [["form"]]), + "macroexpand-all": v.nativeFnCtx("macroexpand-all", function macroexpandAllImpl(ctx, callEnv, form) { + return ctx.expandAll(form, callEnv); + }).doc(joinLines([ + "Fully expands all macros in a form recursively — including in sub-forms.", + "", + "Unlike macroexpand, this descends into every sub-expression.", + "Expansion stops at quote/quasiquote boundaries and fn/loop bodies." + ]), [["form"]]), + namespace: v.nativeFn("namespace", function namespaceImpl(x) { + if (x === undefined) { + throw EvaluationError.atArg("namespace expects an argument", { x }, 0); + } + let raw; + if (is.keyword(x)) { + raw = x.name.slice(1); + } else if (is.symbol(x)) { + raw = x.name; + } else { + throw EvaluationError.atArg(`namespace expects a keyword or symbol, got ${printString(x)}`, { x }, 0); + } + const slashIdx = raw.indexOf("/"); + if (slashIdx <= 0) + return v.nil(); + return v.string(raw.slice(0, slashIdx)); + }).doc("Returns the namespace string of a qualified keyword or symbol, or nil if the argument is not qualified.", [["x"]]), + name: v.nativeFn("name", function nameImpl(x) { + if (x === undefined) { + throw EvaluationError.atArg("name expects an argument", { x }, 0); + } + let raw; + if (is.keyword(x)) { + raw = x.name.slice(1); + } else if (is.symbol(x)) { + raw = x.name; + } else if (x.kind === "string") { + return x; + } else { + throw EvaluationError.atArg(`name expects a keyword, symbol, or string, got ${printString(x)}`, { x }, 0); + } + const slashIdx = raw.indexOf("/"); + return v.string(slashIdx >= 0 ? raw.slice(slashIdx + 1) : raw); + }).doc("Returns the local name of a qualified keyword or symbol, or the string value if the argument is a string.", [["x"]]), + keyword: v.nativeFn("keyword", function keywordImpl(...args) { + if (args.length === 0 || args.length > 2) { + throw new EvaluationError("keyword expects 1 or 2 string arguments", { + args + }); + } + if (args[0].kind !== "string") { + throw EvaluationError.atArg(`keyword expects a string, got ${printString(args[0])}`, { args }, 0); + } + if (args.length === 1) { + return v.keyword(`:${args[0].value}`); + } + if (args[1].kind !== "string") { + throw EvaluationError.atArg(`keyword second argument must be a string, got ${printString(args[1])}`, { args }, 1); + } + return v.keyword(`:${args[0].value}/${args[1].value}`); + }).doc(joinLines([ + "Constructs a keyword with the given name and namespace strings. Returns a keyword value.", + "", + "Note: do not use : in the keyword strings, it will be added automatically.", + 'e.g. (keyword "foo") => :foo' + ]), [["name"], ["ns", "name"]]), + boolean: v.nativeFn("boolean", function booleanImpl(x) { + if (x === undefined) + return v.boolean(false); + return v.boolean(is.truthy(x)); + }).doc("Coerces to boolean. Everything is true except false and nil.", [ + ["x"] + ]), + "clojure-version": v.nativeFn("clojure-version", function clojureVersionImpl() { + return v.string("1.12.0"); + }).doc("Returns a string describing the current Clojure version.", [[]]), + "pr-str": v.nativeFn("pr-str", function prStrImpl(...args) { + return v.string(args.map(printString).join(" ")); + }).doc("Returns a readable string representation of the given values (strings are quoted).", [["&", "args"]]), + "pretty-print-str": v.nativeFn("pretty-print-str", function prettyPrintStrImpl(...args) { + if (args.length === 0) + return v.string(""); + const form = args[0]; + const widthArg = args[1]; + const maxWidth = widthArg !== undefined && widthArg.kind === "number" ? widthArg.value : 80; + return v.string(prettyPrintString(form, maxWidth)); + }).doc("Returns a pretty-printed string representation of form.", [ + ["form"], + ["form", "max-width"] + ]), + "read-string": v.nativeFn("read-string", function readStringImpl(s) { + if (s === undefined || s.kind !== "string") { + throw EvaluationError.atArg(`read-string expects a string${s !== undefined ? `, got ${printString(s)}` : ""}`, { s }, 0); + } + const tokens = tokenize(s.value); + const forms = readForms(tokens); + if (forms.length === 0) + return v.nil(); + return forms[0]; + }).doc("Reads one object from the string s. Returns nil if string is empty.", [["s"]]), + "prn-str": v.nativeFn("prn-str", function prnStrImpl(...args) { + return v.string(args.map(printString).join(" ") + ` +`); + }).doc("pr-str to a string, followed by a newline.", [["&", "args"]]), + "print-str": v.nativeFn("print-str", function printStrImpl(...args) { + return v.string(args.map(valueToString).join(" ")); + }).doc("print to a string (human-readable, no quotes on strings).", [ + ["&", "args"] + ]), + "println-str": v.nativeFn("println-str", function printlnStrImpl(...args) { + return v.string(args.map(valueToString).join(" ") + ` +`); + }).doc("println to a string.", [["&", "args"]]), + symbol: v.nativeFn("symbol", function symbolImpl(...args) { + if (args.length === 0 || args.length > 2) { + throw new EvaluationError("symbol expects 1 or 2 string arguments", { + args + }); + } + if (args.length === 1) { + if (is.symbol(args[0])) + return args[0]; + if (args[0].kind !== "string") { + throw EvaluationError.atArg(`symbol expects a string, got ${printString(args[0])}`, { args }, 0); + } + return v.symbol(args[0].value); + } + if (args[0].kind !== "string" || args[1].kind !== "string") { + throw new EvaluationError("symbol expects string arguments", { args }); + } + return v.symbol(`${args[0].value}/${args[1].value}`); + }).doc("Returns a Symbol with the given namespace and name.", [ + ["name"], + ["ns", "name"] + ]) +}; -(defn escape - "Return a new string, using cmap to escape each character ch from s as - follows: if (cmap ch) is nil, append ch to the new string; otherwise append - (str (cmap ch)). +// src/core/stdlib/lazy.ts +var lazyFunctions = { + force: v.nativeFn("force", function force(value) { + if (is.delay(value)) + return realizeDelay(value); + if (is.lazySeq(value)) + return realizeLazySeq(value); + return value; + }).doc("If x is a Delay or LazySeq, forces and returns the realized value. Otherwise returns x.", [["x"]]), + "delay?": v.nativeFn("delay?", function isDelayPred(value) { + return v.boolean(is.delay(value)); + }).doc("Returns true if x is a Delay.", [["x"]]), + "lazy-seq?": v.nativeFn("lazy-seq?", function isLazySeqPred(value) { + return v.boolean(is.lazySeq(value)); + }).doc("Returns true if x is a LazySeq.", [["x"]]), + "realized?": v.nativeFn("realized?", function isRealized(value) { + if (is.delay(value)) + return v.boolean(value.realized); + if (is.lazySeq(value)) + return v.boolean(value.realized); + return v.boolean(false); + }).doc("Returns true if a Delay or LazySeq has been realized.", [["x"]]) +}; - cmap may be a map or a function. Maps are callable directly (IFn semantics). +// src/core/stdlib/vars.ts +var varFunctions = { + "var?": v.nativeFn("var?", function isVarImpl(x) { + return v.boolean(is.var(x)); + }).doc("Returns true if x is a Var.", [["x"]]), + "var-get": v.nativeFn("var-get", function varGetImpl(x) { + if (!is.var(x)) { + throw new EvaluationError(`var-get expects a Var, got ${x.kind}`, { x }); + } + return x.value; + }).doc("Returns the value in the Var object.", [["x"]]), + "alter-var-root": v.nativeFnCtx("alter-var-root", function alterVarRootImpl(ctx, callEnv, varVal, f, ...args) { + if (!is.var(varVal)) { + throw new EvaluationError(`alter-var-root expects a Var as its first argument, got ${varVal.kind}`, { varVal }); + } + if (!is.aFunction(f)) { + throw new EvaluationError(`alter-var-root expects a function as its second argument, got ${f.kind}`, { f }); + } + const newVal = ctx.applyFunction(f, [varVal.value, ...args], callEnv); + varVal.value = newVal; + return newVal; + }).doc("Atomically alters the root binding of var v by applying f to its current value plus any additional args.", [["v", "f", "&", "args"]]) +}; - Note: Clojure uses char literal keys (e.g. {\\\\< \\"<\\"}). This interpreter - has no char type, so map keys must be single-character strings instead - (e.g. {\\"<\\" \\"<\\"})." - [s cmap] - (apply str (map (fn [c] - (let [r (cmap c)] - (if (nil? r) c (str r)))) - (split s #"")))) -`; +// src/core/stdlib/async-fns.ts +var asyncFunctions = { + then: v.nativeFnCtx("then", (ctx, callEnv, val, f) => { + if (!is.callable(f)) { + throw new EvaluationError(`${printString(f)} is not a callable value`, { fn: f, args: [] }); + } + if (val.kind !== "pending") { + return ctx.applyCallable(f, [val], callEnv); + } + const promise = val.promise.then((resolved) => { + try { + const result = ctx.applyCallable(f, [resolved], callEnv); + return result.kind === "pending" ? result.promise : result; + } catch (e) { + return Promise.reject(e); + } + }); + return v.pending(promise); + }).doc("Applies f to the resolved value of a pending, or to val directly if not pending.", [["val", "f"]]), + "catch*": v.nativeFnCtx("catch*", (ctx, callEnv, val, f) => { + if (!is.callable(f)) { + throw new EvaluationError(`${printString(f)} is not a callable value`, { fn: f, args: [] }); + } + if (val.kind !== "pending") + return val; + const promise = val.promise.catch((err) => { + let errVal; + if (err instanceof CljThrownSignal) { + errVal = err.value; + } else { + errVal = { + kind: "map", + entries: [ + [ + { kind: "keyword", name: ":type" }, + { kind: "keyword", name: ":error/js" } + ], + [ + { kind: "keyword", name: ":message" }, + { + kind: "string", + value: err instanceof Error ? err.message : String(err) + } + ] + ] + }; + } + try { + const result = ctx.applyCallable(f, [errVal], callEnv); + return result.kind === "pending" ? result.promise : result; + } catch (e) { + return Promise.reject(e); + } + }); + return v.pending(promise); + }).doc("Handles rejection of a pending value by calling f with the thrown value or an error map.", [["val", "f"]]), + "pending?": v.nativeFn("pending?", (val) => { + return v.boolean(val.kind === "pending"); + }).doc("Returns true if val is a pending (async) value.", [["val"]]), + "promise-of": v.nativeFn("promise-of", (val) => { + return v.pending(Promise.resolve(val)); + }).doc("Wraps val in an immediately-resolving pending value. Useful for testing async composition.", [["val"]]), + all: v.nativeFn("all", (val) => { + const items = val.kind === "nil" ? [] : toSeq(val); + const promises = items.map((item) => item.kind === "pending" ? item.promise : Promise.resolve(item)); + return v.pending(Promise.all(promises).then((results) => v.vector(results))); + }).doc("Returns a pending that resolves with a vector of all results when every input resolves.", [["pendings"]]) +}; -// src/clojure/generated/builtin-namespace-registry.ts -var builtInNamespaceSources = { - "clojure.core": () => clojure_coreSource, - "clojure.string": () => clojure_stringSource +// src/core/core-module.ts +var nativeFunctions = { + ...arithmeticFunctions, + ...atomFunctions, + ...collectionFunctions, + ...errorFunctions, + ...predicateFunctions, + ...hofFunctions, + ...metaFunctions, + ...transducerFunctions, + ...regexFunctions, + ...stringFunctions, + ...utilFunctions, + ...varFunctions, + ...lazyFunctions, + ...asyncFunctions }; +function readPrintCtx(callEnv) { + const len = tryLookup("*print-length*", callEnv); + const level = tryLookup("*print-level*", callEnv); + return { + printLength: len?.kind === "number" ? len.value : null, + printLevel: level?.kind === "number" ? level.value : null + }; +} +function emitToOut(ctx, callEnv, text) { + const outVar = ctx.resolveNs("clojure.core")?.vars.get("*out*"); + const out = outVar ? derefValue(outVar) : undefined; + if (out && (out.kind === "function" || out.kind === "native-function")) { + ctx.applyCallable(out, [v.string(text)], callEnv); + } else { + ctx.io.stdout(text); + } +} +function emitToErr(ctx, callEnv, text) { + const errVar = ctx.resolveNs("clojure.core")?.vars.get("*err*"); + const err = errVar ? derefValue(errVar) : undefined; + if (err && (err.kind === "function" || err.kind === "native-function")) { + ctx.applyCallable(err, [v.string(text)], callEnv); + } else { + ctx.io.stderr(text); + } +} +function makeCoreModule() { + return { + id: "clojure/core", + declareNs: [ + { + name: "clojure.core", + vars(_ctx) { + const map = new Map; + for (const [name, fn] of Object.entries(nativeFunctions)) { + const meta = fn.meta; + map.set(name, { value: fn, ...meta ? { meta } : {} }); + } + map.set("println", { + value: v.nativeFnCtx("println", (ctx, callEnv, ...args) => { + withPrintContext(readPrintCtx(callEnv), () => { + emitToOut(ctx, callEnv, args.map(valueToString).join(" ") + ` +`); + }); + return v.nil(); + }) + }); + map.set("print", { + value: v.nativeFnCtx("print", (ctx, callEnv, ...args) => { + withPrintContext(readPrintCtx(callEnv), () => { + emitToOut(ctx, callEnv, args.map(valueToString).join(" ")); + }); + return v.nil(); + }) + }); + map.set("newline", { + value: v.nativeFnCtx("newline", (ctx, callEnv) => { + emitToOut(ctx, callEnv, ` +`); + return v.nil(); + }) + }); + map.set("pr", { + value: v.nativeFnCtx("pr", (ctx, callEnv, ...args) => { + withPrintContext(readPrintCtx(callEnv), () => { + emitToOut(ctx, callEnv, args.map((v2) => printString(v2)).join(" ")); + }); + return v.nil(); + }) + }); + map.set("prn", { + value: v.nativeFnCtx("prn", (ctx, callEnv, ...args) => { + withPrintContext(readPrintCtx(callEnv), () => { + emitToOut(ctx, callEnv, args.map((v2) => printString(v2)).join(" ") + ` +`); + }); + return v.nil(); + }) + }); + map.set("pprint", { + value: v.nativeFnCtx("pprint", (ctx, callEnv, form, widthArg) => { + if (form === undefined) + return v.nil(); + const maxWidth = widthArg?.kind === "number" ? widthArg.value : 80; + withPrintContext(readPrintCtx(callEnv), () => { + emitToOut(ctx, callEnv, prettyPrintString(form, maxWidth) + ` +`); + }); + return v.nil(); + }) + }); + map.set("warn", { + value: v.nativeFnCtx("warn", (ctx, callEnv, ...args) => { + withPrintContext(readPrintCtx(callEnv), () => { + emitToErr(ctx, callEnv, args.map(valueToString).join(" ") + ` +`); + }); + return v.nil(); + }) + }); + map.set("*out*", { value: v.nil(), dynamic: true }); + map.set("*err*", { value: v.nil(), dynamic: true }); + map.set("*print-length*", { value: v.nil(), dynamic: true }); + map.set("*print-level*", { value: v.nil(), dynamic: true }); + map.set("*compiler-options*", { value: v.map([]) }); + map.set("clj->js", { + value: v.nativeFnCtx("clj->js", (ctx, callEnv, val) => { + if (is.jsValue(val)) + return val; + const applier = { + applyFunction: (fn, args) => ctx.applyCallable(fn, args, callEnv) + }; + return v.jsValue(cljToJs2(val, applier)); + }) + }); + map.set("js->clj", { + value: v.nativeFn("js->clj", (val, opts) => { + if (val.kind === "nil") + return val; + if (!is.jsValue(val)) { + throw new EvaluationError(`js->clj expects a js-value, got ${val.kind}`, { val }); + } + const keywordizeKeys = (() => { + if (!opts || opts.kind !== "map") + return false; + for (const [k, flag] of opts.entries) { + if (k.kind === "keyword" && k.name === ":keywordize-keys") { + return flag.kind !== "boolean" || flag.value !== false; + } + } + return false; + })(); + return jsToClj2(val.value, { keywordizeKeys }); + }) + }); + return map; + } + } + ] + }; +} -// src/core/session.ts +// src/core/stdlib/js-namespace.ts +function resolveJsKey(key, fnName) { + if (key.kind === "string") + return key.value; + if (key.kind === "keyword") + return key.name.slice(1); + if (key.kind === "number") + return String(key.value); + throw new EvaluationError(`${fnName}: key must be a string, keyword, or number, got ${key.kind}`, { key }); +} +function extractJsTarget(val, fnName) { + switch (val.kind) { + case "js-value": + return val.value; + case "string": + case "number": + case "boolean": + return val.value; + case "nil": + throw new EvaluationError(`${fnName}: cannot access properties on nil`, { val }); + default: + throw new EvaluationError(`${fnName}: expected a js-value or primitive, got ${val.kind}`, { val }); + } +} +function makeJsModule() { + return { + id: "conjure-js/js-namespace", + declareNs: [ + { + name: "js", + vars(_ctx) { + const map = new Map; + map.set("get", { + value: v.nativeFn("js/get", (obj, key, ...rest) => { + const raw = extractJsTarget(obj, "js/get"); + const jsKey = resolveJsKey(key, "js/get"); + const result = raw[jsKey]; + if (result === undefined && rest.length > 0) + return rest[0]; + return jsToClj(result); + }) + }); + map.set("set!", { + value: v.nativeFnCtx("js/set!", (ctx, callEnv, obj, key, val) => { + const raw = extractJsTarget(obj, "js/set!"); + const jsKey = resolveJsKey(key, "js/set!"); + raw[jsKey] = cljToJs(val, ctx, callEnv); + return val; + }) + }); + map.set("call", { + value: v.nativeFnCtx("js/call", (ctx, callEnv, fn, ...args) => { + const rawFn = fn.kind === "js-value" ? fn.value : undefined; + if (typeof rawFn !== "function") { + throw new EvaluationError(`js/call: expected a js-value wrapping a function, got ${fn.kind}`, { fn }); + } + const jsArgs = args.map((a) => cljToJs(a, ctx, callEnv)); + return jsToClj(rawFn(...jsArgs)); + }) + }); + map.set("typeof", { + value: v.nativeFn("js/typeof", (x) => { + switch (x.kind) { + case "nil": + return v.string("object"); + case "number": + return v.string("number"); + case "string": + return v.string("string"); + case "boolean": + return v.string("boolean"); + case "js-value": + return v.string(typeof x.value); + default: + throw new EvaluationError(`js/typeof: cannot determine JS type of Clojure ${x.kind}`, { x }); + } + }) + }); + map.set("instanceof?", { + value: v.nativeFn("js/instanceof?", (obj, cls) => { + if (obj.kind !== "js-value") { + throw new EvaluationError(`js/instanceof?: expected js-value, got ${obj.kind}`, { obj }); + } + if (cls.kind !== "js-value") { + throw new EvaluationError(`js/instanceof?: expected js-value constructor, got ${cls.kind}`, { cls }); + } + return v.boolean(obj.value instanceof cls.value); + }) + }); + map.set("array?", { + value: v.nativeFn("js/array?", (x) => { + if (x.kind !== "js-value") + return v.boolean(false); + return v.boolean(Array.isArray(x.value)); + }) + }); + map.set("null?", { + value: v.nativeFn("js/null?", (x) => { + return v.boolean(x.kind === "nil"); + }) + }); + map.set("undefined?", { + value: v.nativeFn("js/undefined?", (x) => { + return v.boolean(x.kind === "js-value" && x.value === undefined); + }) + }); + map.set("some?", { + value: v.nativeFn("js/some?", (x) => { + if (x.kind === "nil") + return v.boolean(false); + if (x.kind === "js-value" && x.value === undefined) + return v.boolean(false); + return v.boolean(true); + }) + }); + map.set("get-in", { + value: v.nativeFn("js/get-in", (obj, path, ...rest) => { + if (path.kind !== "vector") { + throw new EvaluationError(`js/get-in: path must be a vector, got ${path.kind}`, { path }); + } + if (obj.kind === "nil") { + throw new EvaluationError("js/get-in: cannot access properties on nil", { obj }); + } + const notFound = rest.length > 0 ? rest[0] : v.jsValue(undefined); + let current = obj; + for (const key of path.value) { + if (current.kind === "nil") + return notFound; + if (current.kind === "js-value" && current.value === undefined) + return notFound; + const raw = extractJsTarget(current, "js/get-in"); + const jsKey = resolveJsKey(key, "js/get-in"); + current = jsToClj(raw[jsKey]); + } + if (current.kind === "js-value" && current.value === undefined && rest.length > 0) { + return notFound; + } + return current; + }) + }); + map.set("prop", { + value: v.nativeFn("js/prop", (key, ...rest) => { + const notFound = rest.length > 0 ? rest[0] : v.nil(); + return v.nativeFn("js/prop-accessor", (obj) => { + const raw = extractJsTarget(obj, "js/prop"); + const jsKey = resolveJsKey(key, "js/prop"); + const result = raw[jsKey]; + if (result === undefined) + return notFound; + return jsToClj(result); + }); + }) + }); + map.set("method", { + value: v.nativeFn("js/method", (key, ...partialArgs) => { + return v.nativeFnCtx("js/method-caller", (ctx, callEnv, obj, ...callArgs) => { + const rawObj = extractJsTarget(obj, "js/method"); + const jsKey = resolveJsKey(key, "js/method"); + const method = rawObj[jsKey]; + if (typeof method !== "function") { + throw new EvaluationError(`js/method: property '${jsKey}' is not callable`, { jsKey }); + } + const allArgs = [...partialArgs, ...callArgs].map((a) => cljToJs(a, ctx, callEnv)); + return jsToClj(method.apply(rawObj, allArgs)); + }); + }) + }); + map.set("merge", { + value: v.nativeFnCtx("js/merge", (ctx, callEnv, ...args) => { + const result = Object.assign({}, ...args.map((a) => cljToJs(a, ctx, callEnv))); + return v.jsValue(result); + }) + }); + map.set("seq", { + value: v.nativeFn("js/seq", (arr) => { + if (arr.kind !== "js-value" || !Array.isArray(arr.value)) { + throw new EvaluationError(`js/seq: expected a js-value wrapping an array, got ${arr.kind}`, { arr }); + } + return v.vector(arr.value.map(jsToClj)); + }) + }); + map.set("array", { + value: v.nativeFnCtx("js/array", (ctx, callEnv, ...args) => { + return v.jsValue(args.map((a) => cljToJs(a, ctx, callEnv))); + }) + }); + map.set("obj", { + value: v.nativeFnCtx("js/obj", (ctx, callEnv, ...args) => { + if (args.length % 2 !== 0) { + throw new EvaluationError("js/obj: requires even number of arguments", { count: args.length }); + } + const result = {}; + for (let i = 0;i < args.length; i += 2) { + const jsKey = resolveJsKey(args[i], "js/obj"); + result[jsKey] = cljToJs(args[i + 1], ctx, callEnv); + } + return v.jsValue(result); + }) + }); + map.set("keys", { + value: v.nativeFn("js/keys", (obj) => { + const raw = extractJsTarget(obj, "js/keys"); + return v.vector(Object.keys(raw).map(v.string)); + }) + }); + map.set("values", { + value: v.nativeFn("js/values", (obj) => { + const raw = extractJsTarget(obj, "js/values"); + return v.vector(Object.values(raw).map(jsToClj)); + }) + }); + map.set("entries", { + value: v.nativeFn("js/entries", (obj) => { + const raw = extractJsTarget(obj, "js/entries"); + return v.vector(Object.entries(raw).map(([k, val]) => v.vector([v.string(k), jsToClj(val)]))); + }) + }); + return map; + } + } + ] + }; +} + +// src/core/runtime.ts +function cloneBindings(bindings) { + const out = new Map; + for (const [k, v2] of bindings) { + out.set(k, v2.kind === "var" ? { ...v2 } : v2); + } + return out; +} +function cloneEnv(env, memo) { + if (memo.has(env)) + return memo.get(env); + const cloned = { + bindings: cloneBindings(env.bindings), + outer: null + }; + if (env.ns) { + cloned.ns = { + kind: "namespace", + name: env.ns.name, + vars: new Map([...env.ns.vars].map(([k, v2]) => [k, { ...v2 }])), + aliases: new Map, + readerAliases: new Map(env.ns.readerAliases) + }; + } + memo.set(env, cloned); + if (env.outer) + cloned.outer = cloneEnv(env.outer, memo); + return cloned; +} +function cloneRegistry(registry) { + const memo = new Map; + const next = new Map; + for (const [name, env] of registry) { + next.set(name, cloneEnv(env, memo)); + } + for (const [name, env] of registry) { + const clonedEnv = next.get(name); + if (env.ns && clonedEnv.ns) { + for (const [alias, origNs] of env.ns.aliases) { + const targetCloned = next.get(origNs.name); + if (targetCloned?.ns) + clonedEnv.ns.aliases.set(alias, targetCloned.ns); + } + } + } + return next; +} function extractNsNameFromTokens(tokens) { const meaningful = tokens.filter((t) => t.kind !== "Comment"); if (meaningful.length < 3) @@ -5149,7 +8616,7 @@ function extractAliasMapFromTokens(tokens) { return aliases; } function findNsForm(forms) { - const nsForm = forms.find((f) => isList(f) && isSymbol(f.value[0]) && f.value[0].name === "ns"); + const nsForm = forms.find((f) => isList(f) && f.value.length > 0 && isSymbol(f.value[0]) && f.value[0].name === "ns"); if (!nsForm || !isList(nsForm)) return null; return nsForm; @@ -5167,7 +8634,7 @@ function extractRequireClauses(forms) { } return clauses; } -function processRequireSpec(spec, currentEnv, registry, resolveNamespace) { +function processRequireSpec(spec, currentEnv, registry, resolveNs) { if (!isVector(spec)) { throw new EvaluationError("require spec must be a vector, e.g. [my.ns :as alias]", { spec }); } @@ -5202,8 +8669,8 @@ function processRequireSpec(spec, currentEnv, registry, resolveNamespace) { return; } let targetEnv = registry.get(nsName); - if (!targetEnv && resolveNamespace) { - resolveNamespace(nsName); + if (!targetEnv && resolveNs) { + resolveNs(nsName); targetEnv = registry.get(nsName); } if (!targetEnv) { @@ -5242,18 +8709,11 @@ function processRequireSpec(spec, currentEnv, registry, resolveNamespace) { sym }); } - const v = lookupVar(sym.name, targetEnv); - if (v !== undefined) { - currentEnv.ns.vars.set(sym.name, v); - } else { - let value; - try { - value = lookup(sym.name, targetEnv); - } catch { - throw new EvaluationError(`Symbol ${sym.name} not found in namespace ${nsName}`, { nsName, symbol: sym.name }); - } - define(sym.name, value, currentEnv); + const v2 = targetEnv.ns.vars.get(sym.name); + if (v2 === undefined) { + throw new EvaluationError(`Symbol ${sym.name} not found in namespace ${nsName}`, { nsName, symbol: sym.name }); } + currentEnv.ns.vars.set(sym.name, v2); } i++; } else { @@ -5261,46 +8721,32 @@ function processRequireSpec(spec, currentEnv, registry, resolveNamespace) { } } } -function buildSessionApi(state, options) { - const registry = state.registry; - let currentNs = state.currentNs; - const coreEnv = registry.get("clojure.core"); - coreEnv.resolveNs = (name) => registry.get(name) ?? null; - const emitFn = options?.output ?? ((text) => console.log(text)); - internVar("println", cljNativeFunction("println", (...args) => { - emitFn(args.map(valueToString).join(" ") + ` -`); - return cljNil(); - }), coreEnv); - internVar("print", cljNativeFunction("print", (...args) => { - emitFn(args.map(valueToString).join(" ")); - return cljNil(); - }), coreEnv); - internVar("newline", cljNativeFunction("newline", () => { - emitFn(` -`); - return cljNil(); - }), coreEnv); - const sourceRoots = new Set(options?.sourceRoots ?? []); - function addSourceRoot(path) { - sourceRoots.add(path); +function ensureNamespaceInRegistry(registry, coreEnv, name) { + if (!registry.has(name)) { + const nsEnv = makeEnv(coreEnv); + nsEnv.ns = makeNamespace(name); + registry.set(name, nsEnv); } - const ctx = createEvaluationContext(); - function resolveNamespace(nsName) { + return registry.get(name); +} +function buildRuntime(registry, coreEnv, options) { + const sourceRoots = new Set(options?.sourceRoots ?? []); + const varOwners = new Map; + let currentNsRef = "user"; + function resolveNamespace(nsName, ctx) { const builtInLoader = builtInNamespaceSources[nsName]; if (builtInLoader) { - loadFile(builtInLoader(), nsName); + runtime.loadFile(builtInLoader(), nsName, undefined, ctx); return true; } - if (!options?.readFile || sourceRoots.size === 0) { + if (!options?.readFile || sourceRoots.size === 0) return false; - } for (const root of sourceRoots) { const filePath = `${root.replace(/\/$/, "")}/${nsName.replace(/\./g, "/")}.clj`; try { const source = options.readFile(filePath); if (source) { - loadFile(source); + runtime.loadFile(source, undefined, undefined, ctx); return true; } } catch { @@ -5309,85 +8755,398 @@ function buildSessionApi(state, options) { } return false; } - function ensureNs(name) { - if (!registry.has(name)) { - const nsEnv = makeEnv(coreEnv); - nsEnv.ns = makeNamespace(name); - registry.set(name, nsEnv); - } - return registry.get(name); - } - function setNs(name) { - ensureNs(name); - currentNs = name; - } - function getNs(name) { - return registry.get(name)?.ns ?? null; - } - function getNsEnv(name) { - return registry.get(name) ?? null; + const initialNsObj = registry.get("user")?.ns ?? makeNamespace("user"); + internVar("*ns*", initialNsObj, coreEnv); + const nsVar = coreEnv.ns?.vars.get("*ns*"); + if (nsVar) + nsVar.dynamic = true; + function resolveNsSym(sym) { + if (sym === undefined) + return null; + if (isNamespace(sym)) + return sym; + if (!isSymbol(sym)) + return null; + return registry.get(sym.name)?.ns ?? null; } - internVar("require", cljNativeFunction("require", (...args) => { - const currentEnv = registry.get(currentNs); + internVar("ns-name", v.nativeFn("ns-name", (x) => { + if (x === undefined) + return v.nil(); + if (x.kind === "namespace") + return v.symbol(x.name); + if (x.kind === "symbol") + return x; + if (x.kind === "string") + return v.symbol(x.value); + return v.nil(); + }), coreEnv); + internVar("all-ns", v.nativeFn("all-ns", () => v.list([...registry.values()].map((env) => env.ns).filter(Boolean))), coreEnv); + internVar("find-ns", v.nativeFn("find-ns", (sym) => { + if (sym === undefined || !isSymbol(sym)) + return v.nil(); + return registry.get(sym.name)?.ns ?? v.nil(); + }), coreEnv); + internVar("in-ns", v.nativeFnCtx("in-ns", (ctx, _callEnv, sym) => { + if (!sym || !isSymbol(sym)) { + throw new EvaluationError("in-ns expects a symbol", { sym }); + } + if (ctx.setCurrentNs) + ctx.setCurrentNs(sym.name); + return registry.get(sym.name)?.ns ?? v.nil(); + }), coreEnv); + internVar("ns-aliases", v.nativeFn("ns-aliases", (sym) => { + const ns = resolveNsSym(sym); + if (!ns) + return v.map([]); + const entries = []; + ns.aliases.forEach((targetNs, alias) => { + entries.push([v.symbol(alias), targetNs]); + }); + return v.map(entries); + }), coreEnv); + internVar("ns-interns", v.nativeFn("ns-interns", (sym) => { + const ns = resolveNsSym(sym); + if (!ns) + return v.map([]); + const entries = []; + ns.vars.forEach((theVar, name) => { + if (theVar.ns === ns.name) + entries.push([v.symbol(name), theVar]); + }); + return v.map(entries); + }), coreEnv); + internVar("ns-publics", v.nativeFn("ns-publics", (sym) => { + const ns = resolveNsSym(sym); + if (!ns) + return v.map([]); + const entries = []; + ns.vars.forEach((theVar, name) => { + if (theVar.ns === ns.name) + entries.push([v.symbol(name), theVar]); + }); + return v.map(entries); + }), coreEnv); + internVar("ns-refers", v.nativeFn("ns-refers", (sym) => { + const ns = resolveNsSym(sym); + if (!ns) + return v.map([]); + const entries = []; + ns.vars.forEach((theVar, name) => { + if (theVar.ns !== ns.name) + entries.push([v.symbol(name), theVar]); + }); + return v.map(entries); + }), coreEnv); + internVar("ns-map", v.nativeFn("ns-map", (sym) => { + const ns = resolveNsSym(sym); + if (!ns) + return v.map([]); + const entries = []; + ns.vars.forEach((theVar, name) => { + entries.push([v.symbol(name), theVar]); + }); + return v.map(entries); + }), coreEnv); + internVar("ns-imports", v.nativeFn("ns-imports", (_sym) => v.map([])), coreEnv); + internVar("the-ns", v.nativeFn("the-ns", (sym) => { + if (sym === undefined) + return v.nil(); + if (isNamespace(sym)) + return sym; + if (!isSymbol(sym)) + return v.nil(); + return registry.get(sym.name)?.ns ?? v.nil(); + }), coreEnv); + internVar("instance?", v.nativeFn("instance?", (_cls, _obj) => v.boolean(false)), coreEnv); + internVar("class", v.nativeFn("class", (x) => { + if (x === undefined) + return v.nil(); + return v.string(`conjure.${x.kind}`); + }), coreEnv); + internVar("class?", v.nativeFn("class?", (_x) => v.boolean(false)), coreEnv); + internVar("special-symbol?", v.nativeFn("special-symbol?", (sym) => { + if (sym === undefined || !isSymbol(sym)) + return v.boolean(false); + const specials = new Set([ + "def", + "if", + "do", + "let", + "quote", + "var", + "fn", + "loop", + "recur", + "throw", + "try", + "catch", + "finally", + "ns", + "defmacro", + "binding", + "monitor-enter", + "monitor-exit", + "new", + "set!", + ".", + "import" + ]); + return v.boolean(specials.has(sym.name)); + }), coreEnv); + internVar("loaded-libs", v.nativeFn("loaded-libs", () => v.set([...registry.keys()].map(v.symbol))), coreEnv); + internVar("require", v.nativeFnCtx("require", (ctx, _callEnv, ...args) => { + const currentEnv = registry.get(currentNsRef); for (const arg of args) { - processRequireSpec(arg, currentEnv, registry, resolveNamespace); + processRequireSpec(arg, currentEnv, registry, (nsName) => resolveNamespace(nsName, ctx)); } - return cljNil(); + return v.nil(); }), coreEnv); - internVar("resolve", cljNativeFunction("resolve", (sym) => { + internVar("resolve", v.nativeFn("resolve", (sym) => { if (!isSymbol(sym)) - return cljNil(); + return v.nil(); const slashIdx = sym.name.indexOf("/"); if (slashIdx > 0) { const nsName = sym.name.slice(0, slashIdx); const symName = sym.name.slice(slashIdx + 1); const nsEnv = registry.get(nsName) ?? null; if (!nsEnv) - return cljNil(); - return tryLookup(symName, nsEnv) ?? cljNil(); + return v.nil(); + return tryLookup(symName, nsEnv) ?? v.nil(); } - const currentEnv = registry.get(currentNs); - return tryLookup(sym.name, currentEnv) ?? cljNil(); + const currentEnv = registry.get(currentNsRef); + return tryLookup(sym.name, currentEnv) ?? v.nil(); }), coreEnv); - function processNsRequires(forms, env) { - const requireClauses = extractRequireClauses(forms); - for (const specs of requireClauses) { - for (const spec of specs) { - processRequireSpec(spec, env, registry, resolveNamespace); - } - } - } - function loadFile(source, nsName, filePath) { - const tokens = tokenize(source); - const targetNs = extractNsNameFromTokens(tokens) ?? nsName ?? "user"; - const aliasMap = extractAliasMapFromTokens(tokens); - const forms = readForms(tokens, targetNs, aliasMap); - const env = ensureNs(targetNs); - ctx.currentSource = source; - ctx.currentFile = filePath; - ctx.currentLineOffset = 0; - ctx.currentColOffset = 0; - processNsRequires(forms, env); - try { - for (const form of forms) { - const expanded = ctx.expandAll(form, env); - ctx.evaluate(expanded, env); + const reflectEnv = ensureNamespaceInRegistry(registry, coreEnv, "clojure.reflect"); + internVar("parse-flags", v.nativeFn("parse-flags", (_flags, _kind) => v.set([])), reflectEnv); + internVar("reflect", v.nativeFn("reflect", (_obj) => v.map([])), reflectEnv); + internVar("type-reflect", v.nativeFn("type-reflect", (_typeobj, ..._opts) => v.map([])), reflectEnv); + const cursiveEnv = ensureNamespaceInRegistry(registry, coreEnv, "cursive.repl.runtime"); + internVar("completions", v.nativeFn("completions", (..._args) => v.nil()), cursiveEnv); + for (const javaClass of [ + "Class", + "Object", + "String", + "Number", + "Boolean", + "Integer", + "Long", + "Double", + "Float", + "Byte", + "Short", + "Character", + "Void", + "Math", + "System", + "Runtime", + "Thread", + "Throwable", + "Exception", + "Error", + "Iterable", + "Comparable", + "Runnable", + "Cloneable" + ]) { + internVar(javaClass, v.keyword(`:java.lang/${javaClass}`), coreEnv); + } + const runtime = { + get registry() { + return registry; + }, + ensureNamespace(name) { + return ensureNamespaceInRegistry(registry, coreEnv, name); + }, + getNamespaceEnv(name) { + return registry.get(name) ?? null; + }, + getNs(name) { + return registry.get(name)?.ns ?? null; + }, + syncNsVar(name) { + currentNsRef = name; + const nsVarInner = coreEnv.ns?.vars.get("*ns*"); + if (nsVarInner) { + const nsObj = registry.get(name)?.ns; + if (nsObj) + nsVarInner.value = nsObj; + } + }, + addSourceRoot(path) { + sourceRoots.add(path); + }, + processRequireSpec(spec, fromEnv, ctx) { + processRequireSpec(spec, fromEnv, registry, (nsName) => resolveNamespace(nsName, ctx)); + }, + processNsRequires(forms, fromEnv, ctx) { + const requireClauses = extractRequireClauses(forms); + for (const specs of requireClauses) { + for (const spec of specs) { + if (isVector(spec) && spec.value.length > 0 && isString(spec.value[0])) { + const specifier = spec.value[0].value; + throw new EvaluationError(`String module require ["${specifier}" :as ...] is async — use evaluateAsync() instead of evaluate()`, { specifier }); + } + processRequireSpec(spec, fromEnv, registry, (nsName) => resolveNamespace(nsName, ctx)); + } + } + }, + async processNsRequiresAsync(forms, fromEnv, ctx) { + const requireClauses = extractRequireClauses(forms); + for (const specs of requireClauses) { + for (const spec of specs) { + if (isVector(spec) && spec.value.length > 0 && isString(spec.value[0])) { + const specifier = spec.value[0].value; + if (!ctx.importModule) { + throw new EvaluationError(`importModule is not configured; cannot require "${specifier}". Pass importModule to createSession().`, { specifier }); + } + const elements = spec.value; + let aliasName = null; + for (let i = 1;i < elements.length; i++) { + if (isKeyword(elements[i]) && elements[i].name === ":as") { + i++; + const aliasSym = elements[i]; + if (!aliasSym || !isSymbol(aliasSym)) { + throw new EvaluationError(":as expects a symbol alias", { spec }); + } + aliasName = aliasSym.name; + break; + } + } + if (aliasName === null) { + throw new EvaluationError(`String require spec must have an :as alias: ["${specifier}" :as Alias]`, { spec }); + } + const rawModule = await ctx.importModule(specifier); + internVar(aliasName, v.jsValue(rawModule), fromEnv); + } else { + processRequireSpec(spec, fromEnv, registry, (nsName) => resolveNamespace(nsName, ctx)); + } + } + } + }, + loadFile(source, nsName, filePath, ctx) { + const tokens = tokenize(source); + const targetNs = extractNsNameFromTokens(tokens) ?? nsName ?? "user"; + const aliasMap = extractAliasMapFromTokens(tokens); + const forms = readForms(tokens, targetNs, aliasMap); + const env = this.ensureNamespace(targetNs); + ctx.currentSource = source; + ctx.currentFile = filePath; + ctx.currentLineOffset = 0; + ctx.currentColOffset = 0; + this.processNsRequires(forms, env, ctx); + try { + for (const form of forms) { + const expanded = ctx.expandAll(form, env); + ctx.evaluate(expanded, env); + } + } finally { + ctx.currentSource = undefined; + ctx.currentFile = undefined; + } + return targetNs; + }, + installModules(modules) { + const ordered = resolveModuleOrder(modules, new Set(registry.keys())); + for (const mod of ordered) { + for (const decl of mod.declareNs) { + const nsEnv = ensureNamespaceInRegistry(registry, coreEnv, decl.name); + const ctx = { + getVar(ns, name) { + const nsEnv2 = registry.get(ns); + const v2 = nsEnv2?.ns?.vars.get(name); + return v2 ?? null; + }, + getNamespace(name) { + return registry.get(name)?.ns ?? null; + } + }; + const varMap = decl.vars(ctx); + for (const [varName, decl2] of varMap) { + const key = `${nsEnv.ns.name}/${varName}`; + const existing = varOwners.get(key); + if (existing !== undefined) { + throw new Error(`var '${varName}' in '${nsEnv.ns.name}' already declared by module '${existing}'`); + } + internVar(varName, decl2.value, nsEnv, decl2.meta); + if (decl2.dynamic) { + const v2 = nsEnv.ns.vars.get(varName); + v2.dynamic = true; + } + varOwners.set(key, mod.id); + } + } } - } finally { - ctx.currentSource = undefined; - ctx.currentFile = undefined; + }, + snapshot() { + return { registry: cloneRegistry(registry) }; } - return targetNs; - } - const api = { - registry, + }; + return runtime; +} +function createRuntime(options) { + const registry = new Map; + const coreEnv = makeEnv(); + coreEnv.ns = makeNamespace("clojure.core"); + registry.set("clojure.core", coreEnv); + const userEnv = makeEnv(coreEnv); + userEnv.ns = makeNamespace("user"); + registry.set("user", userEnv); + const runtime = buildRuntime(registry, coreEnv, options); + runtime.installModules([makeCoreModule(), makeJsModule()]); + return runtime; +} + +// src/core/session.ts +function buildSessionFacade(runtime, initialNs, options) { + let currentNs = initialNs; + const ctx = createEvaluationContext(); + ctx.resolveNs = (name) => runtime.getNs(name); + ctx.io = { + stdout: options?.output ?? ((text) => console.log(text)), + stderr: options?.stderr ?? ((text) => console.error(text)) + }; + ctx.importModule = options?.importModule; + ctx.setCurrentNs = (name) => { + runtime.ensureNamespace(name); + currentNs = name; + runtime.syncNsVar(name); + }; + const session = { + get runtime() { + return runtime; + }, + get registry() { + return runtime.registry; + }, get currentNs() { return currentNs; }, - setNs, - getNs, - loadFile, - addSourceRoot, + setNs(name) { + runtime.ensureNamespace(name); + currentNs = name; + runtime.syncNsVar(name); + }, + getNs(name) { + return runtime.getNs(name); + }, + loadFile(source, nsName, filePath) { + return runtime.loadFile(source, nsName, filePath, ctx); + }, + async loadFileAsync(source, nsName, filePath) { + if (nsName) { + const tokens = tokenize(source); + if (!extractNsNameFromTokens(tokens)) { + runtime.ensureNamespace(nsName); + currentNs = nsName; + runtime.syncNsVar(nsName); + } + } + await session.evaluateAsync(source, { file: filePath }); + return currentNs; + }, + addSourceRoot(path) { + runtime.addSourceRoot(path); + }, evaluate(source, opts) { ctx.currentSource = source; ctx.currentFile = opts?.file; @@ -5397,10 +9156,11 @@ function buildSessionApi(state, options) { const tokens = tokenize(source); const declaredNs = extractNsNameFromTokens(tokens); if (declaredNs) { - ensureNs(declaredNs); + runtime.ensureNamespace(declaredNs); currentNs = declaredNs; + runtime.syncNsVar(declaredNs); } - const env = getNsEnv(currentNs); + const env = runtime.getNamespaceEnv(currentNs); const aliasMap = extractAliasMapFromTokens(tokens); env.ns?.aliases.forEach((ns, alias) => { aliasMap.set(alias, ns.name); @@ -5409,8 +9169,8 @@ function buildSessionApi(state, options) { aliasMap.set(alias, nsName); }); const forms = readForms(tokens, currentNs, aliasMap); - processNsRequires(forms, env); - let result = cljNil(); + runtime.processNsRequires(forms, env, ctx); + let result = v.nil(); for (const form of forms) { const expanded = ctx.expandAll(form, env); result = ctx.evaluate(expanded, env); @@ -5437,10 +9197,75 @@ function buildSessionApi(state, options) { ctx.currentFile = undefined; } }, + async evaluateAsync(source, opts) { + ctx.currentSource = source; + ctx.currentFile = opts?.file; + ctx.currentLineOffset = opts?.lineOffset ?? 0; + ctx.currentColOffset = opts?.colOffset ?? 0; + try { + const tokens = tokenize(source); + const declaredNs = extractNsNameFromTokens(tokens); + if (declaredNs) { + runtime.ensureNamespace(declaredNs); + currentNs = declaredNs; + runtime.syncNsVar(declaredNs); + } + const env = runtime.getNamespaceEnv(currentNs); + const aliasMap = extractAliasMapFromTokens(tokens); + env.ns?.aliases.forEach((ns, alias) => { + aliasMap.set(alias, ns.name); + }); + env.ns?.readerAliases.forEach((nsName, alias) => { + aliasMap.set(alias, nsName); + }); + const forms = readForms(tokens, currentNs, aliasMap); + await runtime.processNsRequiresAsync(forms, env, ctx); + let result = v.nil(); + for (const form of forms) { + const expanded = ctx.expandAll(form, env); + result = ctx.evaluate(expanded, env); + } + if (result.kind !== "pending") + return result; + try { + return await result.promise; + } catch (e) { + if (e instanceof CljThrownSignal) { + throw new EvaluationError(`Unhandled throw: ${printString(e.value)}`, { thrownValue: e.value }); + } + throw e; + } + } catch (e) { + if (e instanceof CljThrownSignal) { + throw new EvaluationError(`Unhandled throw: ${printString(e.value)}`, { thrownValue: e.value }); + } + if (e instanceof RecurSignal) { + throw new EvaluationError("recur called outside of loop or fn", { + args: e.args + }); + } + if ((e instanceof EvaluationError || e instanceof ReaderError) && e.pos) { + e.message += formatErrorContext(source, e.pos, { + lineOffset: ctx.currentLineOffset, + colOffset: ctx.currentColOffset + }); + } + throw e; + } finally { + ctx.currentSource = undefined; + ctx.currentFile = undefined; + } + }, + applyFunction(fn, args) { + return ctx.applyCallable(fn, args, makeEnv()); + }, + cljToJs(value) { + return cljToJs2(value, { applyFunction: (fn, args) => ctx.applyCallable(fn, args, makeEnv()) }); + }, evaluateForms(forms) { try { - const env = getNsEnv(currentNs); - let result = cljNil(); + const env = runtime.getNamespaceEnv(currentNs); + let result = v.nil(); for (const form of forms) { const expanded = ctx.expandAll(form, env); result = ctx.evaluate(expanded, env); @@ -5459,7 +9284,7 @@ function buildSessionApi(state, options) { } }, getCompletions(prefix, nsName) { - let env = registry.get(nsName ?? currentNs) ?? null; + let env = runtime.registry.get(nsName ?? currentNs) ?? null; const seen = new Set; while (env) { for (const key of env.bindings.keys()) @@ -5475,23 +9300,34 @@ function buildSessionApi(state, options) { return candidates.filter((k) => k.startsWith(prefix)).sort(); } }; - return api; + return session; } function createSession(options) { - const registry = new Map; - const coreEnv = makeEnv(); - coreEnv.ns = makeNamespace("clojure.core"); - loadCoreFunctions(coreEnv, options?.output); - registry.set("clojure.core", coreEnv); - const userEnv = makeEnv(coreEnv); - userEnv.ns = makeNamespace("user"); - registry.set("user", userEnv); - const session = buildSessionApi({ registry, currentNs: "user" }, options); + const modules = options?.modules ?? []; + const runtime = createRuntime({ + sourceRoots: options?.sourceRoots, + readFile: options?.readFile + }); + const session = buildSessionFacade(runtime, "user", options); const coreLoader = builtInNamespaceSources["clojure.core"]; if (!coreLoader) { throw new Error("Missing built-in clojure.core source in registry"); } session.loadFile(coreLoader(), "clojure.core"); + if (modules.length > 0) { + session.runtime.installModules(modules); + } + if (options?.hostBindings) { + const jsEnv = runtime.getNamespaceEnv("js"); + if (jsEnv) { + for (const [name, rawValue] of Object.entries(options.hostBindings)) { + if (jsEnv.ns?.vars.has(name)) { + throw new Error(`createSession: hostBindings key '${name}' conflicts with built-in js/${name} — choose a different key`); + } + internVar(name, jsToClj(rawValue), jsEnv); + } + } + } for (const source of options?.entries ?? []) { session.loadFile(source); } @@ -5499,6 +9335,7 @@ function createSession(options) { } // src/vite-plugin-clj/namespace-utils.ts +import { resolve, dirname } from "node:path"; function pathToNs(filePath, sourceRoots) { const normalized = filePath.replace(/\\/g, "/"); for (const root of sourceRoots) { @@ -5532,6 +9369,30 @@ function extractNsRequires(source) { } return requires; } +function extractStringRequires(source, filePath) { + const forms = readForms(tokenize(source)); + const nsForm = forms.find((f) => isList(f) && isSymbol(f.value[0]) && f.value[0].name === "ns"); + if (!nsForm || !isList(nsForm)) + return []; + const specifiers = []; + for (let i = 2;i < nsForm.value.length; i++) { + const clause = nsForm.value[i]; + if (isList(clause) && isKeyword(clause.value[0]) && clause.value[0].name === ":require") { + for (let j = 1;j < clause.value.length; j++) { + const spec = clause.value[j]; + const first = isVector(spec) && spec.value.length > 0 ? spec.value[0] : null; + if (!first || first.kind !== "string") + continue; + let specifier = first.value; + if (filePath && (specifier.startsWith("./") || specifier.startsWith("../"))) { + specifier = resolve(dirname(filePath), specifier); + } + specifiers.push(specifier); + } + } + } + return [...new Set(specifiers)]; +} function extractNsName(source) { const forms = readForms(tokenize(source)); const nsForm = forms.find((f) => isList(f) && isSymbol(f.value[0]) && f.value[0].name === "ns"); @@ -5541,10 +9402,137 @@ function extractNsName(source) { return isSymbol(nameSymbol) ? nameSymbol.name : null; } +// src/vite-plugin-clj/static-analysis.ts +function readNamespaceVars(source) { + const forms = readForms(tokenize(source)); + const descriptors = []; + for (const form of forms) { + if (!isList(form) || form.value.length < 2) + continue; + const head = form.value[0]; + if (!isSymbol(head)) + continue; + const descriptor = parseTopLevelDef(form, head.name); + if (descriptor) + descriptors.push(descriptor); + } + return descriptors; +} +function parseTopLevelDef(form, op) { + switch (op) { + case "defn": + return parseDefn(form, false, false); + case "defn-": + return parseDefn(form, true, false); + case "defmacro": + return parseDefn(form, false, true); + case "def": + case "defonce": + return parseDef(form); + case "declare": + return parseDeclare(form); + default: + return null; + } +} +function parseDefn(form, isPrivate, isMacro2) { + const nameSym = form.value[1]; + if (!isSymbol(nameSym)) + return null; + const rest = form.value.slice(2); + const start = rest.length > 0 && rest[0].kind === "string" ? 1 : 0; + const bodyForms = rest.slice(start); + if (bodyForms.length === 0) { + return { name: nameSym.name, kind: "fn", arities: [], isPrivate, isMacro: isMacro2 }; + } + const arities = isList(bodyForms[0]) ? bodyForms.filter(isList).map(parseArityClause) : isVector(bodyForms[0]) ? [vectorToArity(bodyForms[0])] : []; + return { name: nameSym.name, kind: "fn", arities, isPrivate, isMacro: isMacro2 }; +} +function parseArityClause(clause) { + const paramVec = clause.value[0]; + return isVector(paramVec) ? vectorToArity(paramVec) : { params: [], restParam: null, body: [] }; +} +function vectorToArity(paramVec) { + const params = []; + let restParam = null; + for (let i = 0;i < paramVec.value.length; i++) { + const p = paramVec.value[i]; + if (isSymbol(p) && p.name === "&") { + const next = paramVec.value[i + 1]; + if (next) + restParam = next; + break; + } + params.push(p); + } + return { params, restParam, body: [] }; +} +function parseDef(form) { + const nameSym = form.value[1]; + if (!isSymbol(nameSym)) + return null; + const value = form.value[2]; + if (!value) { + return { name: nameSym.name, kind: "unknown", isPrivate: false, isMacro: false }; + } + const fnArities = tryExtractFnArities(value); + if (fnArities !== null) { + return { name: nameSym.name, kind: "fn", arities: fnArities, isPrivate: false, isMacro: false }; + } + const tsType = inferLiteralTsType(value); + if (tsType !== null) { + return { name: nameSym.name, kind: "const", tsType, isPrivate: false, isMacro: false }; + } + return { name: nameSym.name, kind: "unknown", isPrivate: false, isMacro: false }; +} +function inferLiteralTsType(value) { + switch (value.kind) { + case "number": + return "number"; + case "string": + return "string"; + case "boolean": + return "boolean"; + case "nil": + return "null"; + case "keyword": + return "string"; + case "vector": + case "set": + return "unknown[]"; + case "map": + return "Record"; + default: + return null; + } +} +function parseDeclare(form) { + const nameSym = form.value[1]; + if (!isSymbol(nameSym)) + return null; + return { name: nameSym.name, kind: "unknown", isPrivate: false, isMacro: false }; +} +function tryExtractFnArities(value) { + if (!isList(value)) + return null; + const head = value.value[0]; + if (!isSymbol(head) || head.name !== "fn") + return null; + let rest = value.value.slice(1); + if (rest.length > 0 && isSymbol(rest[0])) + rest = rest.slice(1); + if (rest.length === 0) + return []; + if (isVector(rest[0])) { + return [vectorToArity(rest[0])]; + } + return rest.filter(isList).map(parseArityClause); +} + // src/vite-plugin-clj/codegen.ts -function generateModuleCode(ctx, nsNameFromPath, source) { +function generateModuleCode(ctx, nsNameFromPath, source, filePath) { const nsName = extractNsName(source) ?? nsNameFromPath; - ctx.session.loadFile(source, nsName); + const hasStringRequires = extractStringRequires(source, filePath).length > 0; const requires = extractNsRequires(source); const depImports = requires.map((depNs) => { const depPath = ctx.resolveDepPath(depNs); @@ -5553,31 +9541,42 @@ function generateModuleCode(ctx, nsNameFromPath, source) { return null; }).filter(Boolean).join(` `); - const nsData = ctx.session.getNs(nsName); - if (!nsData) { - return `throw new Error('Namespace ${nsName} failed to load');`; - } + const vars = readNamespaceVars(source); const exportLines = []; - for (const [name, v] of nsData.vars) { - const value = v.value; - if (isMacro(value)) + for (const descriptor of vars) { + if (descriptor.isMacro) continue; - const safeName = safeJsIdentifier(name); - const deref2 = `__ns.vars.get(${JSON.stringify(name)}).value`; - if (isAFunction2(value)) { - exportLines.push(`export function ${safeName}(...args) {` + ` const fn = ${deref2};` + ` const cljArgs = args.map(jsToClj);` + ` const result = applyFunction(fn, cljArgs);` + ` return cljToJs(result);` + `}`); + if (descriptor.isPrivate) + continue; + const safeName = safeJsIdentifier(descriptor.name); + const deref2 = `__ns.vars.get(${JSON.stringify(descriptor.name)}).value`; + if (descriptor.kind === "fn") { + exportLines.push(`export function ${safeName}(...args) {` + ` const fn = ${deref2};` + ` const cljArgs = args.map(jsToClj);` + ` const result = __session.applyFunction(fn, cljArgs);` + ` return cljToJs(result, __session);` + `}`); } else { - exportLines.push(`export const ${safeName} = cljToJs(${deref2});`); + exportLines.push(`export const ${safeName} = cljToJs(${deref2}, __session);`); } } const escapedSource = JSON.stringify(source); + const loadCall = hasStringRequires ? `await __session.loadFileAsync(${escapedSource}, ${JSON.stringify(nsName)});` : `__session.loadFile(${escapedSource}, ${JSON.stringify(nsName)});`; + if (exportLines.length === 0) { + return [ + `import { getSession } from ${JSON.stringify(ctx.virtualSessionId)};`, + depImports, + ``, + `const __session = getSession();`, + loadCall, + ``, + `if (import.meta.hot) { import.meta.hot.accept() }` + ].join(` +`); + } return [ `import { getSession } from ${JSON.stringify(ctx.virtualSessionId)};`, - `import { cljToJs, jsToClj, applyFunction } from ${JSON.stringify(ctx.coreIndexPath)};`, + `import { cljToJs, jsToClj } from ${JSON.stringify(ctx.coreIndexPath)};`, depImports, ``, `const __session = getSession();`, - `__session.loadFile(${escapedSource}, ${JSON.stringify(nsName)});`, + loadCall, `const __ns = __session.getNs(${JSON.stringify(nsName)});`, ``, ...exportLines, @@ -5588,38 +9587,31 @@ function generateModuleCode(ctx, nsNameFromPath, source) { ].join(` `); } -function isAFunction2(value) { - return value.kind === "function" || value.kind === "native-function"; -} -function cljValueToTsType(value) { - switch (value.kind) { - case "number": - return "number"; - case "string": - return "string"; - case "boolean": - return "boolean"; - case "nil": - return "null"; - case "keyword": - return "string"; - case "symbol": - return "string"; - case "list": - case "vector": - return "unknown[]"; - case "map": - return "Record"; - case "function": - case "native-function": - return "(...args: unknown[]) => unknown"; - case "macro": - return "never"; - case "var": - return "unknown"; - default: - throw new Error(`Unknown CljValue kind: ${value.kind}`); +function generateDts(ctx, nsNameFromPath, source) { + const nsName = extractNsName(source) ?? nsNameFromPath; + const vars = readNamespaceVars(source); + const declarations = []; + for (const descriptor of vars) { + if (descriptor.isMacro) + continue; + if (descriptor.isPrivate) + continue; + const safeName = safeJsIdentifier(descriptor.name); + if (descriptor.kind === "fn") { + if (descriptor.arities && descriptor.arities.length > 0) { + for (const arity of descriptor.arities) { + declarations.push(`export function ${safeName}${arityToSignature(arity)};`); + } + } else { + declarations.push(`export function ${safeName}(...args: unknown[]): unknown;`); + } + } else { + const tsType = descriptor.tsType ?? "unknown"; + declarations.push(`export const ${safeName}: ${tsType};`); + } } + return declarations.join(` +`); } function patternName(p, index) { if (p.kind === "symbol") @@ -5635,35 +9627,6 @@ function arityToSignature(arity) { } return `(${fixedParams}): unknown`; } -function generateDts(ctx, nsNameFromPath, source) { - const nsName = extractNsName(source) ?? nsNameFromPath; - try { - ctx.session.loadFile(source, nsName); - } catch { - return ""; - } - const nsData = ctx.session.getNs(nsName); - if (!nsData) - return ""; - const declarations = []; - for (const [name, v] of nsData.vars) { - const value = v.value; - if (isMacro(value)) - continue; - const safeName = safeJsIdentifier(name); - if (value.kind === "function") { - for (const arity of value.arities) { - declarations.push(`export function ${safeName}${arityToSignature(arity)};`); - } - } else if (value.kind === "native-function") { - declarations.push(`export function ${safeName}(...args: unknown[]): unknown;`); - } else { - declarations.push(`export const ${safeName}: ${cljValueToTsType(value)};`); - } - } - return declarations.join(` -`); -} var JS_RESERVED_WORDS = new Set([ "break", "case", @@ -5867,7 +9830,91 @@ class BDecoderStream extends stream.Transform { } // src/bin/version.ts -var VERSION = "0.0.10"; +var VERSION = "0.0.12"; + +// src/bin/nrepl-symbol.ts +function resolveSymbol(sym, session, contextNs) { + const ns = contextNs ?? session.currentNs; + const slashIdx = sym.indexOf("/"); + if (slashIdx > 0) { + const qualifier = sym.slice(0, slashIdx); + const localName2 = sym.slice(slashIdx + 1); + const nsEnvFull2 = session.registry.get(qualifier); + if (nsEnvFull2) { + const value2 = tryLookup(localName2, nsEnvFull2); + if (value2 !== undefined) { + const varObj2 = lookupVar(localName2, nsEnvFull2); + return { value: value2, resolvedNs: qualifier, localName: localName2, varObj: varObj2 }; + } + } + const currentNsData = session.getNs(ns); + const aliasedNs = currentNsData?.aliases.get(qualifier); + if (aliasedNs) { + const varObj2 = aliasedNs.vars.get(localName2); + if (varObj2 !== undefined) + return { value: varObj2.value, resolvedNs: aliasedNs.name, localName: localName2, varObj: varObj2 }; + } + return null; + } + const localName = sym; + const nsEnvFull = session.registry.get(ns); + if (!nsEnvFull) + return null; + const value = tryLookup(sym, nsEnvFull); + if (value === undefined) + return null; + const varObj = lookupVar(sym, nsEnvFull); + let resolvedNs; + if (varObj) { + resolvedNs = varObj.ns; + } else if (value.kind === "function" || value.kind === "macro") { + resolvedNs = getNamespaceEnv(value.env).ns?.name ?? ns; + } else if (value.kind === "native-function") { + const i = value.name.indexOf("/"); + resolvedNs = i > 0 ? value.name.slice(0, i) : ns; + } else { + resolvedNs = ns; + } + return { value, resolvedNs, localName, varObj }; +} +function extractMeta(value, varMeta) { + const type = value.kind === "macro" ? "macro" : value.kind === "function" || value.kind === "native-function" ? "function" : "var"; + const meta = varMeta ?? (value.kind === "function" ? value.meta : value.kind === "native-function" ? value.meta : undefined); + let doc = ""; + let arglistsStr = ""; + let eldocArgs = null; + if (meta) { + const docEntry = meta.entries.find(([k]) => k.kind === "keyword" && k.name === ":doc"); + if (docEntry && docEntry[1].kind === "string") + doc = docEntry[1].value; + const argsEntry = meta.entries.find(([k]) => k.kind === "keyword" && k.name === ":arglists"); + if (argsEntry && argsEntry[1].kind === "vector") { + const arglists = argsEntry[1]; + arglistsStr = "(" + arglists.value.map((al) => printString(al)).join(" ") + ")"; + eldocArgs = arglists.value.map((al) => { + if (al.kind !== "vector") + return [printString(al)]; + return al.value.map((p) => p.kind === "symbol" ? p.name : printString(p)); + }); + } + } + if (arglistsStr === "" && (value.kind === "function" || value.kind === "macro")) { + const arityStrs = value.arities.map((arity) => { + const params = arity.params.map((p) => printString(p)); + if (arity.restParam) + params.push("&", printString(arity.restParam)); + return "[" + params.join(" ") + "]"; + }); + arglistsStr = "(" + arityStrs.join(" ") + ")"; + eldocArgs = value.arities.map((arity) => { + const params = arity.params.map((p) => printString(p)); + if (arity.restParam) + params.push("&", printString(arity.restParam)); + return params; + }); + } + return { doc, arglistsStr, eldocArgs, type }; +} // src/vite-plugin-clj/nrepl-relay.ts function makeId() { @@ -5889,16 +9936,16 @@ async function forwardToB(event, data, ws, pending, timeoutMs = 15000) { if (ws.clients.size === 0) { return { id: correlationId, error: "No browser tab connected to Vite dev server" }; } - return new Promise((resolve) => { + return new Promise((resolve2) => { const timer = setTimeout(() => { if (pending.has(correlationId)) { pending.delete(correlationId); - resolve({ id: correlationId, error: "Timed out — no response from browser (15s)" }); + resolve2({ id: correlationId, error: "Timed out — no response from browser (15s)" }); } }, timeoutMs); pending.set(correlationId, (result) => { clearTimeout(timer); - resolve(result); + resolve2(result); }); ws.send({ type: "custom", event, data: { ...data, id: correlationId } }); }); @@ -5945,6 +9992,53 @@ function handleClose(msg, sessions, encoder) { sessions.delete(sessionId); send(encoder, { id, session: sessionId, status: ["done"] }); } +function handleInfo(msg, session, encoder, serverSession) { + const id = msg["id"] ?? ""; + const sym = msg["sym"]; + const nsOverride = msg["ns"]; + if (!sym) { + done(encoder, id, session.id, { status: ["no-info", "done"] }); + return; + } + const resolved = resolveSymbol(sym, serverSession, nsOverride ?? session.currentNs); + if (!resolved) { + done(encoder, id, session.id, { status: ["no-info", "done"] }); + return; + } + const meta = extractMeta(resolved.value, resolved.varObj?.meta); + done(encoder, id, session.id, { + ns: resolved.resolvedNs, + name: resolved.localName, + doc: meta.doc, + "arglists-str": meta.arglistsStr, + type: meta.type + }); +} +function handleEldoc(msg, session, encoder, serverSession) { + const id = msg["id"] ?? ""; + const sym = msg["sym"]; + const nsOverride = msg["ns"]; + if (!sym) { + done(encoder, id, session.id, { status: ["no-eldoc", "done"] }); + return; + } + const resolved = resolveSymbol(sym, serverSession, nsOverride ?? session.currentNs); + if (!resolved) { + done(encoder, id, session.id, { status: ["no-eldoc", "done"] }); + return; + } + const meta = extractMeta(resolved.value, resolved.varObj?.meta); + if (!meta.eldocArgs) { + done(encoder, id, session.id, { status: ["no-eldoc", "done"] }); + return; + } + done(encoder, id, session.id, { + name: resolved.localName, + ns: resolved.resolvedNs, + type: meta.type, + eldoc: meta.eldocArgs + }); +} function handleUnknown(msg, encoder) { const id = msg["id"] ?? ""; send(encoder, { id, status: ["unknown-op", "done"] }); @@ -6017,18 +10111,10 @@ async function handleMessage(msg, sessions, defaultSession, encoder, ws, pending break; case "info": case "lookup": - send(encoder, { - id: msg["id"] ?? "", - session: session.id, - status: ["no-info", "done"] - }); + handleInfo(msg, session, encoder, serverSession); break; case "eldoc": - send(encoder, { - id: msg["id"] ?? "", - session: session.id, - status: ["no-eldoc", "done"] - }); + handleEldoc(msg, session, encoder, serverSession); break; default: handleUnknown(msg, encoder); @@ -6040,17 +10126,17 @@ function startBrowserNreplRelay(options) { const { ws, serverSession, cwd } = options; const pending = new Map; ws.on("conjure:eval-result", (data) => { - const resolve = pending.get(data.id); - if (resolve) { + const resolve2 = pending.get(data.id); + if (resolve2) { pending.delete(data.id); - resolve(data); + resolve2(data); } }); ws.on("conjure:load-file-result", (data) => { - const resolve = pending.get(data.id); - if (resolve) { + const resolve2 = pending.get(data.id); + if (resolve2) { pending.delete(data.id); - resolve(data); + resolve2(data); } }); const server = net.createServer((socket) => { @@ -6094,13 +10180,13 @@ function startBrowserNreplRelay(options) { // src/vite-plugin-clj/index.ts function resolveCoreIndexPath() { - const thisDir = dirname(fileURLToPath(import.meta.url)); - const fromSource = resolve(thisDir, "../core/index.ts"); + const thisDir = dirname2(fileURLToPath(import.meta.url)); + const fromSource = resolve2(thisDir, "../core/index.ts"); try { statSync(fromSource); return fromSource; } catch { - return resolve(thisDir, "../src/core/index.ts"); + return resolve2(thisDir, "../src/core/index.ts"); } } var VIRTUAL_SESSION_ID = "virtual:clj-session"; @@ -6113,6 +10199,8 @@ function cljPlugin(options) { let codegenCtx; let generatorScriptPath; let serveMode = false; + let stringRequires = []; + let entrypointPath = null; function writeFileIfChanged(path, content) { try { const existing = readFileSync(path, "utf-8"); @@ -6149,13 +10237,14 @@ function cljPlugin(options) { } function eagerlyGenerateDts() { for (const root of sourceRoots) { - const rootPath = resolve(projectRoot, root); + const rootPath = resolve2(projectRoot, root); for (const filePath of collectCljFiles(rootPath)) { try { const source = readFileSync(filePath, "utf-8"); const nsNameFromPath = pathToNs(relative(projectRoot, filePath), sourceRoots); const dts = generateDts(codegenCtx, nsNameFromPath, source); - writeFileIfChanged(filePath + ".d.ts", dts); + if (dts) + writeFileIfChanged(filePath + ".d.ts", dts); } catch { continue; } @@ -6163,19 +10252,38 @@ function cljPlugin(options) { } } function initServerSession() { + const projectRequire = createRequire(resolve2(projectRoot, "package.json")); serverSession = createSession({ sourceRoots, - readFile: (filePath) => readFileSync(resolve(projectRoot, filePath), "utf-8"), - output: () => {} + readFile: (filePath) => readFileSync(resolve2(projectRoot, filePath), "utf-8"), + output: () => {}, + importModule: async (s) => { + if (!s.startsWith(".") && !s.startsWith("/")) { + try { + const resolved = projectRequire.resolve(s); + return import(pathToFileURL(resolved).href); + } catch { + try { + return await import(s); + } catch { + return {}; + } + } + } + try { + return await import(s); + } catch { + return {}; + } + } }); codegenCtx = { - session: serverSession, sourceRoots, coreIndexPath, virtualSessionId: VIRTUAL_SESSION_ID, resolveDepPath: (depNs) => { for (const root of sourceRoots) { - const depPath = resolve(projectRoot, nsToPath(depNs, root)); + const depPath = resolve2(projectRoot, nsToPath(depNs, root)); try { readFileSync(depPath); return depPath; @@ -6187,6 +10295,28 @@ function cljPlugin(options) { } }; } + function scanStringRequires() { + const seen = new Map; + for (const root of sourceRoots) { + const rootPath = resolve2(projectRoot, root); + for (const filePath of collectCljFiles(rootPath)) { + try { + const source = readFileSync(filePath, "utf-8"); + const originals = extractStringRequires(source); + const resolved = extractStringRequires(source, filePath); + for (let i = 0;i < originals.length; i++) { + seen.set(originals[i], resolved[i]); + } + } catch { + continue; + } + } + } + stringRequires = [...seen.entries()].map(([original, resolved]) => ({ + original, + resolved + })); + } function regenerateBuiltInNamespaceSources() { try { statSync(generatorScriptPath); @@ -6203,15 +10333,35 @@ function cljPlugin(options) { throw new Error(`Failed to generate built-in namespace sources: ${message}`); } } + function buildImportTable() { + const importLines = []; + const mapEntries = []; + stringRequires.forEach(({ original, resolved }, i) => { + const varName = `_imp_${i}`; + importLines.push(`import * as ${varName} from ${JSON.stringify(resolved)};`); + mapEntries.push(` ${JSON.stringify(original)}: ${varName},`); + }); + return { importLines, mapEntries }; + } return { name: "vite-plugin-clj", configResolved(config) { projectRoot = config.root; serveMode = config.command === "serve"; - generatorScriptPath = resolve(projectRoot, "scripts/gen-core-source.mjs"); + generatorScriptPath = resolve2(projectRoot, "scripts/gen-core-source.mjs"); regenerateBuiltInNamespaceSources(); coreIndexPath = resolveCoreIndexPath(); initServerSession(); + if (options?.entrypoint) { + const ep = resolve2(projectRoot, options.entrypoint); + try { + statSync(ep); + entrypointPath = ep; + } catch { + console.warn(`[vite-plugin-clj] entrypoint not found: ${options.entrypoint} — falling back to auto-generated session`); + } + } + scanStringRequires(); eagerlyGenerateDts(); }, configureServer(server) { @@ -6233,20 +10383,26 @@ function cljPlugin(options) { }, load(id) { if (id === RESOLVED_VIRTUAL_SESSION_ID) { + const { importLines, mapEntries } = buildImportTable(); const lines = [ `import { createSession, printString } from ${JSON.stringify(coreIndexPath)};`, + ...importLines, + ...entrypointPath ? [`import __conjureFactory from ${JSON.stringify(entrypointPath)};`] : [], + ``, + `const __importMap = {`, + ...mapEntries, + `};`, ``, `let _session = null;`, - `let _outputLines = [];`, - `export function getSession() {`, - ` if (!_session) {`, - ` _session = createSession({ output: (text) => { _outputLines.push(text); console.log(text.replace(/\\n$/, '')); } });`, - ` }`, - ` return _session;`, - `}` + `let _outputLines = [];` ]; + if (entrypointPath) { + lines.push(`export function getSession() {`, ` if (!_session) {`, ` const _outputCapture = (text) => { _outputLines.push(text); };`, ` _session = __conjureFactory(__importMap, _outputCapture);`, ` }`, ` return _session;`, `}`); + } else { + lines.push(`export function getSession() {`, ` if (!_session) {`, ` _session = createSession({`, ` importModule: (s) => __importMap[s],`, ` output: (text) => { _outputLines.push(text); console.log(text.replace(/\\n$/, '')); },`, ` });`, ` }`, ` return _session;`, `}`); + } if (serveMode) { - lines.push(``, `// Browser nREPL relay — active only in Vite dev server`, `if (import.meta.hot) {`, ` import.meta.hot.on('conjure:eval', ({ id, code, ns }) => {`, ` const session = getSession();`, ` _outputLines = [];`, ` try {`, ` if (ns && ns !== session.currentNs) session.setNs(ns);`, ` const result = session.evaluate(code);`, ` const out = _outputLines.join('');`, ` import.meta.hot.send('conjure:eval-result', { id, value: printString(result), ns: session.currentNs, ...(out ? { out } : {}) });`, ` } catch (err) {`, ` console.error(err);`, ` const out = _outputLines.join('');`, ` import.meta.hot.send('conjure:eval-result', { id, error: err instanceof Error ? err.message : String(err), ns: session.currentNs, ...(out ? { out } : {}) });`, ` }`, ` });`, ``, ` import.meta.hot.on('conjure:load-file', ({ id, source, nsHint, filePath }) => {`, ` const session = getSession();`, ` _outputLines = [];`, ` try {`, ` const loadedNs = session.loadFile(source, nsHint, filePath || undefined);`, ` if (loadedNs) session.setNs(loadedNs);`, ` const out = _outputLines.join('');`, ` import.meta.hot.send('conjure:load-file-result', { id, value: 'nil', ns: session.currentNs, ...(out ? { out } : {}) });`, ` } catch (err) {`, ` console.error(err);`, ` const out = _outputLines.join('');`, ` import.meta.hot.send('conjure:load-file-result', { id, error: err instanceof Error ? err.message : String(err), ns: session.currentNs, ...(out ? { out } : {}) });`, ` }`, ` });`, `}`); + lines.push(``, `// Browser nREPL relay — active only in Vite dev server`, `if (import.meta.hot) {`, ` import.meta.hot.on('conjure:eval', async ({ id, code, ns }) => {`, ` const session = getSession();`, ` _outputLines = [];`, ` try {`, ` if (ns && ns !== session.currentNs) session.setNs(ns);`, ` const result = await session.evaluateAsync(code);`, ` const out = _outputLines.join('');`, ` import.meta.hot.send('conjure:eval-result', { id, value: printString(result), ns: session.currentNs, ...(out ? { out } : {}) });`, ` } catch (err) {`, ` console.error(err);`, ` const out = _outputLines.join('');`, ` import.meta.hot.send('conjure:eval-result', { id, error: err instanceof Error ? err.message : String(err), ns: session.currentNs, ...(out ? { out } : {}) });`, ` }`, ` });`, ``, ` import.meta.hot.on('conjure:load-file', async ({ id, source, nsHint, filePath }) => {`, ` const session = getSession();`, ` _outputLines = [];`, ` try {`, ` const loadedNs = await session.loadFileAsync(source, nsHint, filePath || undefined);`, ` if (loadedNs) session.setNs(loadedNs);`, ` const out = _outputLines.join('');`, ` import.meta.hot.send('conjure:load-file-result', { id, value: 'nil', ns: session.currentNs, ...(out ? { out } : {}) });`, ` } catch (err) {`, ` console.error(err);`, ` const out = _outputLines.join('');`, ` import.meta.hot.send('conjure:load-file-result', { id, error: err instanceof Error ? err.message : String(err), ns: session.currentNs, ...(out ? { out } : {}) });`, ` }`, ` });`, `}`); } return lines.join(` `); @@ -6254,9 +10410,10 @@ function cljPlugin(options) { if (id.endsWith(".clj") && !id.includes("?")) { const source = readFileSync(id, "utf-8"); const nsNameFromPath = pathToNs(relative(projectRoot, id), sourceRoots); - const code = generateModuleCode(codegenCtx, nsNameFromPath, source); + const code = generateModuleCode(codegenCtx, nsNameFromPath, source, id); const dts = generateDts(codegenCtx, nsNameFromPath, source); - writeFileIfChanged(id + ".d.ts", dts); + if (dts) + writeFileIfChanged(id + ".d.ts", dts); return code; } }, @@ -6264,13 +10421,13 @@ function cljPlugin(options) { if (!file.endsWith(".clj")) return; const doUpdate = async () => { - if (file.startsWith(resolve(projectRoot, "src/clojure") + "/")) { + if (file.startsWith(resolve2(projectRoot, "src/clojure") + "/")) { regenerateBuiltInNamespaceSources(); } const source = await read(); try { const nsNameFromPath = pathToNs(relative(projectRoot, file), sourceRoots); - serverSession.loadFile(source, nsNameFromPath); + await serverSession.loadFileAsync(source, nsNameFromPath); const dts = generateDts(codegenCtx, nsNameFromPath, source); writeFileIfChanged(file + ".d.ts", dts); } catch {} diff --git a/packages/conjure-js/package.json b/packages/conjure-js/package.json index 5497f62..bf58d53 100644 --- a/packages/conjure-js/package.json +++ b/packages/conjure-js/package.json @@ -1,7 +1,7 @@ { "name": "conjure-js", "private": false, - "version": "0.0.12", + "version": "0.0.13", "type": "module", "repository": { "type": "git", diff --git a/packages/conjure-js/src/bin/nrepl.ts b/packages/conjure-js/src/bin/nrepl.ts index c35b982..8bec093 100644 --- a/packages/conjure-js/src/bin/nrepl.ts +++ b/packages/conjure-js/src/bin/nrepl.ts @@ -14,7 +14,7 @@ import { type SessionSnapshot, } from '../core' import { withPrintContext } from '../core/printer' -import { tryLookup } from '../core/env' +import { derefValue } from '../core/env' import { inferSourceRoot } from './nrepl-utils' import { resolveSymbol as resolveSymbolShared, @@ -70,7 +70,8 @@ function createManagedSession( snapshot: SessionSnapshot, encoder: BEncoderStream, sourceRoots?: string[], - onOutput?: (text: string) => void + onOutput?: (text: string) => void, + importModule?: (specifier: string) => unknown | Promise ): ManagedSession { let currentMsgId = '' @@ -81,6 +82,7 @@ function createManagedSession( }, readFile: (filePath) => readFileSync(filePath, 'utf8'), sourceRoots, + importModule, }) session.runtime.installModules([makeNodeHostModule(session)]) @@ -131,11 +133,12 @@ function handleClone( snapshot: SessionSnapshot, encoder: BEncoderStream, sourceRoots?: string[], - onOutput?: (text: string) => void + onOutput?: (text: string) => void, + importModule?: (specifier: string) => unknown | Promise ) { const id = (msg['id'] as string) ?? '' const newId = makeSessionId() - const managed = createManagedSession(newId, snapshot, encoder, sourceRoots, onOutput) + const managed = createManagedSession(newId, snapshot, encoder, sourceRoots, onOutput, importModule) sessions.set(newId, managed) done(encoder, id, undefined, { 'new-session': newId }) } @@ -285,9 +288,15 @@ async function handleEval( lineOffset, colOffset, }) - const nsEnv = managed.session.registry.get(managed.session.currentNs) - const printLen = nsEnv ? tryLookup('*print-length*', nsEnv) : undefined - const printLvl = nsEnv ? tryLookup('*print-level*', nsEnv) : undefined + // Resolve *print-length* / *print-level* via the registry (same rationale + // as emitToOut in core-module.ts: session.getNs goes through the runtime + // registry so we always get the session's own freshly-cloned var, not a + // stale one from a snapshot closure env). + const coreNs = managed.session.getNs('clojure.core') + const lenVar = coreNs?.vars.get('*print-length*') + const lvlVar = coreNs?.vars.get('*print-level*') + const printLen = lenVar ? derefValue(lenVar) : undefined + const printLvl = lvlVar ? derefValue(lvlVar) : undefined const resultStr = withPrintContext( { printLength: printLen?.kind === 'number' ? printLen.value : null, @@ -310,11 +319,11 @@ async function handleEval( } } -function handleLoadFile( +async function handleLoadFile( msg: NreplMessage, managed: ManagedSession, encoder: BEncoderStream -) { +): Promise { const id = (msg['id'] as string) ?? '' const source = (msg['file'] as string) ?? '' const fileName = (msg['file-name'] as string) ?? '' @@ -335,7 +344,10 @@ function handleLoadFile( const nsHint = fileName.replace(/\.clj$/, '').replace(/\//g, '.') || undefined - const loadedNs = managed.session.loadFile( + + // loadFileAsync handles both sync and string (:require ["pkg" :as X]) forms. + // It falls back to the sync path internally if no async requires are present. + const loadedNs = await managed.session.loadFileAsync( source, nsHint, filePath || undefined @@ -528,7 +540,8 @@ function handleMessage( defaultSession: ManagedSession, sourceRoots?: string[], meshNode?: RemoteEvalNode, - onOutput?: (text: string) => void + onOutput?: (text: string) => void, + importModule?: (specifier: string) => unknown | Promise ) { const op = msg['op'] as string const sessionId = msg['session'] as string | undefined @@ -538,7 +551,7 @@ function handleMessage( switch (op) { case 'clone': - handleClone(msg, sessions, snapshot, encoder, sourceRoots, onOutput) + handleClone(msg, sessions, snapshot, encoder, sourceRoots, onOutput, importModule) break case 'describe': handleDescribe(msg, encoder) @@ -555,7 +568,15 @@ function handleMessage( }) break case 'load-file': - handleLoadFile(msg, managed, encoder) + void handleLoadFile(msg, managed, encoder).catch((e) => { + const m = e instanceof Error ? e.message : String(e) + done(encoder, (msg['id'] as string) ?? '', managed.id, { + ex: m, + err: m + '\n', + ns: managed.session.currentNs, + status: ['eval-error', 'done'], + }) + }) break case 'complete': handleComplete(msg, managed, encoder) @@ -599,6 +620,12 @@ export type NreplServerOptions = { * Example: onOutput: (t) => process.stdout.write(t) */ onOutput?: (text: string) => void + /** + * Called when (:require ["specifier" :as Alias]) is encountered in an eval. + * Forwarded to every managed session cloned from the snapshot. + * Example: importModule: (s) => import(s) + */ + importModule?: (specifier: string) => unknown | Promise } export function startNreplServer(options: NreplServerOptions = {}): net.Server { @@ -618,7 +645,7 @@ export function startNreplServer(options: NreplServerOptions = {}): net.Server { }) )) - const { meshNode, onOutput } = options + const { meshNode, onOutput, importModule } = options const server = net.createServer((socket) => { const encoder = new BEncoderStream() @@ -636,7 +663,8 @@ export function startNreplServer(options: NreplServerOptions = {}): net.Server { snapshot, encoder, options.sourceRoots, - onOutput + onOutput, + importModule ) sessions.set(defaultId, defaultSession) @@ -649,7 +677,8 @@ export function startNreplServer(options: NreplServerOptions = {}): net.Server { defaultSession, options.sourceRoots, meshNode, - onOutput + onOutput, + importModule ) }) diff --git a/packages/conjure-js/src/clojure/core.clj b/packages/conjure-js/src/clojure/core.clj index 94bd2ba..2a69996 100644 --- a/packages/conjure-js/src/clojure/core.clj +++ b/packages/conjure-js/src/clojure/core.clj @@ -12,9 +12,15 @@ arglists (if (vector? (first rest-decl)) (vector (first rest-decl)) (reduce (fn [acc arity] (conj acc (first arity))) [] rest-decl)) - meta-map (if doc {:doc doc :arglists arglists} {:arglists arglists})] + meta-map (let [m (if doc {:doc doc :arglists arglists} {:arglists arglists})] + (if (:private (meta name)) (assoc m :private true) m))] `(def ~(with-meta name meta-map) (fn ~@rest-decl)))) +(defmacro defn- + "Same as defn, but marks the var as private." + [name & fdecl] + (list* 'defn (with-meta name (assoc (meta name) :private true)) fdecl)) + (defn vary-meta "Returns an object of the same type and value as obj, with diff --git a/packages/conjure-js/src/clojure/generated/clojure-core-source.ts b/packages/conjure-js/src/clojure/generated/clojure-core-source.ts index 16ba338..eda6c80 100644 --- a/packages/conjure-js/src/clojure/generated/clojure-core-source.ts +++ b/packages/conjure-js/src/clojure/generated/clojure-core-source.ts @@ -15,9 +15,15 @@ export const clojure_coreSource = `\ arglists (if (vector? (first rest-decl)) (vector (first rest-decl)) (reduce (fn [acc arity] (conj acc (first arity))) [] rest-decl)) - meta-map (if doc {:doc doc :arglists arglists} {:arglists arglists})] + meta-map (let [m (if doc {:doc doc :arglists arglists} {:arglists arglists})] + (if (:private (meta name)) (assoc m :private true) m))] \`(def ~(with-meta name meta-map) (fn ~@rest-decl)))) +(defmacro defn- + "Same as defn, but marks the var as private." + [name & fdecl] + (list* 'defn (with-meta name (assoc (meta name) :private true)) fdecl)) + (defn vary-meta "Returns an object of the same type and value as obj, with diff --git a/packages/conjure-js/src/core/__tests__/clojure-set.spec.ts b/packages/conjure-js/src/core/__tests__/clojure-set.spec.ts index d004486..4b911f3 100644 --- a/packages/conjure-js/src/core/__tests__/clojure-set.spec.ts +++ b/packages/conjure-js/src/core/__tests__/clojure-set.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { freshSession } from './evaluator-test-utils' +import { freshSession } from '../evaluator/__tests__/evaluator-test-utils' function s() { const session = freshSession() diff --git a/packages/conjure-js/src/core/__tests__/conversions.spec.ts b/packages/conjure-js/src/core/__tests__/conversions.spec.ts index e8337f8..51c7058 100644 --- a/packages/conjure-js/src/core/__tests__/conversions.spec.ts +++ b/packages/conjure-js/src/core/__tests__/conversions.spec.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' -import { cljToJs, ConversionError, jsToClj } from '../conversions' +import { ConversionError, jsToClj } from '../conversions' import { applyFunction } from '../evaluator' +import { createSession } from '../session' import { cljBoolean, cljFunction, @@ -18,51 +19,53 @@ import { import { makeEnv } from '../env' import type { CljValue } from '../types' +const session = createSession() + describe('cljToJs', () => { describe('primitives', () => { it('converts CljNumber to number', () => { - expect(cljToJs(cljNumber(42))).toBe(42) - expect(cljToJs(cljNumber(0))).toBe(0) - expect(cljToJs(cljNumber(-3.14))).toBe(-3.14) + expect(session.cljToJs(cljNumber(42))).toBe(42) + expect(session.cljToJs(cljNumber(0))).toBe(0) + expect(session.cljToJs(cljNumber(-3.14))).toBe(-3.14) }) it('converts CljString to string', () => { - expect(cljToJs(cljString('hello'))).toBe('hello') - expect(cljToJs(cljString(''))).toBe('') + expect(session.cljToJs(cljString('hello'))).toBe('hello') + expect(session.cljToJs(cljString(''))).toBe('') }) it('converts CljBoolean to boolean', () => { - expect(cljToJs(cljBoolean(true))).toBe(true) - expect(cljToJs(cljBoolean(false))).toBe(false) + expect(session.cljToJs(cljBoolean(true))).toBe(true) + expect(session.cljToJs(cljBoolean(false))).toBe(false) }) it('converts CljNil to null', () => { - expect(cljToJs(cljNil())).toBe(null) + expect(session.cljToJs(cljNil())).toBe(null) }) it('converts CljKeyword to string without colon', () => { - expect(cljToJs(cljKeyword(':foo'))).toBe('foo') - expect(cljToJs(cljKeyword(':hello-world'))).toBe('hello-world') + expect(session.cljToJs(cljKeyword(':foo'))).toBe('foo') + expect(session.cljToJs(cljKeyword(':hello-world'))).toBe('hello-world') }) it('converts CljSymbol to string', () => { - expect(cljToJs(cljSymbol('my-var'))).toBe('my-var') + expect(session.cljToJs(cljSymbol('my-var'))).toBe('my-var') }) }) describe('collections', () => { it('converts CljVector to array', () => { const vec = cljVector([cljNumber(1), cljNumber(2), cljNumber(3)]) - expect(cljToJs(vec)).toEqual([1, 2, 3]) + expect(session.cljToJs(vec)).toEqual([1, 2, 3]) }) it('converts CljList to array', () => { const list = cljList([cljString('a'), cljString('b')]) - expect(cljToJs(list)).toEqual(['a', 'b']) + expect(session.cljToJs(list)).toEqual(['a', 'b']) }) it('converts empty vector to empty array', () => { - expect(cljToJs(cljVector([]))).toEqual([]) + expect(session.cljToJs(cljVector([]))).toEqual([]) }) it('converts nested vectors recursively', () => { @@ -70,7 +73,7 @@ describe('cljToJs', () => { cljNumber(1), cljVector([cljNumber(2), cljNumber(3)]), ]) - expect(cljToJs(nested)).toEqual([1, [2, 3]]) + expect(session.cljToJs(nested)).toEqual([1, [2, 3]]) }) it('converts CljMap with keyword keys to object', () => { @@ -78,7 +81,7 @@ describe('cljToJs', () => { [cljKeyword(':name'), cljString('alice')], [cljKeyword(':age'), cljNumber(30)], ]) - expect(cljToJs(map)).toEqual({ name: 'alice', age: 30 }) + expect(session.cljToJs(map)).toEqual({ name: 'alice', age: 30 }) }) it('converts CljMap with string keys to object', () => { @@ -86,7 +89,7 @@ describe('cljToJs', () => { [cljString('x'), cljNumber(1)], [cljString('y'), cljNumber(2)], ]) - expect(cljToJs(map)).toEqual({ x: 1, y: 2 }) + expect(session.cljToJs(map)).toEqual({ x: 1, y: 2 }) }) it('converts CljMap with number keys to object', () => { @@ -94,7 +97,7 @@ describe('cljToJs', () => { [cljNumber(0), cljString('zero')], [cljNumber(1), cljString('one')], ]) - expect(cljToJs(map)).toEqual({ '0': 'zero', '1': 'one' }) + expect(session.cljToJs(map)).toEqual({ '0': 'zero', '1': 'one' }) }) it('converts nested map values recursively', () => { @@ -107,35 +110,35 @@ describe('cljToJs', () => { ]), ], ]) - expect(cljToJs(map)).toEqual({ + expect(session.cljToJs(map)).toEqual({ person: { name: 'bob', scores: [10, 20] }, }) }) it('converts empty map to empty object', () => { - expect(cljToJs(cljMap([]))).toEqual({}) + expect(session.cljToJs(cljMap([]))).toEqual({}) }) it('throws ConversionError for vector keys in maps', () => { const map = cljMap([ [cljVector([cljNumber(1), cljNumber(2)]), cljString('value')], ]) - expect(() => cljToJs(map)).toThrow(ConversionError) - expect(() => cljToJs(map)).toThrow('Rich key types') + expect(() => session.cljToJs(map)).toThrow(ConversionError) + expect(() => session.cljToJs(map)).toThrow('Rich key types') }) it('throws ConversionError for list keys in maps', () => { const map = cljMap([ [cljList([cljNumber(1)]), cljString('value')], ]) - expect(() => cljToJs(map)).toThrow(ConversionError) + expect(() => session.cljToJs(map)).toThrow(ConversionError) }) it('throws ConversionError for map keys in maps', () => { const map = cljMap([ [cljMap([[cljKeyword(':a'), cljNumber(1)]]), cljString('value')], ]) - expect(() => cljToJs(map)).toThrow(ConversionError) + expect(() => session.cljToJs(map)).toThrow(ConversionError) }) }) @@ -146,7 +149,7 @@ describe('cljToJs', () => { throw new Error('expected numbers') return cljNumber(a.value + b.value) }) - const jsFn = cljToJs(add) as (...args: unknown[]) => unknown + const jsFn = session.cljToJs(add) as (...args: unknown[]) => unknown expect(typeof jsFn).toBe('function') expect(jsFn(3, 4)).toBe(7) }) @@ -171,7 +174,7 @@ describe('cljToJs', () => { env ) - const jsFn = cljToJs(fn) as (...args: unknown[]) => unknown + const jsFn = session.cljToJs(fn) as (...args: unknown[]) => unknown expect(typeof jsFn).toBe('function') expect(jsFn(5)).toBe(15) }) @@ -180,13 +183,13 @@ describe('cljToJs', () => { const vecFn = cljNativeFunction('make-vec', () => cljVector([cljNumber(1), cljNumber(2)]) ) - const jsFn = cljToJs(vecFn) as () => unknown + const jsFn = session.cljToJs(vecFn) as () => unknown expect(jsFn()).toEqual([1, 2]) }) it('function wrapper converts JS args to Clj', () => { const identity = cljNativeFunction('identity', (x: CljValue) => x) - const jsFn = cljToJs(identity) as (x: unknown) => unknown + const jsFn = session.cljToJs(identity) as (x: unknown) => unknown expect(jsFn('hello')).toBe('hello') expect(jsFn(42)).toBe(42) expect(jsFn(null)).toBe(null) @@ -197,8 +200,8 @@ describe('cljToJs', () => { describe('macros', () => { it('throws ConversionError for macros', () => { const macro = cljMacro([cljSymbol('x')], null, [cljSymbol('x')], makeEnv()) - expect(() => cljToJs(macro)).toThrow(ConversionError) - expect(() => cljToJs(macro)).toThrow('Macros cannot be exported') + expect(() => session.cljToJs(macro)).toThrow(ConversionError) + expect(() => session.cljToJs(macro)).toThrow('Macros cannot be exported') }) }) }) @@ -225,8 +228,10 @@ describe('jsToClj', () => { expect(jsToClj(null)).toEqual(cljNil()) }) - it('converts undefined to CljNil', () => { - expect(jsToClj(undefined)).toEqual(cljNil()) + it('converts undefined to CljJsValue wrapping undefined', () => { + const result = jsToClj(undefined) + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toBeUndefined() }) }) @@ -284,6 +289,25 @@ describe('jsToClj', () => { ]) ) }) + + it('uses string keys when keywordizeKeys is false', () => { + const result = jsToClj({ name: 'alice', age: 30 }, { keywordizeKeys: false }) + expect(result).toEqual( + cljMap([ + [cljString('name'), cljString('alice')], + [cljString('age'), cljNumber(30)], + ]) + ) + }) + + it('propagates keywordizeKeys: false recursively', () => { + const result = jsToClj({ person: { name: 'bob' } }, { keywordizeKeys: false }) + expect(result).toEqual( + cljMap([ + [cljString('person'), cljMap([[cljString('name'), cljString('bob')]])], + ]) + ) + }) }) describe('functions', () => { @@ -351,28 +375,28 @@ describe('jsToClj', () => { describe('roundtrip conversions', () => { it('number roundtrips', () => { - expect(cljToJs(jsToClj(42))).toBe(42) + expect(session.cljToJs(jsToClj(42))).toBe(42) }) it('string roundtrips', () => { - expect(cljToJs(jsToClj('hello'))).toBe('hello') + expect(session.cljToJs(jsToClj('hello'))).toBe('hello') }) it('boolean roundtrips', () => { - expect(cljToJs(jsToClj(true))).toBe(true) - expect(cljToJs(jsToClj(false))).toBe(false) + expect(session.cljToJs(jsToClj(true))).toBe(true) + expect(session.cljToJs(jsToClj(false))).toBe(false) }) it('null roundtrips', () => { - expect(cljToJs(jsToClj(null))).toBe(null) + expect(session.cljToJs(jsToClj(null))).toBe(null) }) it('array roundtrips', () => { - expect(cljToJs(jsToClj([1, 2, 3]))).toEqual([1, 2, 3]) + expect(session.cljToJs(jsToClj([1, 2, 3]))).toEqual([1, 2, 3]) }) it('nested structure roundtrips', () => { const original = { name: 'alice', scores: [10, 20], active: true } - expect(cljToJs(jsToClj(original))).toEqual(original) + expect(session.cljToJs(jsToClj(original))).toEqual(original) }) }) diff --git a/packages/conjure-js/src/core/__tests__/js-interop/js-interop-clj-fns.spec.ts b/packages/conjure-js/src/core/__tests__/js-interop/js-interop-clj-fns.spec.ts new file mode 100644 index 0000000..42e3ccc --- /dev/null +++ b/packages/conjure-js/src/core/__tests__/js-interop/js-interop-clj-fns.spec.ts @@ -0,0 +1,181 @@ +/** + * Tests for the Clojure-callable `clj->js` and `js->clj` functions. + */ + +import { describe, expect, it } from 'vitest' +import { cljJsValue, cljNil, cljNumber, cljString, cljVar } from '../../factories' +import type { Session } from '../../session' +import { freshSession } from '../../evaluator/__tests__/evaluator-test-utils' +import { expectError } from '../../evaluator/__tests__/evaluator-test-utils' + +function sessionWithJs(bindings: Record): Session { + const session = freshSession() + const ns = session.getNs('user')! + for (const [name, rawValue] of Object.entries(bindings)) { + ns.vars.set(name, cljVar('user', name, cljJsValue(rawValue))) + } + return session +} + +// --------------------------------------------------------------------------- +// clj->js +// --------------------------------------------------------------------------- + +describe('clj->js', () => { + it('converts a number to a CljJsValue wrapping the raw number', () => { + const result = freshSession().evaluate('(clj->js 42)') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toBe(42) + }) + + it('converts a string', () => { + const result = freshSession().evaluate('(clj->js "hello")') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toBe('hello') + }) + + it('converts nil to a CljJsValue wrapping null', () => { + const result = freshSession().evaluate('(clj->js nil)') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toBe(null) + }) + + it('converts a keyword to a plain string (no colon)', () => { + const result = freshSession().evaluate('(clj->js :foo)') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toBe('foo') + }) + + it('converts a vector to a JS array', () => { + const result = freshSession().evaluate('(clj->js [1 2 3])') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toEqual([1, 2, 3]) + }) + + it('converts a list to a JS array', () => { + const result = freshSession().evaluate("(clj->js '(1 2 3))") + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toEqual([1, 2, 3]) + }) + + it('converts a map with keyword keys (colons stripped)', () => { + const result = freshSession().evaluate('(clj->js {:a 1 :b "hello"})') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') + expect(result.value).toEqual({ a: 1, b: 'hello' }) + }) + + it('converts nested structures recursively', () => { + const result = freshSession().evaluate('(clj->js {:users [{:name "alice"} {:name "bob"}]})') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') + expect(result.value).toEqual({ + users: [{ name: 'alice' }, { name: 'bob' }], + }) + }) + + it('is a no-op on an already-boxed CljJsValue', () => { + const session = sessionWithJs({ obj: { x: 1 } }) + const result = session.evaluate('(clj->js obj)') + expect(result.kind).toBe('js-value') + // Returns the same CljJsValue — no double-wrapping + if (result.kind === 'js-value') expect(result.value).toEqual({ x: 1 }) + }) + + it('result can be passed directly to a JS method', () => { + const session = sessionWithJs({ + json: JSON, + }) + // Convert a map to a JS object, then stringify it + const result = session.evaluate('(. json stringify (clj->js {:a 1}))') + // JSON.stringify produces a string — comes back as CljString + expect(result.kind).toBe('string') + if (result.kind === 'string') expect(JSON.parse(result.value)).toEqual({ a: 1 }) + }) +}) + +// --------------------------------------------------------------------------- +// js->clj +// --------------------------------------------------------------------------- + +describe('js->clj', () => { + it('converts a boxed number to CljNumber', () => { + const session = sessionWithJs({ n: 42 }) + expect(session.evaluate('(js->clj n)')).toEqual(cljNumber(42)) + }) + + it('converts a boxed string to CljString', () => { + const session = sessionWithJs({ s: 'hello' }) + expect(session.evaluate('(js->clj s)')).toEqual(cljString('hello')) + }) + + it('converts nil through unchanged', () => { + expect(freshSession().evaluate('(js->clj nil)')).toEqual(cljNil()) + }) + + it('converts a JS array to a CljVector', () => { + const session = sessionWithJs({ arr: [1, 2, 3] }) + const result = session.evaluate('(js->clj arr)') + expect(result.kind).toBe('vector') + if (result.kind === 'vector') { + expect(result.value).toHaveLength(3) + expect(result.value[0]).toEqual({ kind: 'number', value: 1 }) + } + }) + + it('converts a plain JS object to a CljMap with string keys by default', () => { + const session = sessionWithJs({ obj: { name: 'alice', age: 30 } }) + const result = session.evaluate('(js->clj obj)') + expect(result.kind).toBe('map') + if (result.kind === 'map') { + // keys are strings, not keywords + const keys = result.entries.map(([k]) => k) + expect(keys.every((k) => k.kind === 'string')).toBe(true) + } + }) + + it('keywordizes keys with {:keywordize-keys true}', () => { + const session = sessionWithJs({ obj: { name: 'alice' } }) + const result = session.evaluate('(js->clj obj {:keywordize-keys true})') + expect(result.kind).toBe('map') + if (result.kind === 'map') { + const [k] = result.entries[0] + expect(k.kind).toBe('keyword') + if (k.kind === 'keyword') expect(k.name).toBe(':name') + } + }) + + it('keywordizes keys recursively with {:keywordize-keys true}', () => { + const session = sessionWithJs({ obj: { person: { name: 'bob' } } }) + const result = session.evaluate('(js->clj obj {:keywordize-keys true})') + expect(result.kind).toBe('map') + if (result.kind === 'map') { + const inner = result.entries[0][1] + expect(inner.kind).toBe('map') + if (inner.kind === 'map') { + const [k] = inner.entries[0] + expect(k.kind).toBe('keyword') + } + } + }) + + it('round-trips with clj->js (string keys default)', () => { + const session = sessionWithJs({ obj: { a: 1, b: [2, 3] } }) + // js->clj gives string-keyed map; clj->js on that gives back {"a":1,"b":[2,3]} + const result = session.evaluate('(clj->js (js->clj obj))') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toEqual({ a: 1, b: [2, 3] }) + }) + + it('throws on non-js-value (number literal)', () => { + expectError('(js->clj 42)', 'js->clj expects a js-value') + }) + + it('throws on non-js-value (string literal)', () => { + expectError('(js->clj "hello")', 'js->clj expects a js-value') + }) + + it('throws on a keyword', () => { + expectError('(js->clj :foo)', 'js->clj expects a js-value') + }) +}) diff --git a/packages/conjure-js/src/core/__tests__/js-interop/js-interop-composition.spec.ts b/packages/conjure-js/src/core/__tests__/js-interop/js-interop-composition.spec.ts new file mode 100644 index 0000000..2ffb07f --- /dev/null +++ b/packages/conjure-js/src/core/__tests__/js-interop/js-interop-composition.spec.ts @@ -0,0 +1,538 @@ +/** + * Tests for JS namespace composition utilities: + * js/get-in, js/prop, js/method, js/merge, js/seq, js/array, js/obj + */ + +import { describe, expect, it } from 'vitest' +import { createSession } from '../../session' +import { freshSession, expectError } from '../../evaluator/__tests__/evaluator-test-utils' + +function jsSession(bindings: Record) { + return createSession({ hostBindings: bindings }) +} + +// --------------------------------------------------------------------------- +// js/get-in +// --------------------------------------------------------------------------- + +describe('js/get-in', () => { + it('traverses a string key path', () => { + const obj = { target: { value: 'hello' } } + const session = jsSession({ target: obj }) + const result = session.evaluate('(js/get-in js/target ["target" "value"])') + expect(result).toEqual({ kind: 'string', value: 'hello' }) + }) + + it('traverses a keyword key path (strips colon)', () => { + const obj = { db: { host: 'localhost' } } + const session = jsSession({ target: obj }) + const result = session.evaluate('(js/get-in js/target [:db :host])') + expect(result).toEqual({ kind: 'string', value: 'localhost' }) + }) + + it('traverses a numeric index in path', () => { + const obj = [{ name: 'Alice' }, { name: 'Bob' }] + const session = jsSession({ target: obj }) + const result = session.evaluate('(js/get-in js/target [1 "name"])') + expect(result).toEqual({ kind: 'string', value: 'Bob' }) + }) + + it('returns js-value(undefined) for missing deep key with no default', () => { + const obj = { a: {} } + const session = jsSession({ target: obj }) + const result = session.evaluate('(js/get-in js/target ["a" "missing"])') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toBeUndefined() + }) + + it('returns default when key is missing', () => { + const obj = {} + const session = jsSession({ target: obj }) + const result = session.evaluate('(js/get-in js/target ["missing"] "fallback")') + expect(result).toEqual({ kind: 'string', value: 'fallback' }) + }) + + it('returns default on mid-path nil/undefined', () => { + const obj = { a: null } + const session = jsSession({ target: obj }) + const result = session.evaluate('(js/get-in js/target ["a" "deep"] "default")') + expect(result).toEqual({ kind: 'string', value: 'default' }) + }) + + it('returns value when it exists (no default override)', () => { + const obj = { x: { y: 0 } } + const session = jsSession({ target: obj }) + const result = session.evaluate('(js/get-in js/target ["x" "y"] 99)') + expect(result).toEqual({ kind: 'number', value: 0 }) + }) + + it('throws on non-vector path', () => { + const session = jsSession({ target:{} }) + expect(() => session.evaluate('(js/get-in js/target "bad-path")')).toThrow( + 'path must be a vector' + ) + }) + + it('throws on nil root', () => { + expect(() => freshSession().evaluate('(js/get-in nil ["key"])')).toThrow( + 'cannot access properties on nil' + ) + }) + + it('empty path returns the object itself', () => { + const obj = { x: 1 } + const session = jsSession({ target: obj }) + const result = session.evaluate('(js/get-in js/target [])') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toEqual(obj) + }) +}) + +// --------------------------------------------------------------------------- +// js/prop +// --------------------------------------------------------------------------- + +describe('js/prop', () => { + it('works as arg to map', () => { + const users = [{ name: 'Alice' }, { name: 'Bob' }] + const session = jsSession({ users }) + const result = session.evaluate( + '(vec (map (js/prop "name") (js/seq js/users)))' + ) + expect(result).toEqual({ + kind: 'vector', + value: [ + { kind: 'string', value: 'Alice' }, + { kind: 'string', value: 'Bob' }, + ], + }) + }) + + it('works as predicate in filter — missing key returns nil (falsy)', () => { + const items = [{ enabled: true }, { enabled: false }, {}, { enabled: true }] + const session = jsSession({ items }) + // items without "enabled" → nil → filtered out; false → also filtered out + const r = session.evaluate( + '(vec (filter (js/prop "enabled") (js/seq js/items)))' + ) + expect(r).toEqual({ + kind: 'vector', + value: [ + { kind: 'js-value', value: { enabled: true } }, + { kind: 'js-value', value: { enabled: true } }, + ], + }) + }) + + it('missing key with no default returns nil', () => { + const session = jsSession({ target:{} }) + const r = session.evaluate('((js/prop "gone") js/target)') + expect(r.kind).toBe('nil') + }) + + it('returns default when key is missing', () => { + const obj = {} + const session = jsSession({ target: obj }) + const r = session.evaluate('((js/prop "score" 0) js/target)') + expect(r).toEqual({ kind: 'number', value: 0 }) + }) + + it('returns property when it exists even with default', () => { + const obj = { score: 42 } + const session = jsSession({ target: obj }) + const r = session.evaluate('((js/prop "score" 0) js/target)') + expect(r).toEqual({ kind: 'number', value: 42 }) + }) + + it('accepts keyword key', () => { + const obj = { name: 'Charlie' } + const session = jsSession({ target: obj }) + const r = session.evaluate('((js/prop :name) js/target)') + expect(r).toEqual({ kind: 'string', value: 'Charlie' }) + }) + +}) + +// --------------------------------------------------------------------------- +// js/method +// --------------------------------------------------------------------------- + +describe('js/method', () => { + it('calls a no-arg method via map', () => { + const session = jsSession({ strings: ['hello', 'world'] }) + const result = session.evaluate( + '(vec (map (js/method "toUpperCase") (js/seq js/strings)))' + ) + expect(result).toEqual({ + kind: 'vector', + value: [ + { kind: 'string', value: 'HELLO' }, + { kind: 'string', value: 'WORLD' }, + ], + }) + }) + + it('supports partial args (prepended at creation)', () => { + const session = jsSession({ nums: [1.5, 2.7, 3.14] }) + const result = session.evaluate( + '(vec (map (js/method "toFixed" 1) (js/seq js/nums)))' + ) + expect(result).toEqual({ + kind: 'vector', + value: [ + { kind: 'string', value: '1.5' }, + { kind: 'string', value: '2.7' }, + { kind: 'string', value: '3.1' }, + ], + }) + }) + + it('accepts call-site args only', () => { + const session = jsSession({ arr: [1, 2, 3] }) + const result = session.evaluate('((js/method "slice") js/arr 1)') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toEqual([2, 3]) + }) + + it('combines partial and call-site args', () => { + const session = jsSession({ arr: [1, 2, 3, 4, 5] }) + // slice(1, 3) → [2, 3] + const result = session.evaluate('((js/method "slice" 1) js/arr 3)') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toEqual([2, 3]) + }) + + it('throws when property is not callable', () => { + const session = jsSession({ target:{ x: 42 } }) + expect(() => session.evaluate('((js/method "x") js/target)')).toThrow( + "property 'x' is not callable" + ) + }) +}) + +// --------------------------------------------------------------------------- +// js/merge +// --------------------------------------------------------------------------- + +describe('js/merge', () => { + it('merges two JS objects into a new object', () => { + const a = { x: 1 } + const b = { y: 2 } + const session = jsSession({ a, b }) + const result = session.evaluate('(js/merge js/a js/b)') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toEqual({ x: 1, y: 2 }) + }) + + it('merges three objects', () => { + const a = { x: 1 } + const b = { y: 2 } + const c = { z: 3 } + const session = jsSession({ a, b, c }) + const result = session.evaluate('(js/merge js/a js/b js/c)') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') + expect(result.value).toEqual({ x: 1, y: 2, z: 3 }) + }) + + it('accepts a Clojure map as source', () => { + const base = { x: 1 } + const session = jsSession({ base }) + const result = session.evaluate('(js/merge js/base {:y 2 :z 3})') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') + expect(result.value).toEqual({ x: 1, y: 2, z: 3 }) + }) + + it('does not mutate the original objects', () => { + const a = { x: 1 } + const b = { y: 2 } + const session = jsSession({ a, b }) + const result = session.evaluate('(js/merge js/a js/b)') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toEqual({ x: 1, y: 2 }) + expect(a).toEqual({ x: 1 }) + expect(b).toEqual({ y: 2 }) + }) + + it('later keys win on conflict', () => { + const a = { x: 1 } + const b = { x: 99 } + const session = jsSession({ a, b }) + const result = session.evaluate('(js/merge js/a js/b)') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') + expect((result.value as Record)['x']).toBe(99) + }) +}) + +// --------------------------------------------------------------------------- +// js/seq +// --------------------------------------------------------------------------- + +describe('js/seq', () => { + it('converts a JS array to a Clojure vector', () => { + const arr = [1, 2, 3] + const session = jsSession({ arr }) + const result = session.evaluate('(js/seq js/arr)') + expect(result).toEqual({ + kind: 'vector', + value: [ + { kind: 'number', value: 1 }, + { kind: 'number', value: 2 }, + { kind: 'number', value: 3 }, + ], + }) + }) + + it('converts elements via jsToClj (primitives become Clj types)', () => { + const arr = ['a', true, 42] + const session = jsSession({ arr }) + const result = session.evaluate('(js/seq js/arr)') + expect(result).toEqual({ + kind: 'vector', + value: [ + { kind: 'string', value: 'a' }, + { kind: 'boolean', value: true }, + { kind: 'number', value: 42 }, + ], + }) + }) + + it('boxes object elements as CljJsValue', () => { + const arr = [{ x: 1 }] + const session = jsSession({ arr }) + const result = session.evaluate('(js/seq js/arr)') + expect(result.kind).toBe('vector') + if (result.kind === 'vector') { + expect(result.value[0].kind).toBe('js-value') + } + }) + + it('throws on non-array js-value', () => { + const session = jsSession({ target:{ x: 1 } }) + expect(() => session.evaluate('(js/seq js/target)')).toThrow( + 'expected a js-value wrapping an array' + ) + }) + + it('throws on non-js-value', () => { + expect(() => freshSession().evaluate('(js/seq [1 2 3])')).toThrow( + 'expected a js-value wrapping an array' + ) + }) + + it('returns empty vector for empty array', () => { + const session = jsSession({ arr: [] }) + const result = session.evaluate('(js/seq js/arr)') + expect(result).toEqual({ kind: 'vector', value: [] }) + }) +}) + +// --------------------------------------------------------------------------- +// js/array +// --------------------------------------------------------------------------- + +describe('js/array', () => { + it('zero args produces an empty JS array', () => { + const result = freshSession().evaluate('(js/array)') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toEqual([]) + }) + + it('creates a JS array from primitives', () => { + const result = freshSession().evaluate('(js/array 1 2 3)') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toEqual([1, 2, 3]) + }) + + it('creates a JS array from strings', () => { + const result = freshSession().evaluate('(js/array "a" "b" "c")') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') + expect(result.value).toEqual(['a', 'b', 'c']) + }) + + it('round-trips with js/seq', () => { + const result = freshSession().evaluate('(js/seq (js/array 10 20 30))') + expect(result).toEqual({ + kind: 'vector', + value: [ + { kind: 'number', value: 10 }, + { kind: 'number', value: 20 }, + { kind: 'number', value: 30 }, + ], + }) + }) + + it('converts Clojure maps to JS objects', () => { + const result = freshSession().evaluate('(js/array {:x 1} {:x 2})') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') { + const arr = result.value as unknown[] + expect(arr).toEqual([{ x: 1 }, { x: 2 }]) + } + }) +}) + +// --------------------------------------------------------------------------- +// js/obj +// --------------------------------------------------------------------------- + +describe('js/obj', () => { + it('builds a JS object from string keys', () => { + const result = freshSession().evaluate('(js/obj "key" "val" "n" 42)') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') + expect(result.value).toEqual({ key: 'val', n: 42 }) + }) + + it('builds a JS object from keyword keys (strips colon)', () => { + const result = freshSession().evaluate('(js/obj :name "Alice" :age 30)') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') + expect(result.value).toEqual({ name: 'Alice', age: 30 }) + }) + + it('builds an empty object with zero args', () => { + const result = freshSession().evaluate('(js/obj)') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toEqual({}) + }) + + it('throws on odd number of args', () => { + expect(() => freshSession().evaluate('(js/obj "key")')).toThrow( + 'requires even number of arguments' + ) + }) + + it('values are converted via cljToJs (Clojure map → JS obj)', () => { + const result = freshSession().evaluate('(js/obj "nested" {:x 1})') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') + expect(result.value).toEqual({ nested: { x: 1 } }) + }) +}) + +// --------------------------------------------------------------------------- +// js/keys, js/values, js/entries — Object.keys/values/entries equivalents +// --------------------------------------------------------------------------- + +describe('js/keys', () => { + it('returns a Clojure vector of property name strings', () => { + const result = freshSession().evaluate('(js/keys (js/obj "a" 1 "b" 2 "c" 3))') + expect(result).toEqual({ + kind: 'vector', + value: [ + { kind: 'string', value: 'a' }, + { kind: 'string', value: 'b' }, + { kind: 'string', value: 'c' }, + ], + }) + }) + + it('returns empty vector for empty object', () => { + const result = freshSession().evaluate('(js/keys (js/obj))') + expect(result).toEqual({ kind: 'vector', value: [] }) + }) + + it('composes with count', () => { + const result = freshSession().evaluate('(count (js/keys (js/obj "x" 1 "y" 2)))') + expect(result).toEqual({ kind: 'number', value: 2 }) + }) + + it('enables discovery — (js/keys module) shows exported names', () => { + const session = jsSession({ target: { join: () => {}, resolve: () => {}, sep: '/' } }) + const result = session.evaluate('(set (js/keys js/target))') + expect(result.kind).toBe('set') + if (result.kind === 'set') { + const names = new Set(result.values.map((v) => (v.kind === 'string' ? v.value : ''))) + expect(names.has('join')).toBe(true) + expect(names.has('resolve')).toBe(true) + expect(names.has('sep')).toBe(true) + } + }) + + it('throws on nil', () => { + expect(() => freshSession().evaluate('(js/keys nil)')).toThrow('cannot access properties on nil') + }) +}) + +describe('js/values', () => { + it('returns a Clojure vector of values converted via jsToClj', () => { + const result = freshSession().evaluate('(js/values (js/obj "x" 1 "y" "hello" "z" true))') + expect(result.kind).toBe('vector') + if (result.kind === 'vector') { + expect(result.value).toContainEqual({ kind: 'number', value: 1 }) + expect(result.value).toContainEqual({ kind: 'string', value: 'hello' }) + expect(result.value).toContainEqual({ kind: 'boolean', value: true }) + } + }) + + it('returns empty vector for empty object', () => { + expect(freshSession().evaluate('(js/values (js/obj))')).toEqual({ + kind: 'vector', + value: [], + }) + }) + + it('boxes nested object values as CljJsValue', () => { + const result = freshSession().evaluate('(js/values (js/obj "inner" (js/obj "x" 42)))') + expect(result.kind).toBe('vector') + if (result.kind === 'vector') { + expect(result.value[0].kind).toBe('js-value') + } + }) +}) + +describe('js/entries', () => { + it('returns a vector of [key value] pairs', () => { + const result = freshSession().evaluate('(js/entries (js/obj "name" "Alice" "age" 30))') + expect(result.kind).toBe('vector') + if (result.kind === 'vector') { + expect(result.value).toHaveLength(2) + // Each entry is a vector [string, value] + const [nameEntry, ageEntry] = result.value + expect(nameEntry).toEqual({ + kind: 'vector', + value: [{ kind: 'string', value: 'name' }, { kind: 'string', value: 'Alice' }], + }) + expect(ageEntry).toEqual({ + kind: 'vector', + value: [{ kind: 'string', value: 'age' }, { kind: 'number', value: 30 }], + }) + } + }) + + it('returns empty vector for empty object', () => { + expect(freshSession().evaluate('(js/entries (js/obj))')).toEqual({ + kind: 'vector', + value: [], + }) + }) + + it('composes with destructuring — extract key and value', () => { + const result = freshSession().evaluate(` + (let [entries (js/entries (js/obj "x" 42)) + [[k v]] entries] + (str k "=" v)) + `) + expect(result).toEqual({ kind: 'string', value: 'x=42' }) + }) + + it('composes with into to rebuild as a Clojure map', () => { + const result = freshSession().evaluate( + '(into {} (map (fn [[k v]] [k v]) (js/entries (js/obj "a" 1 "b" 2))))' + ) + expect(result.kind).toBe('map') + if (result.kind === 'map') { + const entries = Object.fromEntries( + result.entries.map(([k, v]) => [ + k.kind === 'string' ? k.value : '', + v.kind === 'number' ? v.value : null, + ]) + ) + expect(entries).toEqual({ a: 1, b: 2 }) + } + }) +}) diff --git a/packages/conjure-js/src/core/__tests__/js-interop/js-interop-phase1.spec.ts b/packages/conjure-js/src/core/__tests__/js-interop/js-interop-phase1.spec.ts new file mode 100644 index 0000000..7df8ad5 --- /dev/null +++ b/packages/conjure-js/src/core/__tests__/js-interop/js-interop-phase1.spec.ts @@ -0,0 +1,88 @@ +/** + * Tests for Phase 1 of JS Interop: CljJsValue type, factory, printer, and assertions. + */ + +import { describe, it, expect } from 'vitest' +import { cljJsValue } from '../../factories' +import { printString } from '../../printer' +import { is, isJsValue } from '../../assertions' + +describe('cljJsValue factory', () => { + it('creates a js-value with the correct kind', () => { + const v = cljJsValue({ foo: 'bar' }) + expect(v.kind).toBe('js-value') + expect(v.value).toEqual({ foo: 'bar' }) + }) + + it('wraps functions', () => { + const fn = () => 42 + const v = cljJsValue(fn) + expect(v.kind).toBe('js-value') + expect(v.value).toBe(fn) + }) + + it('wraps null', () => { + const v = cljJsValue(null) + expect(v.kind).toBe('js-value') + expect(v.value).toBeNull() + }) +}) + +describe('printer — js-value', () => { + it('prints functions as #', () => { + expect(printString(cljJsValue(() => {}))).toBe('#') + expect(printString(cljJsValue(Math.abs))).toBe('#') + }) + + it('prints arrays as #', () => { + expect(printString(cljJsValue([1, 2, 3]))).toBe('#') + expect(printString(cljJsValue([]))).toBe('#') + }) + + it('prints promises as #', () => { + expect(printString(cljJsValue(Promise.resolve(42)))).toBe('#') + }) + + it('prints plain objects as #', () => { + expect(printString(cljJsValue({}))).toBe('#') + expect(printString(cljJsValue({ a: 1 }))).toBe('#') + }) + + it('prints instances with their constructor name', () => { + expect(printString(cljJsValue(new Date('2026-01-01')))).toBe('#') + expect(printString(cljJsValue(new Map()))).toBe('#') + expect(printString(cljJsValue(new Set()))).toBe('#') + }) + + it('prints null as # and undefined as #', () => { + expect(printString(cljJsValue(null))).toBe('#') + expect(printString(cljJsValue(undefined))).toBe('#') + }) +}) + +describe('is.jsValue predicate', () => { + it('returns true for CljJsValue', () => { + expect(is.jsValue(cljJsValue({}))).toBe(true) + expect(isJsValue(cljJsValue(42))).toBe(true) + }) + + it('returns false for other kinds', () => { + expect(is.jsValue({ kind: 'number', value: 42 })).toBe(false) + expect(is.jsValue({ kind: 'nil', value: null })).toBe(false) + expect(is.jsValue({ kind: 'string', value: 'hi' })).toBe(false) + }) +}) + +describe('is.callable with CljJsValue', () => { + it('is callable when the wrapped value is a function', () => { + expect(is.callable(cljJsValue(() => 42))).toBe(true) + expect(is.callable(cljJsValue(Math.max))).toBe(true) + }) + + it('is NOT callable when the wrapped value is a non-function', () => { + expect(is.callable(cljJsValue({}))).toBe(false) + expect(is.callable(cljJsValue([1, 2]))).toBe(false) + expect(is.callable(cljJsValue(null))).toBe(false) + expect(is.callable(cljJsValue(42))).toBe(false) + }) +}) diff --git a/packages/conjure-js/src/core/__tests__/js-interop/js-interop-phase2.spec.ts b/packages/conjure-js/src/core/__tests__/js-interop/js-interop-phase2.spec.ts new file mode 100644 index 0000000..df9dfc1 --- /dev/null +++ b/packages/conjure-js/src/core/__tests__/js-interop/js-interop-phase2.spec.ts @@ -0,0 +1,468 @@ +/** + * Tests for Phase 2 of JS Interop: the `.` special form and `js/new`. + */ + +import { describe, expect, it } from 'vitest' +import { + cljJsValue, + cljNil, + cljNumber, + cljString, + cljBoolean, + cljVar, +} from '../../factories' +import type { Session } from '../../session' +import { freshSession } from '../../evaluator/__tests__/evaluator-test-utils' +import { expectError } from '../../evaluator/__tests__/evaluator-test-utils' + +// --------------------------------------------------------------------------- +// Test helper — inject raw JS values into the `user` namespace so Clojure +// code can reference them by name. Avoids needing Phase 4 (hostBindings). +// --------------------------------------------------------------------------- + +function sessionWithJs(bindings: Record): Session { + const session = freshSession() + const ns = session.getNs('user')! + for (const [name, rawValue] of Object.entries(bindings)) { + ns.vars.set(name, cljVar('user', name, cljJsValue(rawValue))) + } + return session +} + +// --------------------------------------------------------------------------- +// Property access +// --------------------------------------------------------------------------- + +describe('. — property access', () => { + it('reads a string property as CljString', () => { + const session = sessionWithJs({ obj: { name: 'alice' } }) + expect(session.evaluate('(. obj name)')).toEqual(cljString('alice')) + }) + + it('reads a number property as CljNumber', () => { + const session = sessionWithJs({ obj: { count: 42 } }) + expect(session.evaluate('(. obj count)')).toEqual(cljNumber(42)) + }) + + it('reads a boolean property as CljBoolean', () => { + const session = sessionWithJs({ obj: { active: true } }) + expect(session.evaluate('(. obj active)')).toEqual(cljBoolean(true)) + }) + + it('reads an undefined property as CljJsValue wrapping undefined', () => { + const session = sessionWithJs({ obj: {} }) + const result = session.evaluate('(. obj missing)') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toBeUndefined() + }) + + it('reads a null property as CljNil', () => { + const session = sessionWithJs({ obj: { value: null } }) + expect(session.evaluate('(. obj value)')).toEqual(cljNil()) + }) + + it('undefined property is distinct from nil', () => { + const session = sessionWithJs({ obj: { explicit: null } }) + const nullResult = session.evaluate('(. obj explicit)') + const undefinedResult = session.evaluate('(. obj nonexistent)') + expect(nullResult).toEqual(cljNil()) + expect(undefinedResult.kind).toBe('js-value') + if (undefinedResult.kind === 'js-value') + expect(undefinedResult.value).toBeUndefined() + }) + + it('reads an object property as CljJsValue', () => { + const inner = { x: 1 } + const session = sessionWithJs({ obj: { inner } }) + const result = session.evaluate('(. obj inner)') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toBe(inner) + }) + + it('reads an array property as CljJsValue', () => { + const arr = [1, 2, 3] + const session = sessionWithJs({ obj: { arr } }) + const result = session.evaluate('(. obj arr)') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') { + expect(result.value).toBe(arr) + expect(Array.isArray(result.value)).toBe(true) + } + }) + + it('reads a function property as CljJsValue (bound to obj)', () => { + const fn = (x: number) => x * 2 + const session = sessionWithJs({ obj: { fn } }) + const result = session.evaluate('(. obj fn)') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') { + // Function is bound to its object to support the ((. obj method)) zero-arg pattern + expect(typeof result.value).toBe('function') + expect((result.value as (x: number) => number)(5)).toBe(10) + } + }) + + it('zero-arg: (. obj method) returns the function as CljJsValue', () => { + const session = sessionWithJs({ obj: { greet: () => 'hello' } }) + const result = session.evaluate('(. obj greet)') + // zero extra args → property access, returns the function boxed + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') { + expect(typeof result.value).toBe('function') + } + }) +}) + +// --------------------------------------------------------------------------- +// Method calls +// --------------------------------------------------------------------------- + +describe('. — method calls', () => { + it('calls a method with one arg', () => { + const session = sessionWithJs({ math: Math }) + expect(session.evaluate('(. math abs -7)')).toEqual(cljNumber(7)) + }) + + it('calls a method with multiple args', () => { + const session = sessionWithJs({ math: Math }) + expect(session.evaluate('(. math max 3 7 2)')).toEqual(cljNumber(7)) + }) + + it('preserves `this` binding', () => { + const obj = { + multiplier: 3, + multiply(x: number) { + return x * this.multiplier + }, + } + const session = sessionWithJs({ obj }) + expect(session.evaluate('(. obj multiply 5)')).toEqual(cljNumber(15)) + }) + + it('passes a string arg correctly', () => { + const session = sessionWithJs({ + obj: { greet: (name: string) => `hello ${name}` }, + }) + expect(session.evaluate('(. obj greet "world")')).toEqual( + cljString('hello world') + ) + }) + + it('passes a number arg correctly', () => { + const session = sessionWithJs({ obj: { double: (n: number) => n * 2 } }) + expect(session.evaluate('(. obj double 21)')).toEqual(cljNumber(42)) + }) + + it('passes a Clojure vector as a JS array', () => { + const session = sessionWithJs({ + obj: { sum: (arr: number[]) => arr.reduce((a, b) => a + b, 0) }, + }) + expect(session.evaluate('(. obj sum [1 2 3 4])')).toEqual(cljNumber(10)) + }) + + it('passes a Clojure list as a JS array', () => { + const session = sessionWithJs({ + obj: { sum: (arr: number[]) => arr.reduce((a, b) => a + b, 0) }, + }) + expect(session.evaluate('(. obj sum \'(1 2 3 4))')).toEqual(cljNumber(10)) + }) + + it('passes a Clojure map as a JS plain object', () => { + const session = sessionWithJs({ + obj: { getA: (o: Record) => o['a'] }, + }) + // Clojure map {:a 99} → JS object {"a": 99} (keyword colon stripped) + expect(session.evaluate('(. obj getA {:a 99})')).toEqual(cljNumber(99)) + }) + + it('passes a Clojure function as a JS callback', () => { + // Use a custom fold that only passes 2 args to the callback (avoids arity + // mismatch from Array.prototype.reduce which calls with 4 args). + const session = sessionWithJs({ + obj: { + fold: (fn: (acc: number, x: number) => number, init: number) => + [1, 2, 3].reduce((acc, x) => fn(acc, x), init), + }, + }) + const result = session.evaluate('(. obj fold #(+ %1 %2) 0)') + expect(result).toEqual(cljNumber(6)) + }) + + it('method call result object stays boxed as CljJsValue', () => { + const session = sessionWithJs({ json: JSON }) + // Use a string without embedded quotes to avoid tokenizer issues + const result = session.evaluate('(. json parse "{}")') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') { + expect(typeof result.value).toBe('object') + } + }) +}) + +// --------------------------------------------------------------------------- +// Threading with `.` +// --------------------------------------------------------------------------- + +describe('. — threading', () => { + it('chains property access and method call via ->', () => { + const obj = { nested: { value: 'hello' }, length: 5 } + const session = sessionWithJs({ obj }) + // (-> obj (. nested) (. value)) → "hello" + const result = session.evaluate('(-> obj (. nested) (. value))') + expect(result).toEqual(cljString('hello')) + }) + + it('chains two method calls via ->', () => { + const session = sessionWithJs({ math: Math }) + const result = session.evaluate(` + (->> -9 + (. math abs) + (. math sqrt)) + `) + expect(result).toEqual(cljNumber(3)) + }) +}) + +// --------------------------------------------------------------------------- +// Conversion round-trips +// --------------------------------------------------------------------------- + +describe('cljToJs / jsToClj round-trips', () => { + it('number property → CljNumber, passed back as arg → same number result', () => { + const session = sessionWithJs({ + obj: { value: -7 }, + math: Math, + }) + // Read a JS number, pass it to another JS method + const result = session.evaluate('(let [n (. obj value)] (. math abs n))') + expect(result).toEqual(cljNumber(7)) + }) + + it('string property → CljString, passed back as arg → same string result', () => { + const session = sessionWithJs({ + obj: { word: 'hello' }, + str: { upper: (s: string) => s.toUpperCase() }, + }) + const result = session.evaluate('(let [s (. obj word)] (. str upper s))') + expect(result).toEqual(cljString('HELLO')) + }) + + it('object property → CljJsValue, passed back as arg → identity preserved', () => { + const inner = { x: 99 } + const session = sessionWithJs({ + obj: { inner }, + checker: { get: (o: typeof inner) => o.x }, + }) + const result = session.evaluate( + '(let [inner (. obj inner)] (. checker get inner))' + ) + expect(result).toEqual(cljNumber(99)) + }) +}) + +// --------------------------------------------------------------------------- +// Error cases — `.` +// --------------------------------------------------------------------------- + +describe('. — error cases', () => { + it('throws on nil target', () => { + expectError('(. nil foo)', 'cannot use . on nil') + }) + + it('throws on nil js-value target (js null)', () => { + const session = sessionWithJs({ obj: null }) + expectError('(. obj foo)', 'cannot use . on null js value', session) + }) + + it('throws on undefined js-value target', () => { + const session = sessionWithJs({ obj: undefined }) + expectError('(. obj foo)', 'cannot use . on undefined js value', session) + }) + + it('throws on vector target', () => { + expectError('(. [1 2 3] length)', 'cannot use . on vector') + }) + + it('throws on map target', () => { + expectError('(. {:a 1} foo)', 'cannot use . on map') + }) + + it('throws when fewer than 2 args', () => { + const session = sessionWithJs({ obj: {} }) + expectError('(. obj)', '. requires at least 2 arguments', session) + }) + + it('throws when property name is not a symbol', () => { + const session = sessionWithJs({ obj: {} }) + expectError( + '(. obj "foo")', + '. expects a symbol for property name', + session + ) + }) + + it('throws when calling a non-callable property as method', () => { + const session = sessionWithJs({ obj: { x: 42 } }) + expectError('(. obj x 1)', "method 'x' is not callable", session) + }) +}) + +// --------------------------------------------------------------------------- +// Auto-boxing primitives in `.` +// --------------------------------------------------------------------------- + +describe('. — auto-boxing primitive targets', () => { + it('reads string length property', () => { + expect(freshSession().evaluate('(. "hello" length)')).toEqual(cljNumber(5)) + }) + + it('calls a string method with an arg', () => { + expect(freshSession().evaluate('(. "hello" indexOf "l")')).toEqual( + cljNumber(2) + ) + }) + + it('calls a number method on a CljNumber', () => { + // toFixed returns a string + expect(freshSession().evaluate('(. 3.14159 toFixed 2)')).toEqual( + cljString('3.14') + ) + }) + + it('chains string method calls', () => { + expect( + freshSession().evaluate('(-> "hello world" (. indexOf "world"))') + ).toEqual(cljNumber(6)) + }) + + it('reads a number property on a CljNumber', () => { + // e.g. numbers do not have a "foo" prop — returns undefined boxed + const result = freshSession().evaluate('(. 42 foo)') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toBeUndefined() + }) +}) + +// --------------------------------------------------------------------------- +// Conversion — keywords, lists, rich keys +// --------------------------------------------------------------------------- + +describe('cljToJs — keyword conversion', () => { + it('keyword arg strips the colon', () => { + const session = sessionWithJs({ + obj: { echo: (s: string) => s }, + }) + // :foo → "foo" + expect(session.evaluate('(. obj echo :foo)')).toEqual(cljString('foo')) + }) + + it('keyword map key strips the colon', () => { + const session = sessionWithJs({ + obj: { get: (o: Record) => o['a'] }, + }) + expect(session.evaluate('(. obj get {:a 42})')).toEqual(cljNumber(42)) + }) + + it('string map key is used as-is (no modification)', () => { + const session = sessionWithJs({ + obj: { get: (o: Record) => o['key'] }, + }) + expect(session.evaluate('(. obj get {"key" 7})')).toEqual(cljNumber(7)) + }) + + it('number map key converts to string', () => { + const session = sessionWithJs({ + obj: { get: (o: Record) => o['1'] }, + }) + expect(session.evaluate('(. obj get {1 99})')).toEqual(cljNumber(99)) + }) + + it('boolean map key converts to string', () => { + const session = sessionWithJs({ + obj: { get: (o: Record) => o['true'] }, + }) + expect(session.evaluate('(. obj get {true 55})')).toEqual(cljNumber(55)) + }) + + it('rich map key (vector) throws a clear error', () => { + const session = sessionWithJs({ obj: { noop: (_: unknown) => null } }) + expectError( + '(. obj noop {[1 2] "val"})', + 'rich keys are not allowed as JS object keys', + session + ) + }) + + it('rich map key (map) throws a clear error', () => { + const session = sessionWithJs({ obj: { noop: (_: unknown) => null } }) + expectError( + '(. obj noop {{:a 1} "val"})', + 'rich keys are not allowed as JS object keys', + session + ) + }) +}) + +describe('cljToJs — list converts to array', () => { + it('a quoted list is passed as a JS array', () => { + const s = sessionWithJs({ + obj: { sum: (arr: number[]) => arr.reduce((a, b) => a + b, 0) }, + }) + expect(s.evaluate("(. obj sum '(1 2 3 4))")).toEqual(cljNumber(10)) + }) + + it('nested list inside vector converts recursively', () => { + const s = sessionWithJs({ + obj: { first: (arr: number[][]) => arr[0][0] }, + }) + expect(s.evaluate("(. obj first ['(10 20)])")).toEqual(cljNumber(10)) + }) +}) + +// --------------------------------------------------------------------------- +// js/new +// --------------------------------------------------------------------------- + +describe('js/new', () => { + it('constructs a Map with no args', () => { + const session = sessionWithJs({ MyMap: Map }) + const result = session.evaluate('(js/new MyMap)') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') + expect(result.value instanceof Map).toBe(true) + }) + + it('constructs a Date with a string arg', () => { + const session = sessionWithJs({ MyDate: Date }) + const result = session.evaluate('(js/new MyDate "2026-06-15")') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') { + expect(result.value instanceof Date).toBe(true) + // Use UTC year to avoid timezone-boundary issues with midnight dates + expect((result.value as Date).getUTCFullYear()).toBe(2026) + } + }) + + it('constructs and then uses the result with .', () => { + const session = sessionWithJs({ MyMap: Map }) + const result = session.evaluate(` + (let [m (js/new MyMap)] + (. m set "key" 42) + (. m get "key")) + `) + expect(result).toEqual(cljNumber(42)) + }) + + it('throws on non-js-value constructor', () => { + expectError('(js/new 42)', 'js/new: expected js-value constructor') + }) + + it('throws on CljJsValue wrapping a non-function', () => { + const session = sessionWithJs({ notACtor: { x: 1 } }) + expectError( + '(js/new notACtor)', + 'js/new: expected js-value constructor', + session + ) + }) +}) diff --git a/packages/conjure-js/src/core/__tests__/js-interop/js-interop-phase4.spec.ts b/packages/conjure-js/src/core/__tests__/js-interop/js-interop-phase4.spec.ts new file mode 100644 index 0000000..3add3bb --- /dev/null +++ b/packages/conjure-js/src/core/__tests__/js-interop/js-interop-phase4.spec.ts @@ -0,0 +1,411 @@ +/** + * Tests for Phase 4 — js namespace utilities + hostBindings injection. + * Tests for Phase 5 — calling CljJsValue functions from Clojure call position. + */ + +import { describe, expect, it } from 'vitest' +import { createSession } from '../../session' +import { freshSession } from '../../evaluator/__tests__/evaluator-test-utils' +import { expectError } from '../../evaluator/__tests__/evaluator-test-utils' +import type { CljValue } from '../../types' + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function jsSession(bindings: Record) { + return createSession({ hostBindings: bindings }) +} + +function jsRaw(result: CljValue): unknown { + expect(result.kind).toBe('js-value') + return result.kind === 'js-value' ? result.value : undefined +} + +// --------------------------------------------------------------------------- +// hostBindings — resolution as js/name +// --------------------------------------------------------------------------- + +describe('hostBindings', () => { + it('injects a JS object as js/name', () => { + const obj = { x: 42 } + const session = jsSession({ myObj: obj }) + const result = session.evaluate('js/myObj') + expect(result.kind).toBe('js-value') + expect(jsRaw(result)).toBe(obj) + }) + + it('injects a JS function as js/name', () => { + const add = (a: number, b: number) => a + b + const session = jsSession({ add }) + const result = session.evaluate('js/add') + expect(result.kind).toBe('js-value') + expect(jsRaw(result)).toBe(add) + }) + + it('injects primitives using jsToClj — numbers become CljNumber, strings become CljString', () => { + const session = jsSession({ a: 1, b: 'hello' }) + const r1 = session.evaluate('js/a') + const r2 = session.evaluate('js/b') + // jsToClj converts primitives — no unnecessary boxing + expect(r1).toEqual({ kind: 'number', value: 1 }) + expect(r2).toEqual({ kind: 'string', value: 'hello' }) + }) + + it('injects null as CljNil — consistent with jsToClj boundary semantics', () => { + const session = jsSession({ nothing: null }) + const result = session.evaluate('js/nothing') + expect(result.kind).toBe('nil') + }) +}) + +// --------------------------------------------------------------------------- +// js/get — dynamic property access +// --------------------------------------------------------------------------- + +describe('js/get', () => { + it('reads a string-keyed property', () => { + const obj = { name: 'Alice' } + const session = jsSession({ target: obj }) + const result = session.evaluate('(js/get js/target "name")') + expect(result).toEqual({ kind: 'string', value: 'Alice' }) + }) + + it('reads a keyword-keyed property (strips colon)', () => { + const obj = { score: 99 } + const session = jsSession({ target: obj }) + const result = session.evaluate('(js/get js/target :score)') + expect(result).toEqual({ kind: 'number', value: 99 }) + }) + + it('returns CljJsValue(undefined) for missing property', () => { + const obj = {} + const session = jsSession({ target: obj }) + const result = session.evaluate('(js/get js/target "missing")') + // missing property → jsToClj(undefined) → CljJsValue(undefined), distinct from nil + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toBeUndefined() + }) + + it('returns CljJsValue for object properties', () => { + const inner = { deep: true } + const obj = { inner } + const session = jsSession({ target: obj }) + const result = session.evaluate('(js/get js/target "inner")') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toBe(inner) + }) + + it('accepts a string target (auto-boxed like JS would)', () => { + // (js/get "hello" "length") — string is a valid target, same as (. "hello" length) + const result = freshSession().evaluate('(js/get "hello" "length")') + expect(result).toEqual({ kind: 'number', value: 5 }) + }) + + it('accepts a number key — JS coerces obj[0] to obj["0"]', () => { + const session = jsSession({ arr: ['a', 'b', 'c'] }) + const result = session.evaluate('(js/get js/arr 1)') + expect(result).toEqual({ kind: 'string', value: 'b' }) + }) + + it('returns the default when the property is missing', () => { + const obj = {} + const session = jsSession({ target: obj }) + const result = session.evaluate('(js/get js/target "missing" nil)') + expect(result.kind).toBe('nil') + }) + + it('returns the default value when missing (non-nil default)', () => { + const obj = {} + const session = jsSession({ target: obj }) + const result = session.evaluate('(js/get js/target "missing" "fallback")') + expect(result).toEqual({ kind: 'string', value: 'fallback' }) + }) + + it('does NOT use default when the property exists', () => { + const obj = { x: 0 } + const session = jsSession({ target: obj }) + // 0 is a real value — default must not override it + const result = session.evaluate('(js/get js/target "x" 99)') + expect(result).toEqual({ kind: 'number', value: 0 }) + }) + + it('throws on nil target (cannot access properties on nil)', () => { + expectError('(js/get nil "key")', 'cannot access properties on nil') + }) +}) + +// --------------------------------------------------------------------------- +// js/set! — property mutation +// --------------------------------------------------------------------------- + +describe('js/set!', () => { + it('mutates a property and returns the value', () => { + const obj: Record = {} + const session = jsSession({ target: obj }) + const result = session.evaluate('(js/set! js/target "x" 100)') + expect(result).toEqual({ kind: 'number', value: 100 }) + expect(obj['x']).toBe(100) + }) + + it('converts keywords to strings for the key', () => { + const obj: Record = {} + const session = jsSession({ target: obj }) + session.evaluate('(js/set! js/target :active true)') + expect(obj['active']).toBe(true) + }) +}) + +// --------------------------------------------------------------------------- +// js/call — call a JS function with no this +// --------------------------------------------------------------------------- + +describe('js/call', () => { + it('calls a JS function with converted args', () => { + const double = (n: number) => n * 2 + const session = jsSession({ double }) + const result = session.evaluate('(js/call js/double 21)') + expect(result).toEqual({ kind: 'number', value: 42 }) + }) + + it('passes multiple args', () => { + const add = (a: number, b: number) => a + b + const session = jsSession({ add }) + const result = session.evaluate('(js/call js/add 10 32)') + expect(result).toEqual({ kind: 'number', value: 42 }) + }) + + it('converts Clojure map arg to JS object', () => { + const getKey = (obj: Record) => obj['k'] + const session = jsSession({ getKey }) + const result = session.evaluate('(js/call js/getKey {:k "value"})') + expect(result).toEqual({ kind: 'string', value: 'value' }) + }) + + it('throws if the js-value is not callable', () => { + const notAFn = { x: 1 } + const session = jsSession({ notAFn }) + expect(() => session.evaluate('(js/call js/notAFn)')).toThrow('js/call: expected a js-value wrapping a function') + }) +}) + +// --------------------------------------------------------------------------- +// js/typeof +// --------------------------------------------------------------------------- + +describe('js/typeof', () => { + it('returns "function" for a function', () => { + const fn = () => {} + const session = jsSession({ fn }) + const result = session.evaluate('(js/typeof js/fn)') + expect(result).toEqual({ kind: 'string', value: 'function' }) + }) + + it('returns "object" for an object', () => { + const session = jsSession({ target:{} }) + const result = session.evaluate('(js/typeof js/target)') + expect(result).toEqual({ kind: 'string', value: 'object' }) + }) + + it('returns "object" for nil (typeof null === "object")', () => { + const result = freshSession().evaluate('(js/typeof nil)') + expect(result).toEqual({ kind: 'string', value: 'object' }) + }) + + it('returns "number" for a Clojure number (transparent)', () => { + const result = freshSession().evaluate('(js/typeof 42)') + expect(result).toEqual({ kind: 'string', value: 'number' }) + }) + + it('returns "string" for a Clojure string (transparent)', () => { + const result = freshSession().evaluate('(js/typeof "hello")') + expect(result).toEqual({ kind: 'string', value: 'string' }) + }) + + it('returns "boolean" for a Clojure boolean (transparent)', () => { + const result = freshSession().evaluate('(js/typeof true)') + expect(result).toEqual({ kind: 'string', value: 'boolean' }) + }) + + it('throws on Clojure types with no JS equivalent (vector, map, keyword)', () => { + expectError('(js/typeof :foo)', 'js/typeof: cannot determine JS type') + expectError('(js/typeof [1 2 3])', 'js/typeof: cannot determine JS type') + expectError('(js/typeof {:foo "bar"})', 'js/typeof: cannot determine JS type') + expectError('(js/typeof #(println %))', 'js/typeof: cannot determine JS type') + }) +}) + +// --------------------------------------------------------------------------- +// js/instanceof? +// --------------------------------------------------------------------------- + +describe('js/instanceof?', () => { + it('returns true for matching class', () => { + const session = jsSession({ Date }) + const result = session.evaluate('(js/instanceof? (js/new js/Date "2026-01-01") js/Date)') + expect(result).toEqual({ kind: 'boolean', value: true }) + }) + + it('returns false for non-matching class', () => { + const session = jsSession({ Date, Map }) + const result = session.evaluate('(js/instanceof? (js/new js/Date) js/Map)') + expect(result).toEqual({ kind: 'boolean', value: false }) + }) +}) + +// --------------------------------------------------------------------------- +// js/array? +// --------------------------------------------------------------------------- + +describe('js/array?', () => { + it('returns true for a JS array', () => { + const session = jsSession({ arr: [1, 2, 3] }) + const result = session.evaluate('(js/array? js/arr)') + expect(result).toEqual({ kind: 'boolean', value: true }) + }) + + it('returns false for a JS object', () => { + const session = jsSession({ target:{} }) + const result = session.evaluate('(js/array? js/target)') + expect(result).toEqual({ kind: 'boolean', value: false }) + }) + + it('returns false for non-js-value', () => { + const result = freshSession().evaluate('(js/array? [1 2 3])') + expect(result).toEqual({ kind: 'boolean', value: false }) + }) +}) + +// --------------------------------------------------------------------------- +// js/null? / js/undefined? / js/some? +// --------------------------------------------------------------------------- + +describe('js/null? js/undefined? js/some?', () => { + it('js/null? returns true for nil', () => { + const result = freshSession().evaluate('(js/null? nil)') + expect(result).toEqual({ kind: 'boolean', value: true }) + }) + + it('js/null? returns false for a value', () => { + const result = freshSession().evaluate('(js/null? 42)') + expect(result).toEqual({ kind: 'boolean', value: false }) + }) + + it('js/undefined? returns true for CljJsValue(undefined)', () => { + // js property access returning undefined → CljJsValue(undefined) + const session = jsSession({ target:{} }) + const result = session.evaluate('(js/undefined? (js/get js/target "missing"))') + // missing property → jsToClj(undefined) → CljJsValue(undefined) + expect(result).toEqual({ kind: 'boolean', value: true }) + }) + + it('js/undefined? returns false for a string', () => { + const result = freshSession().evaluate('(js/undefined? "hello")') + expect(result).toEqual({ kind: 'boolean', value: false }) + }) + + it('js/some? returns false for nil', () => { + const result = freshSession().evaluate('(js/some? nil)') + expect(result).toEqual({ kind: 'boolean', value: false }) + }) + + it('js/some? returns false for undefined', () => { + const session = jsSession({ target:{} }) + const result = session.evaluate('(js/some? (js/get js/target "missing"))') + expect(result).toEqual({ kind: 'boolean', value: false }) + }) + + it('js/some? returns true for a real value', () => { + const result = freshSession().evaluate('(js/some? 42)') + expect(result).toEqual({ kind: 'boolean', value: true }) + }) + + it('js/some? returns true for a js-value wrapping an object', () => { + const session = jsSession({ target:{ x: 1 } }) + const result = session.evaluate('(js/some? js/target)') + expect(result).toEqual({ kind: 'boolean', value: true }) + }) +}) + +// --------------------------------------------------------------------------- +// Phase 5 — CljJsValue in call position +// --------------------------------------------------------------------------- + +describe('Phase 5 — calling CljJsValue functions', () => { + it('calls a JS function in call position with Clojure args', () => { + const double = (n: number) => n * 2 + const session = jsSession({ double }) + const result = session.evaluate('(js/double 21)') + expect(result).toEqual({ kind: 'number', value: 42 }) + }) + + it('returns a string result', () => { + const greet = (name: string) => `Hello, ${name}!` + const session = jsSession({ greet }) + const result = session.evaluate('(js/greet "world")') + expect(result).toEqual({ kind: 'string', value: 'Hello, world!' }) + }) + + it('returns a boolean result', () => { + const isEven = (n: number) => n % 2 === 0 + const session = jsSession({ isEven }) + const result = session.evaluate('(js/isEven 4)') + expect(result).toEqual({ kind: 'boolean', value: true }) + }) + + it('boxes object return values as CljJsValue', () => { + const makeObj = () => ({ x: 1 }) + const session = jsSession({ makeObj }) + const result = session.evaluate('(js/makeObj)') + expect(result.kind).toBe('js-value') + }) + + it('boxes array return values as CljJsValue', () => { + const makeArr = () => [1, 2, 3] + const session = jsSession({ makeArr }) + const result = session.evaluate('(js/makeArr)') + expect(result.kind).toBe('js-value') + if (result.kind === 'js-value') expect(result.value).toEqual([1, 2, 3]) + }) + + it('returns nil for null return values', () => { + const nullFn = () => null + const session = jsSession({ nullFn }) + const result = session.evaluate('(js/nullFn)') + expect(result.kind).toBe('nil') + }) + + it('converts Clojure vector arg to JS array', () => { + const sum = (arr: number[]) => arr.reduce((a, b) => a + b, 0) + const session = jsSession({ sum }) + const result = session.evaluate('(js/sum [1 2 3 4])') + expect(result).toEqual({ kind: 'number', value: 10 }) + }) + + it('works with . to call a method with args', () => { + const session = jsSession({ arr: [1, 2, 3] }) + // Method call via . preserves this — use (. obj method arg) form + const result = session.evaluate('(. js/arr indexOf 2)') + expect(result).toEqual({ kind: 'number', value: 1 }) + }) + + it('throws when a non-callable CljJsValue is used in call position', () => { + const session = jsSession({ notFn: { x: 1 } }) + // dispatch.ts catches non-callable values before applyCallable + expect(() => session.evaluate('(js/notFn)')).toThrow('is not callable') + }) + + it('passes a Clojure fn as callback to a JS higher-order function', () => { + const applyFn = (f: (n: number) => number, x: number) => f(x) + const session = jsSession({ applyFn }) + const result = session.evaluate('(js/applyFn #(* % 2) 21)') + expect(result).toEqual({ kind: 'number', value: 42 }) + }) + + it('zero-arg method call via ((. obj method)) pattern', () => { + const session = jsSession({ str: 'hello world' }) + // (. obj method) returns the method bound to obj; calling it invokes the zero-arg form + const result = session.evaluate('((. js/str toUpperCase))') + expect(result).toEqual({ kind: 'string', value: 'HELLO WORLD' }) + }) +}) diff --git a/packages/conjure-js/src/core/__tests__/js-interop/js-interop-phase6.spec.ts b/packages/conjure-js/src/core/__tests__/js-interop/js-interop-phase6.spec.ts new file mode 100644 index 0000000..12e85ed --- /dev/null +++ b/packages/conjure-js/src/core/__tests__/js-interop/js-interop-phase6.spec.ts @@ -0,0 +1,214 @@ +import { describe, expect, it } from 'vitest' +import { createSession } from '../../session' + +// --------------------------------------------------------------------------- +// Phase 6 — importModule + string :require in ns form +// --------------------------------------------------------------------------- + +// A fake module for use in tests. Real-world usage: importModule: (s) => import(s) +const fakeReact = { + createElement: (tag: string, _props: unknown, ...children: unknown[]) => + ({ tag, children }), + version: '18.0.0', + default: 'react-default-export', +} + +const fakeUtils = { + double: (n: number) => n * 2, + greet: (name: string) => `Hello, ${name}!`, +} + +function makeSession(extraBindings = {}) { + return createSession({ + importModule: (specifier) => { + if (specifier === 'react') return fakeReact + if (specifier === './utils.js') return fakeUtils + if (specifier === 'async-mod') return Promise.resolve({ value: 42 }) + throw new Error(`Unknown module: ${specifier}`) + }, + hostBindings: extraBindings, + }) +} + +describe('Phase 6 — importModule + string :require', () => { + describe('basic module loading', () => { + it('binds the module to the alias as a CljJsValue', async () => { + const session = makeSession() + const result = await session.evaluateAsync(` + (ns my-app.core + (:require ["react" :as React])) + React + `) + expect(result.kind).toBe('js-value') + expect((result as any).value).toBe(fakeReact) + }) + + it('loads multiple string requires in one ns form', async () => { + const session = makeSession() + const [reactResult, utilsResult] = await Promise.all([ + session.evaluateAsync(` + (ns multi-test + (:require ["react" :as React] + ["./utils.js" :as utils])) + React + `), + session.evaluateAsync(` + (ns multi-test2 + (:require ["react" :as React] + ["./utils.js" :as utils])) + utils + `), + ]) + expect((reactResult as any).value).toBe(fakeReact) + expect((utilsResult as any).value).toBe(fakeUtils) + }) + + it('resolves async importModule (Promise-returning)', async () => { + const session = makeSession() + const result = await session.evaluateAsync(` + (ns async-test + (:require ["async-mod" :as asyncMod])) + asyncMod + `) + expect(result.kind).toBe('js-value') + expect((result as any).value).toEqual({ value: 42 }) + }) + }) + + describe('using the loaded module', () => { + it('can call a function from the module via .', async () => { + const session = makeSession() + const result = await session.evaluateAsync(` + (ns usage-test + (:require ["react" :as React])) + (. React createElement "div" nil "hello") + `) + expect(result.kind).toBe('js-value') + expect((result as any).value).toEqual({ tag: 'div', children: ['hello'] }) + }) + + it('can access a primitive property via .', async () => { + const session = makeSession() + const result = await session.evaluateAsync(` + (ns usage-test2 + (:require ["react" :as React])) + (. React version) + `) + expect(result.kind).toBe('string') + expect((result as any).value).toBe('18.0.0') + }) + + it('can access a property via js/get', async () => { + const session = makeSession() + const result = await session.evaluateAsync(` + (ns usage-test3 + (:require ["./utils.js" :as utils])) + (js/get utils "double") + `) + expect(result.kind).toBe('js-value') + expect(typeof (result as any).value).toBe('function') + }) + + it('can call a loaded module function that was stored in a var', async () => { + const session = makeSession() + const result = await session.evaluateAsync(` + (ns call-test + (:require ["./utils.js" :as utils])) + (let [double-fn (. utils double)] + (double-fn 21)) + `) + expect(result.kind).toBe('number') + expect((result as any).value).toBe(42) + }) + }) + + describe('mixed string + symbol requires', () => { + it('handles string and symbol requires in the same ns form', async () => { + const session = makeSession() + // First create a clojure namespace to require + session.evaluate('(ns my-utils) (defn add [a b] (+ a b))') + const result = await session.evaluateAsync(` + (ns mixed-test + (:require [my-utils :as mu] + ["react" :as React])) + [(mu/add 1 2) (. React version)] + `) + expect(result.kind).toBe('vector') + const vec = (result as any).value + expect(vec[0]).toEqual({ kind: 'number', value: 3 }) + expect(vec[1]).toEqual({ kind: 'string', value: '18.0.0' }) + }) + }) + + describe('error cases', () => { + it('throws when importModule is not configured and a string require is used', async () => { + const session = createSession() // no importModule + await expect( + session.evaluateAsync(` + (ns no-importer-test + (:require ["react" :as React])) + React + `) + ).rejects.toThrow(/importModule is not configured/) + }) + + it('throws when string require is missing :as alias', async () => { + const session = makeSession() + await expect( + session.evaluateAsync(` + (ns no-alias-test + (:require ["react"])) + nil + `) + ).rejects.toThrow(/must have an :as alias/) + }) + + it('throws a helpful error when string require used with sync evaluate()', () => { + const session = makeSession() + expect(() => + session.evaluate(` + (ns sync-test + (:require ["react" :as React])) + React + `) + ).toThrow(/use evaluateAsync/) + }) + + it('throws when importModule throws for an unknown specifier', async () => { + const session = makeSession() + await expect( + session.evaluateAsync(` + (ns unknown-mod-test + (:require ["unknown-package" :as Unknown])) + Unknown + `) + ).rejects.toThrow(/Unknown module: unknown-package/) + }) + }) + + describe('evaluateAsync regression — non-string requires still work', () => { + it('loads a clojure namespace via evaluateAsync', async () => { + const session = makeSession() + session.evaluate('(ns my-lib) (defn greet [name] (str "Hi, " name "!"))') + const result = await session.evaluateAsync(` + (ns regression-test + (:require [my-lib :as lib])) + (lib/greet "world") + `) + expect(result.kind).toBe('string') + expect((result as any).value).toBe('Hi, world!') + }) + + it('evaluateAsync with no ns form still returns the result', async () => { + const session = makeSession() + const result = await session.evaluateAsync('(+ 1 2)') + expect(result).toEqual({ kind: 'number', value: 3 }) + }) + + it('evaluateAsync still awaits CljPending results', async () => { + const session = makeSession() + const result = await session.evaluateAsync('(async (+ 1 2))') + expect(result).toEqual({ kind: 'number', value: 3 }) + }) + }) +}) diff --git a/packages/conjure-js/src/core/__tests__/js-interop/js-interop-session-options.spec.ts b/packages/conjure-js/src/core/__tests__/js-interop/js-interop-session-options.spec.ts new file mode 100644 index 0000000..ec0d283 --- /dev/null +++ b/packages/conjure-js/src/core/__tests__/js-interop/js-interop-session-options.spec.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest' +import { createSession } from '../../session' + +// --------------------------------------------------------------------------- +// createSession options — clobber guard +// --------------------------------------------------------------------------- + +describe('createSession — hostBindings clobber guard', () => { + it('throws when hostBindings key clashes with js/get', () => { + expect(() => + createSession({ hostBindings: { get: (x: unknown) => x } }) + ).toThrow("hostBindings key 'get' conflicts with built-in js/get") + }) + + it('throws when hostBindings key clashes with js/set!', () => { + expect(() => + createSession({ hostBindings: { 'set!': () => {} } }) + ).toThrow("hostBindings key 'set!' conflicts with built-in js/set!") + }) + + it('throws when hostBindings key clashes with js/call', () => { + expect(() => + createSession({ hostBindings: { call: () => {} } }) + ).toThrow("hostBindings key 'call' conflicts with built-in js/call") + }) + + it('throws when hostBindings key clashes with js/merge', () => { + expect(() => + createSession({ hostBindings: { merge: () => {} } }) + ).toThrow("hostBindings key 'merge' conflicts with built-in js/merge") + }) + + it('does not throw for non-conflicting keys', () => { + expect(() => + createSession({ + hostBindings: { Math, console, window: globalThis }, + }) + ).not.toThrow() + }) + + it('interns non-conflicting hostBindings into the js namespace', () => { + const session = createSession({ + hostBindings: { myLib: { version: '1.0' } }, + }) + const result = session.evaluate('js/myLib') + expect(result.kind).toBe('js-value') + expect((result as any).value).toEqual({ version: '1.0' }) + }) +}) diff --git a/packages/conjure-js/src/core/__tests__/reader.spec.ts b/packages/conjure-js/src/core/__tests__/reader.spec.ts index 9e8885a..d4d9545 100644 --- a/packages/conjure-js/src/core/__tests__/reader.spec.ts +++ b/packages/conjure-js/src/core/__tests__/reader.spec.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest' import { readForms } from '../reader' -import { readString } from '../index' +import { printString, readString } from '../index' import { cljBoolean, cljKeyword, @@ -469,7 +469,9 @@ describe('reader', () => { it('expands ::foo inside a map', () => { const result = readForms(tokenize('{::foo 1}'), 'user') - expect(result).toEqual([cljMap([[cljKeyword(':user/foo'), cljNumber(1)]])]) + expect(result).toEqual([ + cljMap([[cljKeyword(':user/foo'), cljNumber(1)]]), + ]) }) it('throws ReaderError for ::alias/foo when no alias map is provided', () => { @@ -478,9 +480,9 @@ describe('reader', () => { it('throws ReaderError for ::alias/foo when alias is not in the map', () => { const aliases = new Map([['other', 'some.ns']]) - expect(() => readForms(tokenize('::unknown/foo'), 'user', aliases)).toThrow( - ReaderError - ) + expect(() => + readForms(tokenize('::unknown/foo'), 'user', aliases) + ).toThrow(ReaderError) }) it('expands ::alias/foo to :full.ns/foo when alias map is provided', () => { @@ -518,9 +520,7 @@ describe('reader', () => { describe('deref reader macro (@)', () => { it('@a expands to (deref a)', () => { const result = readForms(tokenize('@a')) - expect(result).toEqual([ - cljList([cljSymbol('deref'), cljSymbol('a')]), - ]) + expect(result).toEqual([cljList([cljSymbol('deref'), cljSymbol('a')])]) }) it('@@a expands to (deref (deref a)) — chained deref', () => { @@ -586,7 +586,9 @@ describe('readString', () => { }) it('reads a vector', () => { - expect(readString('[1 2 3]')).toEqual(cljVector([cljNumber(1), cljNumber(2), cljNumber(3)])) + expect(readString('[1 2 3]')).toEqual( + cljVector([cljNumber(1), cljNumber(2), cljNumber(3)]) + ) }) it('reads a map', () => { @@ -610,7 +612,6 @@ describe('readString', () => { }) it('round-trips with printString for non-string values', () => { - const { printString } = require('../printer') const cases = ['42', ':keyword', 'nil', 'true', '[1 2 3]', '{:a 1}'] for (const src of cases) { expect(printString(readString(src))).toBe(src) diff --git a/packages/conjure-js/src/core/__tests__/stdlib.spec.ts b/packages/conjure-js/src/core/__tests__/stdlib.spec.ts index 0609d2d..138b4b8 100644 --- a/packages/conjure-js/src/core/__tests__/stdlib.spec.ts +++ b/packages/conjure-js/src/core/__tests__/stdlib.spec.ts @@ -11,7 +11,7 @@ import { } from '../factories' import { printString } from '../printer' import { createSession, createSessionFromSnapshot, snapshotSession } from '../session' -import { materialize } from './evaluator-test-utils' +import { materialize } from '../evaluator/__tests__/evaluator-test-utils' const _snapshot = snapshotSession(createSession()) diff --git a/packages/conjure-js/src/core/assertions.ts b/packages/conjure-js/src/core/assertions.ts index 6488d58..8857ba3 100644 --- a/packages/conjure-js/src/core/assertions.ts +++ b/packages/conjure-js/src/core/assertions.ts @@ -5,6 +5,7 @@ import { type CljCons, type CljDelay, type CljFunction, + type CljJsValue, type CljKeyword, type CljLazySeq, type CljList, @@ -61,9 +62,13 @@ export const isAFunction = ( ): value is CljFunction | CljNativeFunction => isFunction(value) || isNativeFunction(value) +export const isJsValue = (value: CljValue): value is CljJsValue => + value.kind === 'js-value' + /** True for any value that can be invoked like a function (IFn). */ export const isCallable = (value: CljValue): boolean => - isAFunction(value) || isKeyword(value) || isMap(value) + isAFunction(value) || isKeyword(value) || isMap(value) || + (isJsValue(value) && typeof value.value === 'function') export const isMultiMethod = (value: CljValue): value is CljMultiMethod => value.kind === 'multi-method' export const isAtom = (value: CljValue): value is CljAtom => @@ -236,4 +241,5 @@ export const is = { seqable: isSeqable, cljValue: isCljValue, equal: isEqual, + jsValue: isJsValue, } diff --git a/packages/conjure-js/src/core/bootstrap.ts b/packages/conjure-js/src/core/bootstrap.ts new file mode 100644 index 0000000..40363f1 --- /dev/null +++ b/packages/conjure-js/src/core/bootstrap.ts @@ -0,0 +1,337 @@ +import { isNamespace, isSymbol } from './assertions' +import { internVar, makeNamespace, tryLookup } from './env' +import { EvaluationError } from './errors' +import { v } from './factories' +import type { CljNamespace, CljValue, Env, EvaluationContext } from './types' +import { ensureNamespaceInRegistry, processRequireSpec } from './registry' +import type { NamespaceRegistry } from './registry' + +// --------------------------------------------------------------------------- +// wireNsCore — wires *ns*, namespace introspection fns, require, and resolve +// into coreEnv. Called from buildRuntime with explicit parameters instead of +// relying on shared closure state. +// --------------------------------------------------------------------------- + +export function wireNsCore( + registry: NamespaceRegistry, + coreEnv: Env, + getCurrentNs: () => string, + resolveNamespace: (nsName: string, ctx: EvaluationContext) => boolean +): void { + // *ns* var — holds the current namespace as a CljNamespace value + const initialNsObj = registry.get('user')?.ns ?? makeNamespace('user') + internVar('*ns*', initialNsObj, coreEnv) + const nsVar = coreEnv.ns?.vars.get('*ns*') + if (nsVar) nsVar.dynamic = true + + // Helper: resolve a namespace symbol (or namespace object) to its CljNamespace + function resolveNsSym(sym: CljValue): CljNamespace | null { + if (sym === undefined) return null + if (isNamespace(sym)) return sym + if (!isSymbol(sym)) return null + return registry.get(sym.name)?.ns ?? null + } + + // Namespace introspection + internVar( + 'ns-name', + v.nativeFn('ns-name', (x: CljValue) => { + if (x === undefined) return v.nil() + if (x.kind === 'namespace') return v.symbol(x.name) + if (x.kind === 'symbol') return x + if (x.kind === 'string') return v.symbol(x.value) + return v.nil() + }), + coreEnv + ) + + internVar( + 'all-ns', + v.nativeFn('all-ns', () => + v.list([...registry.values()].map((env) => env.ns!).filter(Boolean)) + ), + coreEnv + ) + + internVar( + 'find-ns', + v.nativeFn('find-ns', (sym: CljValue) => { + if (sym === undefined || !isSymbol(sym)) return v.nil() + return registry.get(sym.name)?.ns ?? v.nil() + }), + coreEnv + ) + + internVar( + 'in-ns', + v.nativeFnCtx('in-ns', (ctx, _callEnv, sym: CljValue) => { + if (!sym || !isSymbol(sym)) { + throw new EvaluationError('in-ns expects a symbol', { sym }) + } + if (ctx.setCurrentNs) ctx.setCurrentNs(sym.name) + return registry.get(sym.name)?.ns ?? v.nil() + }), + coreEnv + ) + + internVar( + 'ns-aliases', + v.nativeFn('ns-aliases', (sym: CljValue) => { + const ns = resolveNsSym(sym) + if (!ns) return v.map([]) + const entries: [CljValue, CljValue][] = [] + ns.aliases.forEach((targetNs, alias) => { + entries.push([v.symbol(alias), targetNs]) + }) + return v.map(entries) + }), + coreEnv + ) + + internVar( + 'ns-interns', + v.nativeFn('ns-interns', (sym: CljValue) => { + const ns = resolveNsSym(sym) + if (!ns) return v.map([]) + const entries: [CljValue, CljValue][] = [] + ns.vars.forEach((theVar, name) => { + if (theVar.ns === ns.name) entries.push([v.symbol(name), theVar]) + }) + return v.map(entries) + }), + coreEnv + ) + + internVar( + 'ns-publics', + v.nativeFn('ns-publics', (sym: CljValue) => { + const ns = resolveNsSym(sym) + if (!ns) return v.map([]) + const entries: [CljValue, CljValue][] = [] + ns.vars.forEach((theVar, name) => { + if (theVar.ns !== ns.name) return + const isPrivate = (theVar.meta?.entries ?? []).some( + ([k, val]) => + k.kind === 'keyword' && k.name === ':private' && + val.kind === 'boolean' && val.value === true + ) + if (!isPrivate) entries.push([v.symbol(name), theVar]) + }) + return v.map(entries) + }), + coreEnv + ) + + internVar( + 'ns-refers', + v.nativeFn('ns-refers', (sym: CljValue) => { + const ns = resolveNsSym(sym) + if (!ns) return v.map([]) + const entries: [CljValue, CljValue][] = [] + ns.vars.forEach((theVar, name) => { + if (theVar.ns !== ns.name) entries.push([v.symbol(name), theVar]) + }) + return v.map(entries) + }), + coreEnv + ) + + internVar( + 'ns-map', + v.nativeFn('ns-map', (sym: CljValue) => { + const ns = resolveNsSym(sym) + if (!ns) return v.map([]) + const entries: [CljValue, CljValue][] = [] + ns.vars.forEach((theVar, name) => { + entries.push([v.symbol(name), theVar]) + }) + return v.map(entries) + }), + coreEnv + ) + + internVar( + 'ns-imports', + v.nativeFn('ns-imports', (_sym: CljValue) => v.map([])), + coreEnv + ) + + internVar( + 'the-ns', + v.nativeFn('the-ns', (sym: CljValue) => { + if (sym === undefined) return v.nil() + if (isNamespace(sym)) return sym + if (!isSymbol(sym)) return v.nil() + return registry.get(sym.name)?.ns ?? v.nil() + }), + coreEnv + ) + + internVar( + 'instance?', + v.nativeFn('instance?', (_cls: CljValue, _obj: CljValue) => + v.boolean(false) + ), + coreEnv + ) + + internVar( + 'class', + v.nativeFn('class', (x: CljValue) => { + if (x === undefined) return v.nil() + return v.string(`conjure.${x.kind}`) + }), + coreEnv + ) + + internVar( + 'class?', + v.nativeFn('class?', (_x: CljValue) => v.boolean(false)), + coreEnv + ) + + internVar( + 'special-symbol?', + v.nativeFn('special-symbol?', (sym: CljValue) => { + if (sym === undefined || !isSymbol(sym)) return v.boolean(false) + const specials = new Set([ + 'def', + 'if', + 'do', + 'let', + 'quote', + 'var', + 'fn', + 'loop', + 'recur', + 'throw', + 'try', + 'catch', + 'finally', + 'ns', + 'defmacro', + 'binding', + 'monitor-enter', + 'monitor-exit', + 'new', + 'set!', + '.', + 'import', + ]) + return v.boolean(specials.has(sym.name)) + }), + coreEnv + ) + + internVar( + 'loaded-libs', + v.nativeFn('loaded-libs', () => v.set([...registry.keys()].map(v.symbol))), + coreEnv + ) + + // require — context-aware so it can thread ctx to resolveNamespace + internVar( + 'require', + v.nativeFnCtx('require', (ctx, _callEnv, ...args: CljValue[]) => { + const currentEnv = registry.get(getCurrentNs())! + for (const arg of args) { + processRequireSpec(arg, currentEnv, registry, (nsName) => + resolveNamespace(nsName, ctx) + ) + } + return v.nil() + }), + coreEnv + ) + + internVar( + 'resolve', + v.nativeFn('resolve', (sym: CljValue) => { + if (!isSymbol(sym)) return v.nil() + const slashIdx = sym.name.indexOf('/') + if (slashIdx > 0) { + const nsName = sym.name.slice(0, slashIdx) + const symName = sym.name.slice(slashIdx + 1) + const nsEnv = registry.get(nsName) ?? null + if (!nsEnv) return v.nil() + return tryLookup(symName, nsEnv) ?? v.nil() + } + const currentEnv = registry.get(getCurrentNs())! + return tryLookup(sym.name, currentEnv) ?? v.nil() + }), + coreEnv + ) +} + +// --------------------------------------------------------------------------- +// wireIdeStubs — wires clojure.reflect, cursive.repl.runtime, and Java class +// stubs into the registry. These are no-op shims for IDE compatibility. +// --------------------------------------------------------------------------- + +export function wireIdeStubs(registry: NamespaceRegistry, coreEnv: Env): void { + // IDE stubs: clojure.reflect + const reflectEnv = ensureNamespaceInRegistry( + registry, + coreEnv, + 'clojure.reflect' + ) + internVar( + 'parse-flags', + v.nativeFn('parse-flags', (_flags: CljValue, _kind: CljValue) => v.set([])), + reflectEnv + ) + internVar( + 'reflect', + v.nativeFn('reflect', (_obj: CljValue) => v.map([])), + reflectEnv + ) + internVar( + 'type-reflect', + v.nativeFn('type-reflect', (_typeobj: CljValue, ..._opts: CljValue[]) => + v.map([]) + ), + reflectEnv + ) + + // IDE stubs: cursive.repl.runtime + const cursiveEnv = ensureNamespaceInRegistry( + registry, + coreEnv, + 'cursive.repl.runtime' + ) + internVar( + 'completions', + v.nativeFn('completions', (..._args: CljValue[]) => v.nil()), + cursiveEnv + ) + + // Java class stubs — Cursive references these as bare symbols for type checks + for (const javaClass of [ + 'Class', + 'Object', + 'String', + 'Number', + 'Boolean', + 'Integer', + 'Long', + 'Double', + 'Float', + 'Byte', + 'Short', + 'Character', + 'Void', + 'Math', + 'System', + 'Runtime', + 'Thread', + 'Throwable', + 'Exception', + 'Error', + 'Iterable', + 'Comparable', + 'Runnable', + 'Cloneable', + ]) { + internVar(javaClass, v.keyword(`:java.lang/${javaClass}`), coreEnv) + } +} diff --git a/packages/conjure-js/src/core/conversions.ts b/packages/conjure-js/src/core/conversions.ts index 5c08576..25f3202 100644 --- a/packages/conjure-js/src/core/conversions.ts +++ b/packages/conjure-js/src/core/conversions.ts @@ -1,5 +1,4 @@ import { isCljValue } from './assertions' -import { applyFunction } from './evaluator' import { v } from './factories' import type { CljValue } from './types' @@ -12,9 +11,25 @@ export class ConversionError extends Error { } } +export type FunctionApplier = { + applyFunction: (fn: CljValue, args: CljValue[]) => CljValue +} + const richKeyKinds = new Set(['list', 'vector', 'map']) -export function cljToJs(value: CljValue): unknown { +// Used inside jsToClj's function wrapper when converting CLJ args back to JS. +// CLJ args passed to JS-wrapped functions are almost always data values, so +// no applier is needed. If a CLJ function is encountered here, we throw a clear +// error — use session.cljToJs() for function-bearing values. +const _throwingApplier: FunctionApplier = { + applyFunction: () => { + throw new ConversionError( + 'Cannot convert a CLJ function to JS in this context — use session.cljToJs() instead.' + ) + }, +} + +export function cljToJs(value: CljValue, applier: FunctionApplier): unknown { switch (value.kind) { case 'number': return value.value @@ -30,18 +45,18 @@ export function cljToJs(value: CljValue): unknown { return value.name case 'list': case 'vector': - return value.value.map(cljToJs) + return value.value.map((item) => cljToJs(item, applier)) case 'map': { const obj: Record = {} - for (const [k, v] of value.entries) { + for (const [k, val] of value.entries) { if (richKeyKinds.has(k.kind)) { throw new ConversionError( `Rich key types (${k.kind}) are not supported in JS object conversion. Restructure your map to use string, keyword, or number keys.`, - { key: k, value: v } + { key: k, value: val } ) } - const jsKey = String(cljToJs(k)) - obj[jsKey] = cljToJs(v) + const jsKey = String(cljToJs(k, applier)) + obj[jsKey] = cljToJs(val, applier) } return obj } @@ -49,9 +64,9 @@ export function cljToJs(value: CljValue): unknown { case 'native-function': { const fn = value return (...jsArgs: unknown[]) => { - const cljArgs = jsArgs.map(jsToClj) - const result = applyFunction(fn, cljArgs) - return cljToJs(result) + const cljArgs = jsArgs.map((a) => jsToClj(a)) + const result = applier.applyFunction(fn, cljArgs) + return cljToJs(result, applier) } } case 'macro': @@ -62,8 +77,16 @@ export function cljToJs(value: CljValue): unknown { } } -export function jsToClj(value: unknown): CljValue { - if (value === null || value === undefined) return v.nil() +export interface JsToCljOpts { + /** When true, plain object keys become keywords. Default: true. */ + keywordizeKeys?: boolean +} + +export function jsToClj(value: unknown, opts: JsToCljOpts = {}): CljValue { + const { keywordizeKeys = true } = opts + + if (value === null) return v.nil() + if (value === undefined) return v.jsValue(undefined) if (isCljValue(value)) return value switch (typeof value) { @@ -76,18 +99,21 @@ export function jsToClj(value: unknown): CljValue { case 'function': { const jsFn = value as (...args: unknown[]) => unknown return v.nativeFn('js-fn', (...cljArgs: CljValue[]) => { - const jsArgs = cljArgs.map(cljToJs) + const jsArgs = cljArgs.map((a) => cljToJs(a, _throwingApplier)) const result = jsFn(...jsArgs) - return jsToClj(result) + return jsToClj(result, opts) }) } case 'object': { if (Array.isArray(value)) { - return v.vector(value.map(jsToClj)) + return v.vector(value.map((item) => jsToClj(item, opts))) } const entries: [CljValue, CljValue][] = Object.entries( value as Record - ).map(([k, value]) => [v.keyword(`:${k}`), jsToClj(value)]) + ).map(([k, val]) => [ + keywordizeKeys ? v.keyword(`:${k}`) : v.string(k), + jsToClj(val, opts), + ]) return v.map(entries) } default: diff --git a/packages/conjure-js/src/core/core-module.ts b/packages/conjure-js/src/core/core-module.ts index 7e7b6cb..f37189c 100644 --- a/packages/conjure-js/src/core/core-module.ts +++ b/packages/conjure-js/src/core/core-module.ts @@ -4,13 +4,15 @@ import type { VarMap, ModuleContext, } from './module' -import { prettyPrintString, printString, withPrintContext } from './printer' -import { derefValue, tryLookup } from './env' +import { buildPrintContext, prettyPrintString, printString, withPrintContext } from './printer' +import { derefValue } from './env' import { valueToString } from './transformations' import type { CljMap, CljValue, Env, EvaluationContext } from './types' import { arithmeticFunctions } from './stdlib/arithmetic' import { atomFunctions } from './stdlib/atoms' -import { collectionFunctions } from './stdlib/collections' +import { mapsSetsFunctions } from './stdlib/maps-sets' +import { seqFunctions } from './stdlib/seq' +import { vectorFunctions } from './stdlib/vectors' import { errorFunctions } from './stdlib/errors' import { hofFunctions } from './stdlib/hof' import { metaFunctions } from './stdlib/meta' @@ -24,6 +26,9 @@ import { varFunctions } from './stdlib/vars' // --- ASYNC (experimental) --- import { asyncFunctions } from './stdlib/async-fns' import { v } from './factories' +import { is } from './assertions' +import { EvaluationError } from './errors' +import { cljToJs as cljToJsDeep, jsToClj as jsToCljDeep, type FunctionApplier } from './conversions' // --- END ASYNC --- // --------------------------------------------------------------------------- @@ -72,7 +77,9 @@ import { v } from './factories' const nativeFunctions = { ...arithmeticFunctions, ...atomFunctions, - ...collectionFunctions, + ...seqFunctions, + ...vectorFunctions, + ...mapsSetsFunctions, ...errorFunctions, ...predicateFunctions, ...hofFunctions, @@ -88,14 +95,6 @@ const nativeFunctions = { // --- END ASYNC --- } -function readPrintCtx(callEnv: Env) { - const len = tryLookup('*print-length*', callEnv) - const level = tryLookup('*print-level*', callEnv) - return { - printLength: len?.kind === 'number' ? len.value : null, - printLevel: level?.kind === 'number' ? level.value : null, - } -} /** * Emit text to the current output channel. @@ -164,7 +163,7 @@ export function makeCoreModule(): RuntimeModule { value: v.nativeFnCtx( 'println', (ctx, callEnv, ...args: CljValue[]) => { - withPrintContext(readPrintCtx(callEnv), () => { + withPrintContext(buildPrintContext(ctx), () => { emitToOut( ctx, callEnv, @@ -179,7 +178,7 @@ export function makeCoreModule(): RuntimeModule { value: v.nativeFnCtx( 'print', (ctx, callEnv, ...args: CljValue[]) => { - withPrintContext(readPrintCtx(callEnv), () => { + withPrintContext(buildPrintContext(ctx), () => { emitToOut(ctx, callEnv, args.map(valueToString).join(' ')) }) return v.nil() @@ -194,7 +193,7 @@ export function makeCoreModule(): RuntimeModule { }) map.set('pr', { value: v.nativeFnCtx('pr', (ctx, callEnv, ...args: CljValue[]) => { - withPrintContext(readPrintCtx(callEnv), () => { + withPrintContext(buildPrintContext(ctx), () => { emitToOut( ctx, callEnv, @@ -206,7 +205,7 @@ export function makeCoreModule(): RuntimeModule { }) map.set('prn', { value: v.nativeFnCtx('prn', (ctx, callEnv, ...args: CljValue[]) => { - withPrintContext(readPrintCtx(callEnv), () => { + withPrintContext(buildPrintContext(ctx), () => { emitToOut( ctx, callEnv, @@ -223,7 +222,7 @@ export function makeCoreModule(): RuntimeModule { if (form === undefined) return v.nil() const maxWidth = widthArg?.kind === 'number' ? widthArg.value : 80 - withPrintContext(readPrintCtx(callEnv), () => { + withPrintContext(buildPrintContext(ctx), () => { emitToOut( ctx, callEnv, @@ -238,7 +237,7 @@ export function makeCoreModule(): RuntimeModule { value: v.nativeFnCtx( 'warn', (ctx, callEnv, ...args: CljValue[]) => { - withPrintContext(readPrintCtx(callEnv), () => { + withPrintContext(buildPrintContext(ctx), () => { emitToErr( ctx, callEnv, @@ -263,6 +262,39 @@ export function makeCoreModule(): RuntimeModule { // Compatibility var for IDE tooling map.set('*compiler-options*', { value: v.map([]) }) + // JS interop — deep conversion functions + map.set('clj->js', { + value: v.nativeFnCtx('clj->js', (ctx: EvaluationContext, callEnv: Env, val: CljValue) => { + if (is.jsValue(val)) return val + const applier: FunctionApplier = { + applyFunction: (fn, args) => ctx.applyCallable(fn, args, callEnv), + } + return v.jsValue(cljToJsDeep(val, applier)) + }), + }) + + map.set('js->clj', { + value: v.nativeFn('js->clj', (val: CljValue, opts?: CljValue) => { + if (val.kind === 'nil') return val + if (!is.jsValue(val)) { + throw new EvaluationError( + `js->clj expects a js-value, got ${val.kind}`, + { val } + ) + } + const keywordizeKeys = (() => { + if (!opts || opts.kind !== 'map') return false + for (const [k, flag] of opts.entries) { + if (k.kind === 'keyword' && k.name === ':keywordize-keys') { + return flag.kind !== 'boolean' || flag.value !== false + } + } + return false + })() + return jsToCljDeep(val.value, { keywordizeKeys }) + }), + }) + return map }, }, diff --git a/packages/conjure-js/src/core/__tests__/evaluator-anon-fn.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-anon-fn.spec.ts similarity index 96% rename from packages/conjure-js/src/core/__tests__/evaluator-anon-fn.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-anon-fn.spec.ts index 2a2cab9..73567b5 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-anon-fn.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-anon-fn.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { cljNumber, cljString, cljVector } from '../factories' +import { cljNumber, cljString, cljVector } from '../../factories' import { freshSession } from './evaluator-test-utils' describe('anonymous function reader macro #(...)', () => { diff --git a/packages/conjure-js/src/core/__tests__/evaluator-arithmetic.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-arithmetic.spec.ts similarity index 98% rename from packages/conjure-js/src/core/__tests__/evaluator-arithmetic.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-arithmetic.spec.ts index b645180..042e809 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-arithmetic.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-arithmetic.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { cljBoolean } from '../factories' -import { EvaluationError } from '../errors' +import { cljBoolean } from '../../factories' +import { EvaluationError } from '../../errors' import { expectError, freshSession, toCljValue } from './evaluator-test-utils' describe('basic math', () => { it.each([ diff --git a/packages/conjure-js/src/core/__tests__/evaluator-async.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-async.spec.ts similarity index 99% rename from packages/conjure-js/src/core/__tests__/evaluator-async.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-async.spec.ts index 089785a..734a42c 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-async.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-async.spec.ts @@ -10,9 +10,9 @@ import { cljMap, cljNumber, cljVector, -} from '../factories' -import type { CljKeyword, CljMap, CljPending } from '../types' -import { EvaluationError } from '../errors' +} from '../../factories' +import type { CljKeyword, CljMap, CljPending } from '../../types' +import { EvaluationError } from '../../errors' import { freshSession } from './evaluator-test-utils' // Helper: evaluate and assert we got a CljPending back diff --git a/packages/conjure-js/src/core/__tests__/evaluator-atoms.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-atoms.spec.ts similarity index 98% rename from packages/conjure-js/src/core/__tests__/evaluator-atoms.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-atoms.spec.ts index 7f1142c..d132663 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-atoms.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-atoms.spec.ts @@ -5,8 +5,8 @@ import { cljMap, cljNil, cljNumber, -} from '../factories' -import type { CljAtom } from '../types' +} from '../../factories' +import type { CljAtom } from '../../types' import { expectError, freshSession } from './evaluator-test-utils' describe('atoms', () => { diff --git a/packages/conjure-js/src/core/__tests__/evaluator-calva-macros.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-calva-macros.spec.ts similarity index 98% rename from packages/conjure-js/src/core/__tests__/evaluator-calva-macros.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-calva-macros.spec.ts index dd3f657..9a26f40 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-calva-macros.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-calva-macros.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { cljNil } from '../factories' -import { isEqual } from '../assertions' +import { cljNil } from '../../factories' +import { isEqual } from '../../assertions' import { expectError, freshSession } from './evaluator-test-utils' // --------------------------------------------------------------------------- diff --git a/packages/conjure-js/src/core/__tests__/evaluator-collections.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-collections.spec.ts similarity index 99% rename from packages/conjure-js/src/core/__tests__/evaluator-collections.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-collections.spec.ts index 03d1581..94772ab 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-collections.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-collections.spec.ts @@ -7,7 +7,7 @@ import { cljNumber, cljString, cljVector, -} from '../factories' +} from '../../factories' import { expectError, freshSession, materialize, toCljValue } from './evaluator-test-utils' describe('count', () => { it.each([ diff --git a/packages/conjure-js/src/core/__tests__/evaluator-core-forms.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-core-forms.spec.ts similarity index 99% rename from packages/conjure-js/src/core/__tests__/evaluator-core-forms.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-core-forms.spec.ts index e5ea3ad..1c7e9f8 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-core-forms.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-core-forms.spec.ts @@ -8,7 +8,7 @@ import { cljNumber, cljSymbol, cljVector, -} from '../factories' +} from '../../factories' import { expectError, freshSession, toCljValue } from './evaluator-test-utils' describe('keywords', () => { diff --git a/packages/conjure-js/src/core/__tests__/evaluator-cursive-compat.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-cursive-compat.spec.ts similarity index 100% rename from packages/conjure-js/src/core/__tests__/evaluator-cursive-compat.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-cursive-compat.spec.ts diff --git a/packages/conjure-js/src/core/__tests__/evaluator-destructuring.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-destructuring.spec.ts similarity index 98% rename from packages/conjure-js/src/core/__tests__/evaluator-destructuring.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-destructuring.spec.ts index 555615b..9a296c8 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-destructuring.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-destructuring.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' -import { isEqual } from '../assertions' -import { cljNil, cljNumber, cljVector } from '../factories' -import { EvaluationError } from '../errors' +import { isEqual } from '../../assertions' +import { cljNil, cljNumber, cljVector } from '../../factories' +import { EvaluationError } from '../../errors' import { freshSession, toCljValue } from './evaluator-test-utils' describe('destructuring', () => { diff --git a/packages/conjure-js/src/core/__tests__/evaluator-errors.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-errors.spec.ts similarity index 99% rename from packages/conjure-js/src/core/__tests__/evaluator-errors.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-errors.spec.ts index 114d074..727e010 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-errors.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-errors.spec.ts @@ -6,8 +6,8 @@ import { cljNil, cljNumber, cljString, -} from '../factories' -import { EvaluationError } from '../errors' +} from '../../factories' +import { EvaluationError } from '../../errors' import { expectError, freshSession } from './evaluator-test-utils' function catchError(code: string): EvaluationError { diff --git a/packages/conjure-js/src/core/__tests__/evaluator-hof.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-hof.spec.ts similarity index 99% rename from packages/conjure-js/src/core/__tests__/evaluator-hof.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-hof.spec.ts index 1d4a366..d25dcb1 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-hof.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-hof.spec.ts @@ -8,8 +8,8 @@ import { cljNumber, cljString, cljVector, -} from '../factories' -import { createSession } from '../session' +} from '../../factories' +import { createSession } from '../../session' import { expectError, freshSession, materialize, toCljValue } from './evaluator-test-utils' describe('str', () => { it.each([ diff --git a/packages/conjure-js/src/core/__tests__/evaluator-io.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-io.spec.ts similarity index 79% rename from packages/conjure-js/src/core/__tests__/evaluator-io.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-io.spec.ts index c75afa1..12ded4a 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-io.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-io.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { cljString } from '../factories' -import { createSession, createSessionFromSnapshot, snapshotSession } from '../session' +import { cljString } from '../../factories' +import { createSession, createSessionFromSnapshot, snapshotSession } from '../../session' const _snapshot = snapshotSession(createSession()) @@ -95,3 +95,20 @@ describe('*err* / warn / with-err-str', () => { expect(out).toEqual([]) }) }) + +describe('*print-length* dynamic binding', () => { + it('binding *print-length* truncates pr output', () => { + expect(session().evaluate('(binding [*print-length* 3] (pr-str [1 2 3 4 5]))')).toEqual( + cljString('[1 2 3 ...]') + ) + }) + + it('*print-length* binding inside a function body is respected', () => { + // This test exercises the snapshot env aliasing fix: the function is defined + // after bootstrap and its body closes over the session's env. Without the fix, + // tryLookup(callEnv) would traverse the stale snapshot env and miss the binding. + const s = session() + s.evaluate('(defn bounded-print [coll] (binding [*print-length* 2] (pr-str coll)))') + expect(s.evaluate('(bounded-print [1 2 3 4])')).toEqual(cljString('[1 2 ...]')) + }) +}) diff --git a/packages/conjure-js/src/core/__tests__/evaluator-lazy.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-lazy.spec.ts similarity index 99% rename from packages/conjure-js/src/core/__tests__/evaluator-lazy.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-lazy.spec.ts index 45f1723..47428f7 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-lazy.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-lazy.spec.ts @@ -7,7 +7,7 @@ import { cljNumber, cljString, cljVector, -} from '../factories' +} from '../../factories' import { freshSession, materialize } from './evaluator-test-utils' describe('delay / force', () => { diff --git a/packages/conjure-js/src/core/__tests__/evaluator-loop-recur.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-loop-recur.spec.ts similarity index 99% rename from packages/conjure-js/src/core/__tests__/evaluator-loop-recur.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-loop-recur.spec.ts index 944331c..b0578da 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-loop-recur.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-loop-recur.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { cljNumber, cljVector } from '../factories' +import { cljNumber, cljVector } from '../../factories' import { expectError, freshSession } from './evaluator-test-utils' describe('loop/recur', () => { diff --git a/packages/conjure-js/src/core/__tests__/evaluator-maps-ifn.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-maps-ifn.spec.ts similarity index 96% rename from packages/conjure-js/src/core/__tests__/evaluator-maps-ifn.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-maps-ifn.spec.ts index dc3f882..1144b09 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-maps-ifn.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-maps-ifn.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { cljKeyword, cljList, cljNil, cljNumber } from '../factories' +import { cljKeyword, cljList, cljNil, cljNumber } from '../../factories' import { expectError, freshSession, materialize } from './evaluator-test-utils' describe('maps as IFn', () => { diff --git a/packages/conjure-js/src/core/__tests__/evaluator-metadata.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-metadata.spec.ts similarity index 99% rename from packages/conjure-js/src/core/__tests__/evaluator-metadata.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-metadata.spec.ts index 3e9b46f..c1cfbec 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-metadata.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-metadata.spec.ts @@ -8,8 +8,8 @@ import { cljString, cljSymbol, cljVector, -} from '../factories' -import { createSession } from '../session' +} from '../../factories' +import { createSession } from '../../session' import { expectError, freshSession } from './evaluator-test-utils' describe('docstrings and metadata', () => { diff --git a/packages/conjure-js/src/core/__tests__/evaluator-multi-arity.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-multi-arity.spec.ts similarity index 99% rename from packages/conjure-js/src/core/__tests__/evaluator-multi-arity.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-multi-arity.spec.ts index af21812..ff79de0 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-multi-arity.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-multi-arity.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { cljBoolean, cljKeyword, cljNumber, cljString } from '../factories' +import { cljBoolean, cljKeyword, cljNumber, cljString } from '../../factories' import { expectError, freshSession } from './evaluator-test-utils' describe('multi-arity fn', () => { diff --git a/packages/conjure-js/src/core/__tests__/evaluator-multimethods.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-multimethods.spec.ts similarity index 98% rename from packages/conjure-js/src/core/__tests__/evaluator-multimethods.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-multimethods.spec.ts index 94e31f1..74e396b 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-multimethods.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-multimethods.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { cljKeyword, cljNumber, cljString } from '../factories' +import { cljKeyword, cljNumber, cljString } from '../../factories' import { expectError, freshSession } from './evaluator-test-utils' describe('multimethods', () => { diff --git a/packages/conjure-js/src/core/__tests__/evaluator-namespace-values.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-namespace-values.spec.ts similarity index 71% rename from packages/conjure-js/src/core/__tests__/evaluator-namespace-values.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-namespace-values.spec.ts index ff3a0f3..e4c6aab 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-namespace-values.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-namespace-values.spec.ts @@ -195,3 +195,77 @@ describe(':refer of nonexistent symbol throws', () => { ).toThrow('does-not-exist') }) }) + +// --------------------------------------------------------------------------- +// Phase 4: :private metadata — defn-, ^:private, ns-publics filtering +// --------------------------------------------------------------------------- + +describe('defn- defines a callable private function', () => { + it('defn- function is callable within the same namespace', () => { + const s = freshSession() + s.evaluate('(defn- helper [x] (* x 2))') + const result = s.evaluate('(helper 5)') + expect(result).toMatchObject({ kind: 'number', value: 10 }) + }) + + it('defn- var has :private true in metadata', () => { + const s = freshSession() + s.evaluate('(defn- secret [x] x)') + const result = s.evaluate("(:private (meta (var secret)))") + expect(result).toMatchObject({ kind: 'boolean', value: true }) + }) +}) + +describe('ns-publics excludes private vars', () => { + it('defn- var is excluded from ns-publics', () => { + const s = freshSession() + s.evaluate('(defn- private-fn [x] x)') + s.evaluate('(defn public-fn [x] x)') + const inPublics = s.evaluate("(contains? (ns-publics 'user) 'private-fn)") + expect(inPublics).toMatchObject({ kind: 'boolean', value: false }) + }) + + it('defn- var IS included in ns-interns', () => { + const s = freshSession() + s.evaluate('(defn- private-fn [x] x)') + const inInterns = s.evaluate("(contains? (ns-interns 'user) 'private-fn)") + expect(inInterns).toMatchObject({ kind: 'boolean', value: true }) + }) + + it('public defn IS included in ns-publics', () => { + const s = freshSession() + s.evaluate('(defn public-fn [x] x)') + const inPublics = s.evaluate("(contains? (ns-publics 'user) 'public-fn)") + expect(inPublics).toMatchObject({ kind: 'boolean', value: true }) + }) + + it('(def ^:private ...) var is excluded from ns-publics', () => { + const s = freshSession() + s.evaluate('(def ^:private internal-const 42)') + const inPublics = s.evaluate("(contains? (ns-publics 'user) 'internal-const)") + expect(inPublics).toMatchObject({ kind: 'boolean', value: false }) + }) + + it('(def ^:private ...) var IS included in ns-interns', () => { + const s = freshSession() + s.evaluate('(def ^:private internal-const 42)') + const inInterns = s.evaluate("(contains? (ns-interns 'user) 'internal-const)") + expect(inInterns).toMatchObject({ kind: 'boolean', value: true }) + }) +}) + +describe('defn preserves reader metadata other than doc/arglists', () => { + it('(defn ^:private ...) marks the var as private', () => { + const s = freshSession() + s.evaluate('(defn ^:private priv-fn [x] x)') + const result = s.evaluate("(:private (meta (var priv-fn)))") + expect(result).toMatchObject({ kind: 'boolean', value: true }) + }) + + it('(defn ^:private ...) var is excluded from ns-publics', () => { + const s = freshSession() + s.evaluate('(defn ^:private priv-fn [x] x)') + const inPublics = s.evaluate("(contains? (ns-publics 'user) 'priv-fn)") + expect(inPublics).toMatchObject({ kind: 'boolean', value: false }) + }) +}) diff --git a/packages/conjure-js/src/core/__tests__/evaluator-namespaces.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-namespaces.spec.ts similarity index 97% rename from packages/conjure-js/src/core/__tests__/evaluator-namespaces.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-namespaces.spec.ts index ec44ab1..a9cedc8 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-namespaces.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-namespaces.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { cljKeyword, cljNumber } from '../factories' +import { cljKeyword, cljNumber } from '../../factories' import { freshSession } from './evaluator-test-utils' function sessionWithNs(nsName: string, defs: string) { diff --git a/packages/conjure-js/src/core/__tests__/evaluator-nested-maps.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-nested-maps.spec.ts similarity index 100% rename from packages/conjure-js/src/core/__tests__/evaluator-nested-maps.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-nested-maps.spec.ts diff --git a/packages/conjure-js/src/core/__tests__/evaluator-predicates.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-predicates.spec.ts similarity index 99% rename from packages/conjure-js/src/core/__tests__/evaluator-predicates.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-predicates.spec.ts index f60b7cc..ffd38f8 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-predicates.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-predicates.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { cljBoolean, cljKeyword } from '../factories' +import { cljBoolean, cljKeyword } from '../../factories' import { freshSession, toCljValue } from './evaluator-test-utils' describe('truthy?', () => { it.each([ diff --git a/packages/conjure-js/src/core/__tests__/evaluator-stdlib-expansion.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-stdlib-expansion.spec.ts similarity index 100% rename from packages/conjure-js/src/core/__tests__/evaluator-stdlib-expansion.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-stdlib-expansion.spec.ts diff --git a/packages/conjure-js/src/core/__tests__/evaluator-test-utils.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-test-utils.ts similarity index 95% rename from packages/conjure-js/src/core/__tests__/evaluator-test-utils.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-test-utils.ts index fcdf4ec..ea52e0a 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-test-utils.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-test-utils.ts @@ -1,5 +1,5 @@ import { expect } from 'vitest' -import { isCljValue, isCons, isEqual, isLazySeq, isNil } from '../assertions' +import { isCljValue, isCons, isEqual, isLazySeq, isNil } from '../../assertions' import { cljBoolean, cljList, @@ -8,11 +8,11 @@ import { cljNumber, cljString, cljVector, -} from '../factories' -import { EvaluationError } from '../errors' -import type { CljMap, CljValue } from '../types' -import { createSession, createSessionFromSnapshot, snapshotSession } from '../session' -import { toSeq } from '../transformations' +} from '../../factories' +import { EvaluationError } from '../../errors' +import type { CljMap, CljValue } from '../../types' +import { createSession, createSessionFromSnapshot, snapshotSession } from '../../session' +import { toSeq } from '../../transformations' /** Recursively convert lazy-seqs/cons to flat lists for test comparisons. */ export function materialize(value: CljValue): CljValue { diff --git a/packages/conjure-js/src/core/__tests__/evaluator-threading-macros.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-threading-macros.spec.ts similarity index 100% rename from packages/conjure-js/src/core/__tests__/evaluator-threading-macros.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-threading-macros.spec.ts diff --git a/packages/conjure-js/src/core/__tests__/evaluator-transducers.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-transducers.spec.ts similarity index 99% rename from packages/conjure-js/src/core/__tests__/evaluator-transducers.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-transducers.spec.ts index 5e6279a..ce5405a 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-transducers.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-transducers.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { cljBoolean, cljList, cljNil, cljNumber, cljVector } from '../factories' +import { cljBoolean, cljList, cljNil, cljNumber, cljVector } from '../../factories' import { expectError, freshSession, materialize } from './evaluator-test-utils' describe('reduced / unreduced / ensure-reduced', () => { diff --git a/packages/conjure-js/src/core/__tests__/evaluator-vars.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-vars.spec.ts similarity index 98% rename from packages/conjure-js/src/core/__tests__/evaluator-vars.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator-vars.spec.ts index aed040a..7bfbe31 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator-vars.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator-vars.spec.ts @@ -3,9 +3,9 @@ import { createSession, snapshotSession, createSessionFromSnapshot, -} from '../session' -import type { Session, SessionSnapshot } from '../session' -import { printString } from '../printer' +} from '../../session' +import type { Session, SessionSnapshot } from '../../session' +import { printString } from '../../printer' import { materialize } from './evaluator-test-utils' let snapshot: SessionSnapshot diff --git a/packages/conjure-js/src/core/__tests__/evaluator.spec.ts b/packages/conjure-js/src/core/evaluator/__tests__/evaluator.spec.ts similarity index 98% rename from packages/conjure-js/src/core/__tests__/evaluator.spec.ts rename to packages/conjure-js/src/core/evaluator/__tests__/evaluator.spec.ts index 56a1a81..21ae75a 100644 --- a/packages/conjure-js/src/core/__tests__/evaluator.spec.ts +++ b/packages/conjure-js/src/core/evaluator/__tests__/evaluator.spec.ts @@ -9,8 +9,8 @@ import { cljString, cljSymbol, cljVector, -} from '../factories' -import { lookup } from '../env' +} from '../../factories' +import { lookup } from '../../env' import { freshSession, toCljValue } from './evaluator-test-utils' describe('evaluator spec', () => { diff --git a/packages/conjure-js/src/core/evaluator/apply.ts b/packages/conjure-js/src/core/evaluator/apply.ts index b57b894..474f992 100644 --- a/packages/conjure-js/src/core/evaluator/apply.ts +++ b/packages/conjure-js/src/core/evaluator/apply.ts @@ -11,6 +11,7 @@ import type { EvaluationContext, } from '../types' import { bindParams, RecurSignal, resolveArity } from './arity' +import { cljToJs, jsToClj } from './js-interop' export function applyFunctionWithContext( fn: CljFunction | CljNativeFunction, @@ -58,6 +59,23 @@ export function applyFunctionWithContext( ) } +export function applyMacroWithContext( + macro: CljMacro, + rawArgs: CljValue[], + ctx: EvaluationContext +): CljValue { + const arity = resolveArity(macro.arities, rawArgs.length) + const localEnv = bindParams( + arity.params, + arity.restParam, + rawArgs, + macro.env, + ctx, + macro.env + ) + return ctx.evaluateForms(arity.body, localEnv) +} + /** * Invokes any IFn value — functions, native functions, keywords, and maps. * Used by comp, partial, and any other HOF that needs to call an arbitrary @@ -72,6 +90,17 @@ export function applyCallableWithContext( if (is.aFunction(fn)) { return applyFunctionWithContext(fn, args, ctx, callEnv) } + if (is.jsValue(fn)) { + if (typeof fn.value !== 'function') { + throw new EvaluationError( + `js-value is not callable: ${typeof fn.value}`, + { fn, args } + ) + } + const jsArgs = args.map((a) => cljToJs(a, ctx, callEnv)) + const rawResult = (fn.value as (...a: unknown[]) => unknown)(...jsArgs) + return jsToClj(rawResult) + } if (is.keyword(fn)) { const target = args[0] const defaultVal = args.length > 1 ? args[1] : cljNil() @@ -83,7 +112,10 @@ export function applyCallableWithContext( } if (is.map(fn)) { if (args.length === 0) { - throw new EvaluationError('Map used as function requires at least one argument', { fn, args }) + throw new EvaluationError( + 'Map used as function requires at least one argument', + { fn, args } + ) } const key = args[0] const defaultVal = args.length > 1 ? args[1] : cljNil() @@ -95,20 +127,3 @@ export function applyCallableWithContext( args, }) } - -export function applyMacroWithContext( - macro: CljMacro, - rawArgs: CljValue[], - ctx: EvaluationContext -): CljValue { - const arity = resolveArity(macro.arities, rawArgs.length) - const localEnv = bindParams( - arity.params, - arity.restParam, - rawArgs, - macro.env, - ctx, - macro.env - ) - return ctx.evaluateForms(arity.body, localEnv) -} diff --git a/packages/conjure-js/src/core/evaluator/async-evaluator.ts b/packages/conjure-js/src/core/evaluator/async-evaluator.ts index 9f03f4c..4c94693 100644 --- a/packages/conjure-js/src/core/evaluator/async-evaluator.ts +++ b/packages/conjure-js/src/core/evaluator/async-evaluator.ts @@ -1,21 +1,57 @@ /** * Async sub-evaluator for (async ...) blocks. * - * EXPERIMENTAL — gated behind the (async ...) special form in special-forms.ts. - * To revert: delete this file, remove the `async` case + import in special-forms.ts, - * remove CljPending from types.ts, remove cljPending from factories.ts, - * remove the pending case from printer.ts, delete async-fns.ts. + * ## Architecture invariant * - * Design: .regibyte/sessions/87-async-pending-design-and-plan.md + * The SYNC evaluator (evaluate.ts / special-forms.ts / apply.ts) is the + * canonical path. This file is a thin async wrapper — it handles only the + * forms that can contain sub-expressions that must be awaited (CljPending + * values unwrapped via @). Everything else delegates to asyncCtx.syncCtx. + * + * Design rule: never add `await` to the sync evaluator. Even trivial forms + * like (+ 1 2) must remain zero-overhead synchronous. The async path pays + * the Promise overhead only for code inside (async ...) blocks. + * + * ## What needs an async handler vs. what delegates to sync + * + * Forms with their own async handler (can contain @ sub-expressions): + * - `if`, `do`, `let/let*`, `loop`, `recur`, `try`, `set!` + * + * Forms that are safe to delegate to syncCtx.evaluate: + * - `quote`, `var`, `fn/fn*`, `ns` — no sub-expression evaluation at the + * creation site; fn bodies are evaluated async only when the fn is called. + * - `defmacro`, `defmulti`, `defmethod`, `letfn`, `delay`, `lazy-seq`, + * `quasiquote` — create thunks or install definitions; content is + * evaluated lazily or later. + * - `binding` — V1 limitation: async-computed binding values are not + * supported. Bind the var before the async block and use set! if needed. + * - `.`, `js/new` — JS interop is sync; args are NOT awaited before the + * call (V1 limitation: deref @ pending values explicitly before the form). + * - `async` — nested async blocks create a new CljPending via the sync path. + * - `def` — throws with a helpful message; define vars outside async blocks. + * + * ## Revert instructions + * + * To remove the async feature: delete this file, remove the `async` case and + * its import in special-forms.ts, remove CljPending from types.ts, remove + * cljPending from factories.ts, remove the pending case from printer.ts, + * and delete async-fns.ts from stdlib. + * + * Design session: .regibyte/sessions/87-async-pending-design-and-plan.md */ import { is } from '../assertions' import { extend } from '../env' import { CljThrownSignal, EvaluationError } from '../errors' import { cljNil } from '../factories' -import type { CljValue, Env, EvaluationContext } from '../types' +import type { CljList, CljValue, Env, EvaluationContext } from '../types' import { bindParams, RecurSignal, resolveArity } from './arity' import { destructureBindings } from './destructure' +import { + matchesDiscriminator, + parseTryStructure, + validateBindingVector, +} from './form-parsers' // ---- AsyncEvalCtx ---- // A parallel evaluation context where all dispatch methods are async. @@ -125,6 +161,10 @@ async function evaluateFormsAsync( // ---- List evaluation ---- +// Must mirror specialFormKeywords in special-forms.ts. +// If a new special form is added to the sync dispatcher and omitted here, +// (async ...) blocks will silently treat it as a function call at runtime. +// Add new forms here and delegate to syncCtx if no async-aware handling needed. const ASYNC_SPECIAL_FORMS = new Set([ 'quote', 'def', @@ -149,6 +189,10 @@ const ASYNC_SPECIAL_FORMS = new Set([ 'lazy-seq', 'ns', 'async', + // JS interop — delegate to sync; args inside (async ...) are not awaited + // before the interop call (V1 limitation: use @ explicitly before the form). + '.', + 'js/new', ]) async function evaluateListAsync( @@ -294,15 +338,7 @@ async function evaluateLetAsync( asyncCtx: AsyncEvalCtx ): Promise { const bindings = list.value[1] - if (!is.vector(bindings)) { - throw new EvaluationError('let bindings must be a vector', { list, env }) - } - if (bindings.value.length % 2 !== 0) { - throw new EvaluationError( - 'let bindings must have an even number of forms', - { list, env } - ) - } + validateBindingVector(bindings, 'let', env) let currentEnv = env const pairs = bindings.value @@ -332,15 +368,7 @@ async function evaluateLoopAsync( asyncCtx: AsyncEvalCtx ): Promise { const loopBindings = list.value[1] - if (!is.vector(loopBindings)) { - throw new EvaluationError('loop bindings must be a vector', { list, env }) - } - if (loopBindings.value.length % 2 !== 0) { - throw new EvaluationError( - 'loop bindings must have an even number of forms', - { list, env } - ) - } + validateBindingVector(loopBindings, 'loop', env) const loopBody = list.value.slice(2) @@ -422,38 +450,13 @@ async function evaluateTryAsync( env: Env, asyncCtx: AsyncEvalCtx ): Promise { - const forms = list.value.slice(1) - const bodyForms: CljValue[] = [] - const catchClauses: Array<{ - discriminator: CljValue - binding: string - body: CljValue[] - }> = [] - let finallyForms: CljValue[] | null = null - - for (let i = 0; i < forms.length; i++) { - const form = forms[i] - if ( - form.kind === 'list' && - form.value.length > 0 && - form.value[0].kind === 'symbol' - ) { - const head = form.value[0].name - if (head === 'catch') { - catchClauses.push({ - discriminator: form.value[1], - binding: (form.value[2] as { name: string }).name, - body: form.value.slice(3), - }) - continue - } - if (head === 'finally') { - finallyForms = form.value.slice(1) - continue - } - } - bodyForms.push(form) - } + // parseTryStructure validates catch/finally structure (binding symbol, ordering). + // matchesDiscriminator uses asyncCtx.syncCtx — discriminator evaluation is always + // synchronous (keyword checks, predicate calls on already-resolved values). + const { bodyForms, catchClauses, finallyForms } = parseTryStructure( + list as CljList, + env + ) let result: CljValue = cljNil() let pendingThrow: unknown = null @@ -486,11 +489,19 @@ async function evaluateTryAsync( let handled = false for (const clause of catchClauses) { - // Simple catch-all: match everything (V1 — no type discrimination) - const catchEnv = extend([clause.binding], [thrownValue], env) - result = await evaluateFormsAsync(clause.body, catchEnv, asyncCtx) - handled = true - break + if ( + matchesDiscriminator( + clause.discriminator, + thrownValue, + env, + asyncCtx.syncCtx + ) + ) { + const catchEnv = extend([clause.binding], [thrownValue], env) + result = await evaluateFormsAsync(clause.body, catchEnv, asyncCtx) + handled = true + break + } } if (!handled) { diff --git a/packages/conjure-js/src/core/evaluator/form-parsers.ts b/packages/conjure-js/src/core/evaluator/form-parsers.ts new file mode 100644 index 0000000..358709f --- /dev/null +++ b/packages/conjure-js/src/core/evaluator/form-parsers.ts @@ -0,0 +1,178 @@ +/** + * Shared structural parsers for special forms. + * + * These helpers are pure data transformations — no evaluation, no side effects. + * Both the sync evaluator (special-forms.ts) and the async evaluator + * (async-evaluator.ts) import from here so the logic lives in exactly one place. + * + * matchesDiscriminator also lives here because it is the same algorithm in both + * paths — only the EvaluationContext it receives differs (the real ctx in sync, + * asyncCtx.syncCtx in async, which is correct: discriminator matching is + * inherently synchronous). + */ + +import { is } from '../assertions' +import { EvaluationError } from '../errors' +import type { + CljFunction, + CljList, + CljNativeFunction, + CljValue, + CljVector, + Env, + EvaluationContext, +} from '../types' + +// ---- let / loop bindings ---- + +/** + * Validates that a binding vector is well-formed: must be a vector with an + * even number of forms. Used by let and loop handlers in both the sync and + * async evaluators so the error messages are consistent. + * + * @param formName Display name for the error message, e.g. 'let' or 'loop'. + */ +export function validateBindingVector( + vec: CljValue, + formName: string, + env: Env +): asserts vec is CljVector { + if (!is.vector(vec)) { + throw new EvaluationError(`${formName} bindings must be a vector`, { + bindings: vec, + env, + }) + } + if (vec.value.length % 2 !== 0) { + throw new EvaluationError( + `${formName} bindings must have an even number of forms`, + { bindings: vec, env } + ) + } +} + +// ---- try ---- + +export type CatchClause = { + discriminator: CljValue + binding: string + body: CljValue[] +} + +export type TryStructure = { + bodyForms: CljValue[] + catchClauses: CatchClause[] + finallyForms: CljValue[] | null +} + +/** + * Splits a (try ...) list into body forms, catch clauses, and an optional + * finally clause. Validates that catch has a discriminator + binding symbol and + * that finally (if present) is the last form. + */ +export function parseTryStructure(list: CljList, env: Env): TryStructure { + const forms = list.value.slice(1) + const bodyForms: CljValue[] = [] + const catchClauses: CatchClause[] = [] + let finallyForms: CljValue[] | null = null + + for (let i = 0; i < forms.length; i++) { + const form = forms[i] + if (is.list(form) && form.value.length > 0 && is.symbol(form.value[0])) { + const head = form.value[0].name + + if (head === 'catch') { + if (form.value.length < 3) { + throw new EvaluationError( + 'catch requires a discriminator and a binding symbol', + { form, env } + ) + } + const discriminator = form.value[1] + const bindingSym = form.value[2] + if (!is.symbol(bindingSym)) { + throw new EvaluationError('catch binding must be a symbol', { + form, + env, + }) + } + catchClauses.push({ + discriminator, + binding: bindingSym.name, + body: form.value.slice(3), + }) + continue + } + + if (head === 'finally') { + if (i !== forms.length - 1) { + throw new EvaluationError( + 'finally clause must be the last in try expression', + { form, env } + ) + } + finallyForms = form.value.slice(1) + continue + } + } + bodyForms.push(form) + } + + return { bodyForms, catchClauses, finallyForms } +} + +/** + * Determines whether a catch clause's discriminator matches a thrown value. + * + * Rules (same as Clojure-on-JVM, adapted for JS runtime): + * - Discriminator evaluates to a symbol → catch-all (JVM class names fall here) + * - `:default` keyword → catch-all + * - Any other keyword → matches if thrown is a map with a :type entry equal to + * the discriminator keyword + * - A callable function → call it with the thrown value; truthy = match + * - Anything else → error + * + * @param ctx Use the real EvaluationContext in sync paths; use asyncCtx.syncCtx + * in async paths. Discriminator matching is always synchronous. + */ +export function matchesDiscriminator( + discriminator: CljValue, + thrown: CljValue, + env: Env, + ctx: EvaluationContext +): boolean { + let disc: CljValue + try { + disc = ctx.evaluate(discriminator, env) + } catch { + // Discriminator failed to evaluate (e.g. unresolvable Java class name like + // java.lang.Throwable). Treat as catch-all — we're not on the JVM. + return true + } + // A symbol that evaluated to itself (shouldn't happen, but guard anyway) + if (disc.kind === 'symbol') return true + + if (is.keyword(disc)) { + if (disc.name === ':default') return true + if (!is.map(thrown)) return false + const typeEntry = thrown.entries.find( + ([k]) => is.keyword(k) && k.name === ':type' + ) + if (!typeEntry) return false + return is.equal(typeEntry[1], disc) + } + + if (is.aFunction(disc)) { + const result = ctx.applyFunction( + disc as CljFunction | CljNativeFunction, + [thrown], + env + ) + return is.truthy(result) + } + + throw new EvaluationError( + 'catch discriminator must be a keyword or a predicate function', + { discriminator: disc, env } + ) +} diff --git a/packages/conjure-js/src/core/evaluator/index.ts b/packages/conjure-js/src/core/evaluator/index.ts index 58e2be5..bca826a 100644 --- a/packages/conjure-js/src/core/evaluator/index.ts +++ b/packages/conjure-js/src/core/evaluator/index.ts @@ -51,6 +51,7 @@ export function createEvaluationContext(): EvaluationContext { /** Public API, this is the only place where we create a new evaluation context * All inner evaluations will use the same context + * @deprecated use session.applyFunction instead */ export function applyFunction( fn: CljFunction | CljNativeFunction, @@ -59,15 +60,6 @@ export function applyFunction( ): CljValue { return createEvaluationContext().applyFunction(fn, args, callEnv) } -export function applyMacro(macro: CljMacro, rawArgs: CljValue[]): CljValue { - return createEvaluationContext().applyMacro(macro, rawArgs) -} -export function evaluate(expr: CljValue, env: Env): CljValue { - return createEvaluationContext().evaluate(expr, env) -} -export function evaluateForms(forms: CljValue[], env: Env): CljValue { - return createEvaluationContext().evaluateForms(forms, env) -} export function evaluateWithMeasurements( expr: CljValue, diff --git a/packages/conjure-js/src/core/evaluator/js-interop.ts b/packages/conjure-js/src/core/evaluator/js-interop.ts new file mode 100644 index 0000000..4e0337f --- /dev/null +++ b/packages/conjure-js/src/core/evaluator/js-interop.ts @@ -0,0 +1,189 @@ +import { is } from '../assertions' +import { EvaluationError } from '../errors' +import { cljBoolean, cljJsValue, cljNil, cljNumber, cljString } from '../factories' +import type { CljList, CljValue, Env, EvaluationContext } from '../types' + +// --------------------------------------------------------------------------- +// JS ↔ Clojure conversion +// --------------------------------------------------------------------------- + +/** + * Convert a raw JS value to a CljValue. + * - null → CljNil (intentional absence) + * - undefined → CljJsValue(undefined) (property does not exist / unset — distinct from null) + * - primitives convert; everything else boxes. + */ +export function jsToClj(raw: unknown): CljValue { + if (raw === null) return cljNil() + if (raw === undefined) return cljJsValue(undefined) + if (typeof raw === 'number') return cljNumber(raw) + if (typeof raw === 'string') return cljString(raw) + if (typeof raw === 'boolean') return cljBoolean(raw) + return cljJsValue(raw) +} + +/** + * Convert a CljValue map key to a JS object key string. + * Only primitive keys are allowed. Rich keys (vectors, maps, sets, etc.) + * have no meaningful JS representation and must be reduced to a primitive first. + */ +function mapKeyToString(key: CljValue): string { + if (key.kind === 'string') return key.value + if (key.kind === 'keyword') return key.name.slice(1) // strip leading ':' + if (key.kind === 'number') return String(key.value) + if (key.kind === 'boolean') return String(key.value) + throw new EvaluationError( + `cljToJs: map key must be a string, keyword, number, or boolean — ` + + `got ${key.kind} (rich keys are not allowed as JS object keys; reduce to a primitive first)`, + { key } + ) +} + +/** + * Convert a CljValue to a raw JS value for crossing the interop boundary. + * Called on each argument passed to `.` and `js/new`. + */ +export function cljToJs(val: CljValue, ctx: EvaluationContext, callEnv: Env): unknown { + switch (val.kind) { + case 'js-value': return val.value + case 'number': return val.value + case 'string': return val.value + case 'boolean': return val.value + case 'nil': return null + case 'keyword': return val.name.slice(1) // strip leading ':' + case 'function': + case 'native-function': { + const fn = val + // Wrap so JS can call it: converts args JS→Clj on entry, result Clj→JS on exit. + return (...jsArgs: unknown[]) => { + const cljArgs = jsArgs.map(jsToClj) + const result = ctx.applyCallable(fn, cljArgs, callEnv) + return cljToJs(result, ctx, callEnv) + } + } + case 'list': + case 'vector': + return val.value.map((v) => cljToJs(v, ctx, callEnv)) + case 'map': { + const obj: Record = {} + for (const [key, value] of val.entries) { + obj[mapKeyToString(key)] = cljToJs(value, ctx, callEnv) + } + return obj + } + default: + throw new EvaluationError( + `cannot convert ${val.kind} to JS value — no coercion defined`, + { val } + ) + } +} + +// --------------------------------------------------------------------------- +// (. obj prop) / (. obj method arg1 arg2 ...) +// --------------------------------------------------------------------------- + +/** + * Extract the raw JS value from a target CljValue for use in `.`. + * Strings, numbers, and booleans are auto-boxed (JS auto-promotes them for + * property/method access). Nil and all other Clojure types are rejected. + */ +function extractRawTarget(target: CljValue): unknown { + switch (target.kind) { + case 'js-value': return target.value + case 'string': + case 'number': + case 'boolean': return target.value + default: + throw new EvaluationError( + `cannot use . on ${target.kind}`, + { target } + ) + } +} + +export function evaluateDot( + list: CljList, + env: Env, + ctx: EvaluationContext +): CljValue { + if (list.value.length < 3) { + throw new EvaluationError( + '. requires at least 2 arguments: (. obj prop)', + { list } + ) + } + + const target = ctx.evaluate(list.value[1], env) + const rawTarget = extractRawTarget(target) + + if (rawTarget === null || rawTarget === undefined) { + const label = rawTarget === null ? 'null' : 'undefined' + throw new EvaluationError( + `cannot use . on ${label} js value — check for nil/undefined before accessing properties`, + { target } + ) + } + + const propForm = list.value[2] + if (!is.symbol(propForm)) { + throw new EvaluationError( + `. expects a symbol for property name, got: ${propForm.kind}`, + { propForm } + ) + } + + const propName = propForm.name + const rawObj = rawTarget as Record + + if (list.value.length === 3) { + // Property access — zero extra args. + // Functions are bound to their object so that ((. obj method)) works correctly. + const rawProp = rawObj[propName] + if (typeof rawProp === 'function') { + return cljJsValue((rawProp as (...a: unknown[]) => unknown).bind(rawObj)) + } + return jsToClj(rawProp) + } + + // Method call — one or more extra args + const method = rawObj[propName] + if (typeof method !== 'function') { + throw new EvaluationError( + `method '${propName}' is not callable on ${String(rawObj)}`, + { propName, rawObj } + ) + } + + const cljArgs = list.value.slice(3).map((a) => ctx.evaluate(a, env)) + const jsArgs = cljArgs.map((a) => cljToJs(a, ctx, env)) + const rawResult = (method as (...args: unknown[]) => unknown).apply(rawObj, jsArgs) + return jsToClj(rawResult) +} + +// --------------------------------------------------------------------------- +// (js/new ClassName arg1 arg2 ...) +// --------------------------------------------------------------------------- + +export function evaluateNew( + list: CljList, + env: Env, + ctx: EvaluationContext +): CljValue { + if (list.value.length < 2) { + throw new EvaluationError('js/new requires a constructor argument', { list }) + } + + const cls = ctx.evaluate(list.value[1], env) + if (!is.jsValue(cls) || typeof cls.value !== 'function') { + throw new EvaluationError( + `js/new: expected js-value constructor, got ${cls.kind}`, + { cls } + ) + } + + const cljArgs = list.value.slice(2).map((a) => ctx.evaluate(a, env)) + const jsArgs = cljArgs.map((a) => cljToJs(a, ctx, env)) + const ctor = cls.value as new (...args: unknown[]) => unknown + return cljJsValue(new ctor(...jsArgs)) +} diff --git a/packages/conjure-js/src/core/evaluator/special-forms.ts b/packages/conjure-js/src/core/evaluator/special-forms.ts index 2e06663..8e60404 100644 --- a/packages/conjure-js/src/core/evaluator/special-forms.ts +++ b/packages/conjure-js/src/core/evaluator/special-forms.ts @@ -27,8 +27,14 @@ import type { } from '../types' import { parseArities, RecurSignal } from './arity' import { destructureBindings } from './destructure' +import { evaluateDot, evaluateNew } from './js-interop' import { evaluateQuasiquote } from './quasiquote' import { assertRecurInTailPosition } from './recur-check' +import { + matchesDiscriminator, + parseTryStructure, + validateBindingVector, +} from './form-parsers' function hasDynamicMeta(meta: CljMap | undefined): boolean { if (!meta) return false @@ -69,6 +75,10 @@ export const specialFormKeywords = { // --- ASYNC (experimental) --- async: 'async', // --- END ASYNC --- + // --- JS INTEROP --- + '.': '.', + 'js/new': 'js/new', + // --- END JS INTEROP --- } as const function keywordToDispatchFn(kw: CljKeyword): CljNativeFunction { @@ -85,90 +95,10 @@ function evaluateTry( env: Env, ctx: EvaluationContext ): CljValue { - const forms = list.value.slice(1) - const bodyForms: CljValue[] = [] - const catchClauses: Array<{ - discriminator: CljValue - binding: string - body: CljValue[] - }> = [] - let finallyForms: CljValue[] | null = null - - for (let i = 0; i < forms.length; i++) { - const form = forms[i] - if (is.list(form) && form.value.length > 0 && is.symbol(form.value[0])) { - const head = form.value[0].name - if (head === 'catch') { - if (form.value.length < 3) { - throw new EvaluationError( - 'catch requires a discriminator and a binding symbol', - { form, env } - ) - } - const discriminator = form.value[1] - const bindingSym = form.value[2] - if (!is.symbol(bindingSym)) { - throw new EvaluationError('catch binding must be a symbol', { - form, - env, - }) - } - catchClauses.push({ - discriminator, - binding: bindingSym.name, - body: form.value.slice(3), - }) - continue - } - if (head === 'finally') { - if (i !== forms.length - 1) { - throw new EvaluationError( - 'finally clause must be the last in try expression', - { - form, - env, - } - ) - } - finallyForms = form.value.slice(1) - continue - } - } - bodyForms.push(form) - } - - function matchesDiscriminator( - discriminator: CljValue, - thrown: CljValue - ): boolean { - let disc: CljValue - try { - disc = ctx.evaluate(discriminator, env) - } catch { - // Discriminator failed to evaluate (e.g. unresolvable Java class name like - // java.lang.Throwable). Treat as catch-all — we're not on the JVM. - return true - } - // A symbol that evaluated to itself (shouldn't happen, but guard anyway) - if (disc.kind === 'symbol') return true - if (is.keyword(disc)) { - if (disc.name === ':default') return true - if (!is.map(thrown)) return false - const typeEntry = thrown.entries.find( - ([k]) => is.keyword(k) && k.name === ':type' - ) - if (!typeEntry) return false - return is.equal(typeEntry[1], disc) - } - if (is.aFunction(disc)) { - const result = ctx.applyFunction(disc, [thrown], env) - return is.truthy(result) - } - throw new EvaluationError( - 'catch discriminator must be a keyword or a predicate function', - { discriminator: disc, env } - ) - } + const { bodyForms, catchClauses, finallyForms } = parseTryStructure( + list, + env + ) let result: CljValue = v.nil() let pendingThrow: unknown = null @@ -192,7 +122,7 @@ function evaluateTry( let handled = false for (const clause of catchClauses) { - if (matchesDiscriminator(clause.discriminator, thrownValue)) { + if (matchesDiscriminator(clause.discriminator, thrownValue, env, ctx)) { const catchEnv = extend([clause.binding], [thrownValue], env) result = ctx.evaluateForms(clause.body, catchEnv) handled = true @@ -338,18 +268,7 @@ function evaluateLet( ctx: EvaluationContext ): CljValue { const bindings = list.value[1] - if (!is.vector(bindings)) { - throw new EvaluationError('Bindings must be a vector', { - bindings, - env, - }) - } - if (bindings.value.length % 2 !== 0) { - throw new EvaluationError( - 'Bindings must be a balanced pair of keys and values', - { bindings, env } - ) - } + validateBindingVector(bindings, 'let', env) const body = list.value.slice(2) let localEnv = env for (let i = 0; i < bindings.value.length; i += 2) { @@ -485,18 +404,7 @@ function evaluateLoop( ctx: EvaluationContext ): CljValue { const loopBindings = list.value[1] - if (!is.vector(loopBindings)) { - throw new EvaluationError('loop bindings must be a vector', { - loopBindings, - env, - }) - } - if (loopBindings.value.length % 2 !== 0) { - throw new EvaluationError( - 'loop bindings must be a balanced pair of keys and values', - { loopBindings, env } - ) - } + validateBindingVector(loopBindings, 'loop', env) const loopBody = list.value.slice(2) assertRecurInTailPosition(loopBody) @@ -841,6 +749,10 @@ const specialFormEvaluatorEntries = { // --- ASYNC (experimental) --- async: evaluateAsyncBlock, // --- END ASYNC --- + // --- JS INTEROP --- + '.': evaluateDot, + 'js/new': evaluateNew, + // --- END JS INTEROP --- } as const satisfies Record< keyof typeof specialFormKeywords, SpecialFormEvaluatorFn diff --git a/packages/conjure-js/src/core/factories.ts b/packages/conjure-js/src/core/factories.ts index c66bd79..2e14ebc 100644 --- a/packages/conjure-js/src/core/factories.ts +++ b/packages/conjure-js/src/core/factories.ts @@ -6,6 +6,7 @@ import type { CljCons, CljDelay, CljFunction, + CljJsValue, CljKeyword, CljLazySeq, CljList, @@ -160,6 +161,11 @@ export const cljNamespace = (name: string): CljNamespace => ({ readerAliases: new Map(), }) +export const cljJsValue = (value: unknown): CljJsValue => ({ + kind: 'js-value', + value, +}) + // --- ASYNC (experimental) --- export const cljPending = (promise: Promise): CljPending => { const pending: CljPending = { kind: 'pending', promise } @@ -328,4 +334,5 @@ export const v = { lazySeq: cljLazySeq, namespace: cljNamespace, pending: cljPending, + jsValue: cljJsValue, } diff --git a/packages/conjure-js/src/core/index.ts b/packages/conjure-js/src/core/index.ts index bd6c7cf..a8b6afd 100644 --- a/packages/conjure-js/src/core/index.ts +++ b/packages/conjure-js/src/core/index.ts @@ -1,6 +1,10 @@ // Session API -export { createSession, snapshotSession, createSessionFromSnapshot } from './session' -export type { Session, SessionSnapshot } from './session' +export { + createSession, + snapshotSession, + createSessionFromSnapshot, +} from './session' +export type { Session, SessionSnapshot, SessionOptions } from './session' // Runtime API (advanced embedding) export { createRuntime, restoreRuntime } from './runtime' @@ -19,9 +23,13 @@ export type { // Conversions export { cljToJs, jsToClj, ConversionError } from './conversions' +export type { FunctionApplier } from './conversions' // Evaluator -export { applyFunction, applyMacro, evaluateWithMeasurements } from './evaluator' +export { + applyFunction, + evaluateWithMeasurements, +} from './evaluator' // Errors export { EvaluationError, ReaderError, TokenizerError } from './errors' @@ -97,6 +105,9 @@ export function readString(source: string): _CljValue { return forms[0] } +// JS interop — import map type for user-defined session entrypoints +export type ImportMap = Record + // Types export type { CljValue, @@ -114,7 +125,7 @@ export type { CljMacro, CljVar, CljNamespace, - CljPending, + CljPending, // experimental Env, Arity, IOContext, diff --git a/packages/conjure-js/src/core/ns-forms.ts b/packages/conjure-js/src/core/ns-forms.ts new file mode 100644 index 0000000..a5e6ef8 --- /dev/null +++ b/packages/conjure-js/src/core/ns-forms.ts @@ -0,0 +1,107 @@ +import { isKeyword, isList, isSymbol } from './assertions' +import type { CljValue, Token, TokenSymbol } from './types' + +// --------------------------------------------------------------------------- +// Token scan helpers — lightweight pre-parse scans for ns form metadata. +// These are semantic (module declaration analysis), not syntactic (parsing). +// Exported so session.evaluate and runtime.loadFile can reuse them. +// --------------------------------------------------------------------------- + +// Looks for the pattern: LParen Symbol("ns") Symbol(name) at the top of the +// token stream. Returns the namespace name or null. +export function extractNsNameFromTokens(tokens: Token[]): string | null { + const meaningful = tokens.filter((t) => t.kind !== 'Comment') + if (meaningful.length < 3) return null + if (meaningful[0].kind !== 'LParen') return null + if (meaningful[1].kind !== 'Symbol' || meaningful[1].value !== 'ns') + return null + if (meaningful[2].kind !== 'Symbol') return null + return meaningful[2].value +} + +// Returns Map { 'alias' -> 'full.ns.name' } for all [some.ns :as alias] and +// [some.ns :as-alias alias] specs found in the ns form's :require clauses. +// Runs before readForms so the reader can expand ::alias/foo at read time. +export function extractAliasMapFromTokens( + tokens: Token[] +): Map { + const aliases = new Map() + const meaningful = tokens.filter( + (t) => t.kind !== 'Comment' && t.kind !== 'Whitespace' + ) + if (meaningful.length < 3) return aliases + if (meaningful[0].kind !== 'LParen') return aliases + if (meaningful[1].kind !== 'Symbol' || meaningful[1].value !== 'ns') + return aliases + + let i = 3 // skip ( ns + let depth = 1 + while (i < meaningful.length && depth > 0) { + const tok = meaningful[i] + if (tok.kind === 'LParen') { + depth++ + i++ + continue + } + if (tok.kind === 'RParen') { + depth-- + i++ + continue + } + if (tok.kind === 'LBracket') { + let j = i + 1 + let nsSym: string | null = null + while (j < meaningful.length && meaningful[j].kind !== 'RBracket') { + const t = meaningful[j] + if (t.kind === 'Symbol' && nsSym === null) { + nsSym = t.value + } + if ( + t.kind === 'Keyword' && + (t.value === ':as' || t.value === ':as-alias') + ) { + j++ + if ( + j < meaningful.length && + meaningful[j].kind === 'Symbol' && + nsSym + ) { + aliases.set((meaningful[j] as TokenSymbol).value, nsSym) + } + } + j++ + } + } + i++ + } + return aliases +} + +function findNsForm(forms: CljValue[]) { + const nsForm = forms.find( + (f) => + isList(f) && + f.value.length > 0 && + isSymbol(f.value[0]) && + f.value[0].name === 'ns' + ) + if (!nsForm || !isList(nsForm)) return null + return nsForm +} + +export function extractRequireClauses(forms: CljValue[]): CljValue[][] { + const nsForm = findNsForm(forms) + if (!nsForm) return [] + const clauses: CljValue[][] = [] + for (let i = 2; i < nsForm.value.length; i++) { + const clause = nsForm.value[i] + if ( + isList(clause) && + isKeyword(clause.value[0]) && + clause.value[0].name === ':require' + ) { + clauses.push(clause.value.slice(1)) + } + } + return clauses +} diff --git a/packages/conjure-js/src/core/printer.ts b/packages/conjure-js/src/core/printer.ts index 9a6d8e6..929e604 100644 --- a/packages/conjure-js/src/core/printer.ts +++ b/packages/conjure-js/src/core/printer.ts @@ -1,6 +1,7 @@ import { EvaluationError } from './errors' import type { CljCons, CljLazySeq } from './types' -import { valueKeywords, type CljMultiMethod, type CljValue } from './types' +import { valueKeywords, type CljMultiMethod, type CljValue, type EvaluationContext } from './types' +import { derefValue } from './env' const LAZY_PRINT_CAP = 100 @@ -80,6 +81,23 @@ export function withPrintContext(ctx: PrintContext, fn: () => T): T { } } +/** + * Build a PrintContext by reading *print-length* and *print-level* from the + * runtime registry via ctx.resolveNs. Use this (instead of tryLookup) so that + * dynamic bindings from inside Clojure function bodies are visible even after + * a snapshot restore (where closure envs are stale). + */ +export function buildPrintContext(ctx: EvaluationContext): PrintContext { + const lenVar = ctx.resolveNs('clojure.core')?.vars.get('*print-length*') + const lvlVar = ctx.resolveNs('clojure.core')?.vars.get('*print-level*') + const len = lenVar ? derefValue(lenVar) : undefined + const level = lvlVar ? derefValue(lvlVar) : undefined + return { + printLength: len?.kind === 'number' ? len.value : null, + printLevel: level?.kind === 'number' ? level.value : null, + } +} + export function printString(value: CljValue, _depth = 0): string { const { printLevel } = _printCtx if (printLevel !== null && _depth >= printLevel) { @@ -204,6 +222,24 @@ function printStringImpl(value: CljValue, depth: number): string { return `#` return '#' // --- END ASYNC --- + case valueKeywords.jsValue: { + const raw = value.value + let typeName: string + if (raw === null) { + typeName = 'null' + } else if (raw === undefined) { + typeName = 'undefined' + } else if (typeof raw === 'function') { + typeName = 'Function' + } else if (Array.isArray(raw)) { + typeName = 'Array' + } else if (raw instanceof Promise) { + typeName = 'Promise' + } else { + typeName = (raw as { constructor?: { name?: string } }).constructor?.name ?? 'Object' + } + return `#` + } default: throw new EvaluationError(`unhandled value type: ${value.kind}`, { value, diff --git a/packages/conjure-js/src/core/registry.ts b/packages/conjure-js/src/core/registry.ts new file mode 100644 index 0000000..28802e4 --- /dev/null +++ b/packages/conjure-js/src/core/registry.ts @@ -0,0 +1,209 @@ +import { isKeyword, isSymbol, isVector } from './assertions' +import { makeEnv, makeNamespace } from './env' +import { EvaluationError } from './errors' +import type { CljValue, Env } from './types' + +// --------------------------------------------------------------------------- +// Core types +// --------------------------------------------------------------------------- + +export type NamespaceRegistry = Map + +// --------------------------------------------------------------------------- +// Clone helpers — used by snapshot / restoreRuntime +// --------------------------------------------------------------------------- + +function cloneBindings(bindings: Map): Map { + const out = new Map() + for (const [k, v] of bindings) { + out.set(k, v.kind === 'var' ? { ...v } : v) + } + return out +} + +function cloneEnv(env: Env, memo: Map): Env { + if (memo.has(env)) return memo.get(env)! + const cloned: Env = { + bindings: cloneBindings(env.bindings), + outer: null, + } + if (env.ns) { + cloned.ns = { + kind: 'namespace', + name: env.ns.name, + vars: new Map([...env.ns.vars].map(([k, v]) => [k, { ...v }])), + aliases: new Map(), // wired in cloneRegistry pass 2 + readerAliases: new Map(env.ns.readerAliases), + } + } + memo.set(env, cloned) + if (env.outer) cloned.outer = cloneEnv(env.outer, memo) + return cloned +} + +export function cloneRegistry(registry: NamespaceRegistry): NamespaceRegistry { + const memo = new Map() + const next = new Map() + // Pass 1: clone all envs (ns.aliases left empty) + for (const [name, env] of registry) { + next.set(name, cloneEnv(env, memo)) + } + // Pass 2: wire ns.aliases to the cloned CljNamespace objects + for (const [name, env] of registry) { + const clonedEnv = next.get(name)! + if (env.ns && clonedEnv.ns) { + for (const [alias, origNs] of env.ns.aliases) { + const targetCloned = next.get(origNs.name) + if (targetCloned?.ns) clonedEnv.ns.aliases.set(alias, targetCloned.ns) + } + } + } + return next +} + +// --------------------------------------------------------------------------- +// ensureNamespaceInRegistry — creates namespace env if it doesn't exist yet +// --------------------------------------------------------------------------- + +export function ensureNamespaceInRegistry( + registry: NamespaceRegistry, + coreEnv: Env, + name: string +): Env { + if (!registry.has(name)) { + const nsEnv = makeEnv(coreEnv) + nsEnv.ns = makeNamespace(name) + registry.set(name, nsEnv) + } + return registry.get(name)! +} + +// --------------------------------------------------------------------------- +// processRequireSpec — processes a single [ns.name :as alias :refer [...]] spec. +// resolveNs is called when the target namespace isn't yet loaded. +// --------------------------------------------------------------------------- + +export function processRequireSpec( + spec: CljValue, + currentEnv: Env, + registry: NamespaceRegistry, + resolveNs?: (nsName: string) => boolean +): void { + if (!isVector(spec)) { + throw new EvaluationError( + 'require spec must be a vector, e.g. [my.ns :as alias]', + { spec } + ) + } + + const elements = spec.value + if (elements.length === 0 || !isSymbol(elements[0])) { + throw new EvaluationError( + 'First element of require spec must be a namespace symbol', + { spec } + ) + } + + const nsName = elements[0].name + + const hasAsAlias = elements.some( + (el) => isKeyword(el) && el.name === ':as-alias' + ) + if (hasAsAlias) { + let i = 1 + while (i < elements.length) { + const kw = elements[i] + if (!isKeyword(kw)) { + throw new EvaluationError( + `Expected keyword in require spec, got ${kw.kind}`, + { spec, position: i } + ) + } + if (kw.name === ':as-alias') { + i++ + const alias = elements[i] + if (!alias || !isSymbol(alias)) { + throw new EvaluationError(':as-alias expects a symbol alias', { + spec, + position: i, + }) + } + currentEnv.ns!.readerAliases.set(alias.name, nsName) + i++ + } else { + throw new EvaluationError( + `:as-alias specs only support :as-alias, got ${kw.name}`, + { spec } + ) + } + } + return + } + + let targetEnv = registry.get(nsName) + if (!targetEnv && resolveNs) { + resolveNs(nsName) + targetEnv = registry.get(nsName) + } + if (!targetEnv) { + throw new EvaluationError( + `Namespace ${nsName} not found. Only already-loaded namespaces can be required.`, + { nsName } + ) + } + + let i = 1 + while (i < elements.length) { + const kw = elements[i] + if (!isKeyword(kw)) { + throw new EvaluationError( + `Expected keyword in require spec, got ${kw.kind}`, + { spec, position: i } + ) + } + + if (kw.name === ':as') { + i++ + const alias = elements[i] + if (!alias || !isSymbol(alias)) { + throw new EvaluationError(':as expects a symbol alias', { + spec, + position: i, + }) + } + currentEnv.ns!.aliases.set(alias.name, targetEnv.ns!) + i++ + } else if (kw.name === ':refer') { + i++ + const symsVec = elements[i] + if (!symsVec || !isVector(symsVec)) { + throw new EvaluationError(':refer expects a vector of symbols', { + spec, + position: i, + }) + } + for (const sym of symsVec.value) { + if (!isSymbol(sym)) { + throw new EvaluationError(':refer vector must contain only symbols', { + spec, + sym, + }) + } + const v = targetEnv.ns!.vars.get(sym.name) + if (v === undefined) { + throw new EvaluationError( + `Symbol ${sym.name} not found in namespace ${nsName}`, + { nsName, symbol: sym.name } + ) + } + currentEnv.ns!.vars.set(sym.name, v) + } + i++ + } else { + throw new EvaluationError( + `Unknown require option ${kw.name}. Supported: :as, :refer`, + { spec, keyword: kw.name } + ) + } + } +} diff --git a/packages/conjure-js/src/core/runtime.ts b/packages/conjure-js/src/core/runtime.ts index 49e2ddd..0976be2 100644 --- a/packages/conjure-js/src/core/runtime.ts +++ b/packages/conjure-js/src/core/runtime.ts @@ -1,36 +1,36 @@ -import { - isKeyword, - isList, - isNamespace, - isSymbol, - isVector, -} from './assertions' +import { isKeyword, isString, isSymbol, isVector } from './assertions' import { resolveModuleOrder, type RuntimeModule, type ModuleContext, } from './module' -import { internVar, makeEnv, makeNamespace, tryLookup } from './env' +import { internVar, makeEnv, makeNamespace } from './env' import { EvaluationError } from './errors' import { v } from './factories' import { readForms } from './reader' import { tokenize } from './tokenizer' -import type { - CljNamespace, - CljValue, - Env, - EvaluationContext, - Token, - TokenSymbol, -} from './types' +import type { CljNamespace, CljValue, Env, EvaluationContext } from './types' import { builtInNamespaceSources } from '../clojure/generated/builtin-namespace-registry' import { makeCoreModule } from './core-module' +import { makeJsModule } from './stdlib/js-namespace' +import { + cloneRegistry, + ensureNamespaceInRegistry, + processRequireSpec, +} from './registry' +import type { NamespaceRegistry } from './registry' +import { + extractAliasMapFromTokens, + extractNsNameFromTokens, + extractRequireClauses, +} from './ns-forms' +import { wireIdeStubs, wireNsCore } from './bootstrap' // --------------------------------------------------------------------------- // Core types // --------------------------------------------------------------------------- -export type NamespaceRegistry = Map +export type { NamespaceRegistry } export type RuntimeSnapshot = { registry: NamespaceRegistry @@ -59,6 +59,16 @@ export type Runtime = { fromEnv: Env, ctx: EvaluationContext ): void + /** + * Async variant of processNsRequires. + * Handles both symbol specs (sync) and string specs (async via ctx.importModule). + * Must be used when the ns form contains string (:require ["module" :as Alias]) entries. + */ + processNsRequiresAsync( + forms: CljValue[], + fromEnv: Env, + ctx: EvaluationContext + ): Promise // File loading — ctx comes from the owning session loadFile( @@ -75,312 +85,9 @@ export type Runtime = { installModules(modules: RuntimeModule[]): void } -// --------------------------------------------------------------------------- -// Clone helpers — used by snapshot / restoreRuntime -// --------------------------------------------------------------------------- - -function cloneBindings(bindings: Map): Map { - const out = new Map() - for (const [k, v] of bindings) { - out.set(k, v.kind === 'var' ? { ...v } : v) - } - return out -} - -function cloneEnv(env: Env, memo: Map): Env { - if (memo.has(env)) return memo.get(env)! - const cloned: Env = { - bindings: cloneBindings(env.bindings), - outer: null, - } - if (env.ns) { - cloned.ns = { - kind: 'namespace', - name: env.ns.name, - vars: new Map([...env.ns.vars].map(([k, v]) => [k, { ...v }])), - aliases: new Map(), // wired in cloneRegistry pass 2 - readerAliases: new Map(env.ns.readerAliases), - } - } - memo.set(env, cloned) - if (env.outer) cloned.outer = cloneEnv(env.outer, memo) - return cloned -} - -export function cloneRegistry(registry: NamespaceRegistry): NamespaceRegistry { - const memo = new Map() - const next = new Map() - // Pass 1: clone all envs (ns.aliases left empty) - for (const [name, env] of registry) { - next.set(name, cloneEnv(env, memo)) - } - // Pass 2: wire ns.aliases to the cloned CljNamespace objects - for (const [name, env] of registry) { - const clonedEnv = next.get(name)! - if (env.ns && clonedEnv.ns) { - for (const [alias, origNs] of env.ns.aliases) { - const targetCloned = next.get(origNs.name) - if (targetCloned?.ns) clonedEnv.ns.aliases.set(alias, targetCloned.ns) - } - } - } - return next -} - -// --------------------------------------------------------------------------- -// Token scan helpers — lightweight pre-parse scans for ns form metadata. -// Exported so session.evaluate can reuse them. -// --------------------------------------------------------------------------- - -// Looks for the pattern: LParen Symbol("ns") Symbol(name) at the top of the -// token stream. Returns the namespace name or null. -export function extractNsNameFromTokens(tokens: Token[]): string | null { - const meaningful = tokens.filter((t) => t.kind !== 'Comment') - if (meaningful.length < 3) return null - if (meaningful[0].kind !== 'LParen') return null - if (meaningful[1].kind !== 'Symbol' || meaningful[1].value !== 'ns') - return null - if (meaningful[2].kind !== 'Symbol') return null - return meaningful[2].value -} - -// Returns Map { 'alias' -> 'full.ns.name' } for all [some.ns :as alias] and -// [some.ns :as-alias alias] specs found in the ns form's :require clauses. -// Runs before readForms so the reader can expand ::alias/foo at read time. -export function extractAliasMapFromTokens( - tokens: Token[] -): Map { - const aliases = new Map() - const meaningful = tokens.filter( - (t) => t.kind !== 'Comment' && t.kind !== 'Whitespace' - ) - if (meaningful.length < 3) return aliases - if (meaningful[0].kind !== 'LParen') return aliases - if (meaningful[1].kind !== 'Symbol' || meaningful[1].value !== 'ns') - return aliases - - let i = 3 // skip ( ns - let depth = 1 - while (i < meaningful.length && depth > 0) { - const tok = meaningful[i] - if (tok.kind === 'LParen') { - depth++ - i++ - continue - } - if (tok.kind === 'RParen') { - depth-- - i++ - continue - } - if (tok.kind === 'LBracket') { - let j = i + 1 - let nsSym: string | null = null - while (j < meaningful.length && meaningful[j].kind !== 'RBracket') { - const t = meaningful[j] - if (t.kind === 'Symbol' && nsSym === null) { - nsSym = t.value - } - if ( - t.kind === 'Keyword' && - (t.value === ':as' || t.value === ':as-alias') - ) { - j++ - if ( - j < meaningful.length && - meaningful[j].kind === 'Symbol' && - nsSym - ) { - aliases.set((meaningful[j] as TokenSymbol).value, nsSym) - } - } - j++ - } - } - i++ - } - return aliases -} - -function findNsForm(forms: CljValue[]) { - const nsForm = forms.find( - (f) => - isList(f) && - f.value.length > 0 && - isSymbol(f.value[0]) && - f.value[0].name === 'ns' - ) - if (!nsForm || !isList(nsForm)) return null - return nsForm -} - -function extractRequireClauses(forms: CljValue[]): CljValue[][] { - const nsForm = findNsForm(forms) - if (!nsForm) return [] - const clauses: CljValue[][] = [] - for (let i = 2; i < nsForm.value.length; i++) { - const clause = nsForm.value[i] - if ( - isList(clause) && - isKeyword(clause.value[0]) && - clause.value[0].name === ':require' - ) { - clauses.push(clause.value.slice(1)) - } - } - return clauses -} - -// --------------------------------------------------------------------------- -// processRequireSpec — processes a single [ns.name :as alias :refer [...]] spec. -// resolveNs is called when the target namespace isn't yet loaded. -// --------------------------------------------------------------------------- - -function processRequireSpec( - spec: CljValue, - currentEnv: Env, - registry: NamespaceRegistry, - resolveNs?: (nsName: string) => boolean -): void { - if (!isVector(spec)) { - throw new EvaluationError( - 'require spec must be a vector, e.g. [my.ns :as alias]', - { spec } - ) - } - - const elements = spec.value - if (elements.length === 0 || !isSymbol(elements[0])) { - throw new EvaluationError( - 'First element of require spec must be a namespace symbol', - { spec } - ) - } - - const nsName = elements[0].name - - const hasAsAlias = elements.some( - (el) => isKeyword(el) && el.name === ':as-alias' - ) - if (hasAsAlias) { - let i = 1 - while (i < elements.length) { - const kw = elements[i] - if (!isKeyword(kw)) { - throw new EvaluationError( - `Expected keyword in require spec, got ${kw.kind}`, - { spec, position: i } - ) - } - if (kw.name === ':as-alias') { - i++ - const alias = elements[i] - if (!alias || !isSymbol(alias)) { - throw new EvaluationError(':as-alias expects a symbol alias', { - spec, - position: i, - }) - } - currentEnv.ns!.readerAliases.set(alias.name, nsName) - i++ - } else { - throw new EvaluationError( - `:as-alias specs only support :as-alias, got ${kw.name}`, - { spec } - ) - } - } - return - } - - let targetEnv = registry.get(nsName) - if (!targetEnv && resolveNs) { - resolveNs(nsName) - targetEnv = registry.get(nsName) - } - if (!targetEnv) { - throw new EvaluationError( - `Namespace ${nsName} not found. Only already-loaded namespaces can be required.`, - { nsName } - ) - } - - let i = 1 - while (i < elements.length) { - const kw = elements[i] - if (!isKeyword(kw)) { - throw new EvaluationError( - `Expected keyword in require spec, got ${kw.kind}`, - { spec, position: i } - ) - } - - if (kw.name === ':as') { - i++ - const alias = elements[i] - if (!alias || !isSymbol(alias)) { - throw new EvaluationError(':as expects a symbol alias', { - spec, - position: i, - }) - } - currentEnv.ns!.aliases.set(alias.name, targetEnv.ns!) - i++ - } else if (kw.name === ':refer') { - i++ - const symsVec = elements[i] - if (!symsVec || !isVector(symsVec)) { - throw new EvaluationError(':refer expects a vector of symbols', { - spec, - position: i, - }) - } - for (const sym of symsVec.value) { - if (!isSymbol(sym)) { - throw new EvaluationError(':refer vector must contain only symbols', { - spec, - sym, - }) - } - const v = targetEnv.ns!.vars.get(sym.name) - if (v === undefined) { - throw new EvaluationError( - `Symbol ${sym.name} not found in namespace ${nsName}`, - { nsName, symbol: sym.name } - ) - } - currentEnv.ns!.vars.set(sym.name, v) - } - i++ - } else { - throw new EvaluationError( - `Unknown require option ${kw.name}. Supported: :as, :refer`, - { spec, keyword: kw.name } - ) - } - } -} - -// --------------------------------------------------------------------------- -// ensureNamespaceInRegistry — creates namespace env if it doesn't exist yet -// --------------------------------------------------------------------------- - -function ensureNamespaceInRegistry( - registry: NamespaceRegistry, - coreEnv: Env, - name: string -): Env { - if (!registry.has(name)) { - const nsEnv = makeEnv(coreEnv) - nsEnv.ns = makeNamespace(name) - registry.set(name, nsEnv) - } - return registry.get(name)! -} - // --------------------------------------------------------------------------- // buildRuntime — shared factory used by createRuntime and restoreRuntime. -// Wires all native fns, IO, introspection, require, and IDE stubs into coreEnv. +// Orchestrates registry wiring and native fn installation via bootstrap.ts. // Does NOT load clojure.core source — that's the session's bootstrap job. // --------------------------------------------------------------------------- @@ -425,297 +132,8 @@ function buildRuntime( return false } - // *ns* var — holds the current namespace as a CljNamespace value - const initialNsObj = registry.get('user')?.ns ?? makeNamespace('user') - internVar('*ns*', initialNsObj, coreEnv) - const nsVar = coreEnv.ns?.vars.get('*ns*') - if (nsVar) nsVar.dynamic = true - - // Helper: resolve a namespace symbol (or namespace object) to its CljNamespace - function resolveNsSym(sym: CljValue): CljNamespace | null { - if (sym === undefined) return null - if (isNamespace(sym)) return sym - if (!isSymbol(sym)) return null - return registry.get(sym.name)?.ns ?? null - } - - // Namespace introspection - internVar( - 'ns-name', - v.nativeFn('ns-name', (x: CljValue) => { - if (x === undefined) return v.nil() - if (x.kind === 'namespace') return v.symbol(x.name) - if (x.kind === 'symbol') return x - if (x.kind === 'string') return v.symbol(x.value) - return v.nil() - }), - coreEnv - ) - - internVar( - 'all-ns', - v.nativeFn('all-ns', () => - v.list([...registry.values()].map((env) => env.ns!).filter(Boolean)) - ), - coreEnv - ) - - internVar( - 'find-ns', - v.nativeFn('find-ns', (sym: CljValue) => { - if (sym === undefined || !isSymbol(sym)) return v.nil() - return registry.get(sym.name)?.ns ?? v.nil() - }), - coreEnv - ) - - internVar( - 'ns-aliases', - v.nativeFn('ns-aliases', (sym: CljValue) => { - const ns = resolveNsSym(sym) - if (!ns) return v.map([]) - const entries: [CljValue, CljValue][] = [] - ns.aliases.forEach((targetNs, alias) => { - entries.push([v.symbol(alias), targetNs]) - }) - return v.map(entries) - }), - coreEnv - ) - - internVar( - 'ns-interns', - v.nativeFn('ns-interns', (sym: CljValue) => { - const ns = resolveNsSym(sym) - if (!ns) return v.map([]) - const entries: [CljValue, CljValue][] = [] - ns.vars.forEach((theVar, name) => { - if (theVar.ns === ns.name) entries.push([v.symbol(name), theVar]) - }) - return v.map(entries) - }), - coreEnv - ) - - internVar( - 'ns-publics', - v.nativeFn('ns-publics', (sym: CljValue) => { - const ns = resolveNsSym(sym) - if (!ns) return v.map([]) - const entries: [CljValue, CljValue][] = [] - ns.vars.forEach((theVar, name) => { - if (theVar.ns === ns.name) entries.push([v.symbol(name), theVar]) - }) - return v.map(entries) - }), - coreEnv - ) - - internVar( - 'ns-refers', - v.nativeFn('ns-refers', (sym: CljValue) => { - const ns = resolveNsSym(sym) - if (!ns) return v.map([]) - const entries: [CljValue, CljValue][] = [] - ns.vars.forEach((theVar, name) => { - if (theVar.ns !== ns.name) entries.push([v.symbol(name), theVar]) - }) - return v.map(entries) - }), - coreEnv - ) - - internVar( - 'ns-map', - v.nativeFn('ns-map', (sym: CljValue) => { - const ns = resolveNsSym(sym) - if (!ns) return v.map([]) - const entries: [CljValue, CljValue][] = [] - ns.vars.forEach((theVar, name) => { - entries.push([v.symbol(name), theVar]) - }) - return v.map(entries) - }), - coreEnv - ) - - internVar( - 'ns-imports', - v.nativeFn('ns-imports', (_sym: CljValue) => v.map([])), - coreEnv - ) - - internVar( - 'the-ns', - v.nativeFn('the-ns', (sym: CljValue) => { - if (sym === undefined) return v.nil() - if (isNamespace(sym)) return sym - if (!isSymbol(sym)) return v.nil() - return registry.get(sym.name)?.ns ?? v.nil() - }), - coreEnv - ) - - internVar( - 'instance?', - v.nativeFn('instance?', (_cls: CljValue, _obj: CljValue) => - v.boolean(false) - ), - coreEnv - ) - - internVar( - 'class', - v.nativeFn('class', (x: CljValue) => { - if (x === undefined) return v.nil() - return v.string(`conjure.${x.kind}`) - }), - coreEnv - ) - - internVar( - 'class?', - v.nativeFn('class?', (_x: CljValue) => v.boolean(false)), - coreEnv - ) - - internVar( - 'special-symbol?', - v.nativeFn('special-symbol?', (sym: CljValue) => { - if (sym === undefined || !isSymbol(sym)) return v.boolean(false) - const specials = new Set([ - 'def', - 'if', - 'do', - 'let', - 'quote', - 'var', - 'fn', - 'loop', - 'recur', - 'throw', - 'try', - 'catch', - 'finally', - 'ns', - 'defmacro', - 'binding', - 'monitor-enter', - 'monitor-exit', - 'new', - 'set!', - '.', - 'import', - ]) - return v.boolean(specials.has(sym.name)) - }), - coreEnv - ) - - internVar( - 'loaded-libs', - v.nativeFn('loaded-libs', () => v.set([...registry.keys()].map(v.symbol))), - coreEnv - ) - - // require — context-aware so it can thread ctx to resolveNamespace - internVar( - 'require', - v.nativeFnCtx('require', (ctx, _callEnv, ...args: CljValue[]) => { - const currentEnv = registry.get(currentNsRef)! - for (const arg of args) { - processRequireSpec(arg, currentEnv, registry, (nsName) => - resolveNamespace(nsName, ctx) - ) - } - return v.nil() - }), - coreEnv - ) - - internVar( - 'resolve', - v.nativeFn('resolve', (sym: CljValue) => { - if (!isSymbol(sym)) return v.nil() - const slashIdx = sym.name.indexOf('/') - if (slashIdx > 0) { - const nsName = sym.name.slice(0, slashIdx) - const symName = sym.name.slice(slashIdx + 1) - const nsEnv = registry.get(nsName) ?? null - if (!nsEnv) return v.nil() - return tryLookup(symName, nsEnv) ?? v.nil() - } - const currentEnv = registry.get(currentNsRef)! - return tryLookup(sym.name, currentEnv) ?? v.nil() - }), - coreEnv - ) - - // IDE stubs: clojure.reflect - const reflectEnv = ensureNamespaceInRegistry( - registry, - coreEnv, - 'clojure.reflect' - ) - internVar( - 'parse-flags', - v.nativeFn('parse-flags', (_flags: CljValue, _kind: CljValue) => v.set([])), - reflectEnv - ) - internVar( - 'reflect', - v.nativeFn('reflect', (_obj: CljValue) => v.map([])), - reflectEnv - ) - internVar( - 'type-reflect', - v.nativeFn('type-reflect', (_typeobj: CljValue, ..._opts: CljValue[]) => - v.map([]) - ), - reflectEnv - ) - - // IDE stubs: cursive.repl.runtime - const cursiveEnv = ensureNamespaceInRegistry( - registry, - coreEnv, - 'cursive.repl.runtime' - ) - internVar( - 'completions', - v.nativeFn('completions', (..._args: CljValue[]) => v.nil()), - cursiveEnv - ) - - // Java class stubs — Cursive references these as bare symbols for type checks - for (const javaClass of [ - 'Class', - 'Object', - 'String', - 'Number', - 'Boolean', - 'Integer', - 'Long', - 'Double', - 'Float', - 'Byte', - 'Short', - 'Character', - 'Void', - 'Math', - 'System', - 'Runtime', - 'Thread', - 'Throwable', - 'Exception', - 'Error', - 'Iterable', - 'Comparable', - 'Runnable', - 'Cloneable', - ]) { - internVar(javaClass, v.keyword(`:java.lang/${javaClass}`), coreEnv) - } + wireNsCore(registry, coreEnv, () => currentNsRef, resolveNamespace) + wireIdeStubs(registry, coreEnv) const runtime: Runtime = { get registry() { @@ -765,6 +183,17 @@ function buildRuntime( const requireClauses = extractRequireClauses(forms) for (const specs of requireClauses) { for (const spec of specs) { + if ( + isVector(spec) && + spec.value.length > 0 && + isString(spec.value[0]) + ) { + const specifier = spec.value[0].value + throw new EvaluationError( + `String module require ["${specifier}" :as ...] is async — use evaluateAsync() instead of evaluate()`, + { specifier } + ) + } processRequireSpec(spec, fromEnv, registry, (nsName) => resolveNamespace(nsName, ctx) ) @@ -772,6 +201,63 @@ function buildRuntime( } }, + async processNsRequiresAsync( + forms: CljValue[], + fromEnv: Env, + ctx: EvaluationContext + ): Promise { + const requireClauses = extractRequireClauses(forms) + for (const specs of requireClauses) { + for (const spec of specs) { + if ( + isVector(spec) && + spec.value.length > 0 && + isString(spec.value[0]) + ) { + // String module require — calls importModule and interns result as a var + const specifier = spec.value[0].value + if (!ctx.importModule) { + throw new EvaluationError( + `importModule is not configured; cannot require "${specifier}". Pass importModule to createSession().`, + { specifier } + ) + } + const elements = spec.value + let aliasName: string | null = null + for (let i = 1; i < elements.length; i++) { + if ( + isKeyword(elements[i]) && + (elements[i] as { name: string }).name === ':as' + ) { + i++ + const aliasSym = elements[i] + if (!aliasSym || !isSymbol(aliasSym)) { + throw new EvaluationError(':as expects a symbol alias', { + spec, + }) + } + aliasName = aliasSym.name + break + } + } + if (aliasName === null) { + throw new EvaluationError( + `String require spec must have an :as alias: ["${specifier}" :as Alias]`, + { spec } + ) + } + const rawModule = await ctx.importModule(specifier) + internVar(aliasName, v.jsValue(rawModule), fromEnv) + } else { + // Symbol require spec — sync path + processRequireSpec(spec, fromEnv, registry, (nsName) => + resolveNamespace(nsName, ctx) + ) + } + } + } + }, + loadFile( source: string, nsName: string | undefined, @@ -866,7 +352,7 @@ export function createRuntime(options?: RuntimeOptions): Runtime { registry.set('user', userEnv) const runtime = buildRuntime(registry, coreEnv, options) - runtime.installModules([makeCoreModule()]) + runtime.installModules([makeCoreModule(), makeJsModule()]) return runtime } diff --git a/packages/conjure-js/src/core/session.ts b/packages/conjure-js/src/core/session.ts index 8fecac0..c7b4ff6 100644 --- a/packages/conjure-js/src/core/session.ts +++ b/packages/conjure-js/src/core/session.ts @@ -1,18 +1,17 @@ import { builtInNamespaceSources } from '../clojure/generated/builtin-namespace-registry' import { CljThrownSignal, EvaluationError, ReaderError } from './errors' import { createEvaluationContext, RecurSignal } from './evaluator' +import { internVar, makeEnv } from './env' import { v } from './factories' +import { jsToClj } from './evaluator/js-interop' import type { RuntimeModule } from './module' +import { cljToJs as _cljToJs } from './conversions' import { formatErrorContext } from './positions' import { printString } from './printer' import { readForms } from './reader' import type { Runtime, RuntimeSnapshot } from './runtime' -import { - createRuntime, - extractAliasMapFromTokens, - extractNsNameFromTokens, - restoreRuntime, -} from './runtime' +import { createRuntime, restoreRuntime } from './runtime' +import { extractAliasMapFromTokens, extractNsNameFromTokens } from './ns-forms' import { tokenize } from './tokenizer' import type { CljNamespace, CljValue, Env } from './types' @@ -20,7 +19,7 @@ import type { CljNamespace, CljValue, Env } from './types' // Public types // --------------------------------------------------------------------------- -type SessionOptions = { +export type SessionOptions = { /** Primary output channel — wired to ctx.io.stdout (println, print, pr, prn, pprint, newline). */ output?: (text: string) => void /** Secondary error channel — wired to ctx.io.stderr. */ @@ -29,6 +28,23 @@ type SessionOptions = { sourceRoots?: string[] readFile?: (filePath: string) => string modules?: RuntimeModule[] + /** + * Ambient JS globals injected into the `js` namespace as CljJsValue vars. + * Each key becomes accessible as `js/` in Clojure code without any require. + * Example: `{ Math, console, fetch }` → `js/Math`, `js/console`, `js/fetch`. + */ + hostBindings?: Record + /** + * Called when (:require ["specifier" :as Alias]) is encountered. + * Must return (or resolve to) the module object, which is boxed as CljJsValue + * and bound to Alias in the current namespace. + * Only usable via evaluateAsync() — string requires are inherently async. + * Examples: + * Node/Bun: importModule: (s) => import(s) + * Vite: importModule: (s) => import(s) // Vite resolves statically at build time + * Tests: importModule: (s) => fakeModules[s] + */ + importModule?: (specifier: string) => unknown | Promise } export type Session = { @@ -40,6 +56,8 @@ export type Session = { setNs: (namespace: string) => void getNs: (namespace: string) => CljNamespace | null loadFile: (source: string, nsName?: string, filePath?: string) => string + /** Async variant of loadFile — handles string requires ((:require ["pkg" :as X])). */ + loadFileAsync: (source: string, nsName?: string, filePath?: string) => Promise evaluate: ( source: string, opts?: { lineOffset?: number; colOffset?: number; file?: string } @@ -49,6 +67,19 @@ export type Session = { opts?: { lineOffset?: number; colOffset?: number; file?: string } ) => Promise evaluateForms: (forms: CljValue[]) => CljValue + /** + * Call a CljFunction or CljNativeFunction using this session's evaluation context. + * Unlike the bare `applyFunction` export from `core/index`, this resolves namespaces + * through the session's runtime registry — required for any CLJ code that references + * qualified symbols like `js/Math` or `:require`-d aliases. + */ + applyFunction: (fn: CljValue, args: CljValue[]) => CljValue + /** + * Convert a CljValue to a plain JS value using this session's evaluation context. + * CLJ functions are wrapped as JS callbacks that invoke via session.applyFunction, + * ensuring namespace resolution works for js/Math and other runtime namespaces. + */ + cljToJs: (value: CljValue) => unknown addSourceRoot: (path: string) => void getCompletions: (prefix: string, nsName?: string) => string[] } @@ -77,6 +108,12 @@ function buildSessionFacade( stdout: options?.output ?? ((text) => console.log(text)), stderr: options?.stderr ?? ((text) => console.error(text)), } + ctx.importModule = options?.importModule + ctx.setCurrentNs = (name: string) => { + runtime.ensureNamespace(name) + currentNs = name + runtime.syncNsVar(name) + } const session: Session = { get runtime() { @@ -105,6 +142,21 @@ function buildSessionFacade( return runtime.loadFile(source, nsName, filePath, ctx) }, + async loadFileAsync(source: string, nsName?: string, filePath?: string): Promise { + // If there is no ns declaration in the source, pre-set the namespace from + // the hint so the forms evaluate in the right context. + if (nsName) { + const tokens = tokenize(source) + if (!extractNsNameFromTokens(tokens)) { + runtime.ensureNamespace(nsName) + currentNs = nsName + runtime.syncNsVar(nsName) + } + } + await session.evaluateAsync(source, { file: filePath }) + return currentNs + }, + addSourceRoot(path: string): void { runtime.addSourceRoot(path) }, @@ -177,10 +229,45 @@ function buildSessionFacade( source: string, opts?: { lineOffset?: number; colOffset?: number; file?: string } ): Promise { - const result = session.evaluate(source, opts) - if (result.kind !== 'pending') return result + ctx.currentSource = source + ctx.currentFile = opts?.file + ctx.currentLineOffset = opts?.lineOffset ?? 0 + ctx.currentColOffset = opts?.colOffset ?? 0 try { - return await result.promise + const tokens = tokenize(source) + const declaredNs = extractNsNameFromTokens(tokens) + if (declaredNs) { + runtime.ensureNamespace(declaredNs) + currentNs = declaredNs + runtime.syncNsVar(declaredNs) + } + const env = runtime.getNamespaceEnv(currentNs)! + const aliasMap = extractAliasMapFromTokens(tokens) + env.ns?.aliases.forEach((ns, alias) => { + aliasMap.set(alias, ns.name) + }) + env.ns?.readerAliases.forEach((nsName, alias) => { + aliasMap.set(alias, nsName) + }) + const forms = readForms(tokens, currentNs, aliasMap) + await runtime.processNsRequiresAsync(forms, env, ctx) + let result: CljValue = v.nil() + for (const form of forms) { + const expanded = ctx.expandAll(form, env) + result = ctx.evaluate(expanded, env) + } + if (result.kind !== 'pending') return result + try { + return await result.promise + } catch (e) { + if (e instanceof CljThrownSignal) { + throw new EvaluationError( + `Unhandled throw: ${printString(e.value)}`, + { thrownValue: e.value } + ) + } + throw e + } } catch (e) { if (e instanceof CljThrownSignal) { throw new EvaluationError( @@ -188,10 +275,35 @@ function buildSessionFacade( { thrownValue: e.value } ) } + if (e instanceof RecurSignal) { + throw new EvaluationError('recur called outside of loop or fn', { + args: e.args, + }) + } + if ( + (e instanceof EvaluationError || e instanceof ReaderError) && + e.pos + ) { + e.message += formatErrorContext(source, e.pos, { + lineOffset: ctx.currentLineOffset, + colOffset: ctx.currentColOffset, + }) + } throw e + } finally { + ctx.currentSource = undefined + ctx.currentFile = undefined } }, + applyFunction(fn: CljValue, args: CljValue[]): CljValue { + return ctx.applyCallable(fn, args, makeEnv()) + }, + + cljToJs(value: CljValue): unknown { + return _cljToJs(value, { applyFunction: (fn, args) => ctx.applyCallable(fn, args, makeEnv()) }) + }, + evaluateForms(forms: CljValue[]): CljValue { try { const env = runtime.getNamespaceEnv(currentNs)! @@ -258,6 +370,23 @@ export function createSession(options?: SessionOptions): Session { session.runtime.installModules(modules) } + // Intern host bindings into the js namespace as CljJsValue vars. + // Guard: built-in utility names (js/get, js/set!, js/call, etc.) must not be + // clobbered — they are already installed by makeJsModule() above. + if (options?.hostBindings) { + const jsEnv = runtime.getNamespaceEnv('js') + if (jsEnv) { + for (const [name, rawValue] of Object.entries(options.hostBindings)) { + if (jsEnv.ns?.vars.has(name)) { + throw new Error( + `createSession: hostBindings key '${name}' conflicts with built-in js/${name} — choose a different key` + ) + } + internVar(name, jsToClj(rawValue), jsEnv) + } + } + } + for (const source of options?.entries ?? []) { session.loadFile(source) } diff --git a/packages/conjure-js/src/core/stdlib/js-namespace.ts b/packages/conjure-js/src/core/stdlib/js-namespace.ts new file mode 100644 index 0000000..0f8816c --- /dev/null +++ b/packages/conjure-js/src/core/stdlib/js-namespace.ts @@ -0,0 +1,344 @@ +// js namespace — ambient JS interop utilities. +// Installed automatically alongside clojure.core. No explicit require needed. +// Users inject host globals (js/Math, js/console, etc.) via createSession({ hostBindings }). +import { EvaluationError } from '../errors' +import { v } from '../factories' +import { cljToJs, jsToClj } from '../evaluator/js-interop' +import type { RuntimeModule, VarMap } from '../module' +import type { CljValue, Env, EvaluationContext } from '../types' + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** + * Convert a Clojure key value to a JS object key string. + * Strings, keywords, and numbers are allowed — JS coerces numbers anyway. + */ +function resolveJsKey(key: CljValue, fnName: string): string { + if (key.kind === 'string') return key.value + if (key.kind === 'keyword') return key.name.slice(1) // strip leading ':' + if (key.kind === 'number') return String(key.value) // JS coerces obj[0] to obj["0"] + throw new EvaluationError( + `${fnName}: key must be a string, keyword, or number, got ${key.kind}`, + { key } + ) +} + +/** + * Extract the raw value from a target CljValue for use in js/get and js/set!. + * Mirrors extractRawTarget in js-interop.ts: CljJsValue, CljString, CljNumber, + * CljBoolean are all valid targets — JS auto-boxes primitives for property access. + * Nil and other Clojure types are rejected. + */ +function extractJsTarget(val: CljValue, fnName: string): unknown { + switch (val.kind) { + case 'js-value': return val.value + case 'string': + case 'number': + case 'boolean': return val.value + case 'nil': + throw new EvaluationError( + `${fnName}: cannot access properties on nil`, + { val } + ) + default: + throw new EvaluationError( + `${fnName}: expected a js-value or primitive, got ${val.kind}`, + { val } + ) + } +} + +// --------------------------------------------------------------------------- +// Module +// --------------------------------------------------------------------------- + +export function makeJsModule(): RuntimeModule { + return { + id: 'conjure-js/js-namespace', + declareNs: [ + { + name: 'js', + vars(_ctx): VarMap { + const map = new Map() + + // (js/get obj key) / (js/get obj key not-found) + // Dynamic property access. Primitives (string, number, boolean) are valid + // targets — same auto-boxing JS applies. Optional not-found default is returned + // when the property is absent (undefined), allowing idiomatic nil defaults. + map.set('get', { + value: v.nativeFn('js/get', (obj: CljValue, key: CljValue, ...rest: CljValue[]) => { + const raw = extractJsTarget(obj, 'js/get') as Record + const jsKey = resolveJsKey(key, 'js/get') + const result = raw[jsKey] + if (result === undefined && rest.length > 0) return rest[0] + return jsToClj(result) + }), + }) + + // (js/set! obj key val) — mutate a property; returns val + map.set('set!', { + value: v.nativeFnCtx( + 'js/set!', + (ctx: EvaluationContext, callEnv: Env, obj: CljValue, key: CljValue, val: CljValue) => { + const raw = extractJsTarget(obj, 'js/set!') as Record + const jsKey = resolveJsKey(key, 'js/set!') + raw[jsKey] = cljToJs(val, ctx, callEnv) + return val + } + ), + }) + + // (js/call fn & args) — call a JS function with no this binding + map.set('call', { + value: v.nativeFnCtx( + 'js/call', + (ctx: EvaluationContext, callEnv: Env, fn: CljValue, ...args: CljValue[]) => { + const rawFn = fn.kind === 'js-value' ? fn.value : undefined + if (typeof rawFn !== 'function') { + throw new EvaluationError( + `js/call: expected a js-value wrapping a function, got ${fn.kind}`, + { fn } + ) + } + const jsArgs = args.map((a) => cljToJs(a, ctx, callEnv)) + return jsToClj((rawFn as (...a: unknown[]) => unknown)(...jsArgs)) + } + ), + }) + + // (js/typeof x) — typeof equivalent for CljValues. + // Clojure primitives have unambiguous JS typeof values; js-value delegates to + // the raw typeof. Functions and other Clojure types throw — they're not at the + // JS boundary. + map.set('typeof', { + value: v.nativeFn('js/typeof', (x: CljValue) => { + switch (x.kind) { + case 'nil': return v.string('object') // typeof null === 'object' + case 'number': return v.string('number') + case 'string': return v.string('string') + case 'boolean': return v.string('boolean') + case 'js-value': return v.string(typeof x.value) + default: + throw new EvaluationError( + `js/typeof: cannot determine JS type of Clojure ${x.kind}`, + { x } + ) + } + }), + }) + + // (js/instanceof? obj cls) — obj instanceof cls + map.set('instanceof?', { + value: v.nativeFn('js/instanceof?', (obj: CljValue, cls: CljValue) => { + if (obj.kind !== 'js-value') { + throw new EvaluationError( + `js/instanceof?: expected js-value, got ${obj.kind}`, + { obj } + ) + } + if (cls.kind !== 'js-value') { + throw new EvaluationError( + `js/instanceof?: expected js-value constructor, got ${cls.kind}`, + { cls } + ) + } + return v.boolean( + obj.value instanceof (cls.value as new (...a: unknown[]) => unknown) + ) + }), + }) + + // (js/array? x) — Array.isArray on the raw value + map.set('array?', { + value: v.nativeFn('js/array?', (x: CljValue) => { + if (x.kind !== 'js-value') return v.boolean(false) + return v.boolean(Array.isArray(x.value)) + }), + }) + + // (js/null? x) — true if x is nil (JS null comes in as CljNil) + map.set('null?', { + value: v.nativeFn('js/null?', (x: CljValue) => { + return v.boolean(x.kind === 'nil') + }), + }) + + // (js/undefined? x) — true if x is CljJsValue wrapping undefined + map.set('undefined?', { + value: v.nativeFn('js/undefined?', (x: CljValue) => { + return v.boolean(x.kind === 'js-value' && x.value === undefined) + }), + }) + + // (js/some? x) — true if x is neither null (nil) nor undefined + map.set('some?', { + value: v.nativeFn('js/some?', (x: CljValue) => { + if (x.kind === 'nil') return v.boolean(false) + if (x.kind === 'js-value' && x.value === undefined) return v.boolean(false) + return v.boolean(true) + }), + }) + + // (js/get-in obj path) / (js/get-in obj path not-found) + // Deep property access. path must be a CljVector of string/keyword/number keys. + map.set('get-in', { + value: v.nativeFn('js/get-in', (obj: CljValue, path: CljValue, ...rest: CljValue[]) => { + if (path.kind !== 'vector') { + throw new EvaluationError( + `js/get-in: path must be a vector, got ${path.kind}`, + { path } + ) + } + // Validate root eagerly — nil root is always an error + if (obj.kind === 'nil') { + throw new EvaluationError( + 'js/get-in: cannot access properties on nil', + { obj } + ) + } + const notFound = rest.length > 0 ? rest[0] : v.jsValue(undefined) + let current: CljValue = obj + for (const key of path.value) { + if (current.kind === 'nil') return notFound + if (current.kind === 'js-value' && current.value === undefined) return notFound + const raw = extractJsTarget(current, 'js/get-in') as Record + const jsKey = resolveJsKey(key, 'js/get-in') + current = jsToClj((raw as Record)[jsKey]) + } + if (current.kind === 'js-value' && current.value === undefined && rest.length > 0) { + return notFound + } + return current + }), + }) + + // (js/prop key) / (js/prop key not-found) + // Returns a single-arg function that reads the given property from an object. + // Use with map/filter: (map (js/prop "name") users) + map.set('prop', { + value: v.nativeFn('js/prop', (key: CljValue, ...rest: CljValue[]) => { + const notFound = rest.length > 0 ? rest[0] : v.nil() + return v.nativeFn('js/prop-accessor', (obj: CljValue) => { + const raw = extractJsTarget(obj, 'js/prop') as Record + const jsKey = resolveJsKey(key, 'js/prop') + const result = raw[jsKey] + if (result === undefined) return notFound + return jsToClj(result) + }) + }), + }) + + // (js/method key & partialArgs) + // Returns a function that calls the named method on an object, prepending any partial args. + // (map (js/method "trim") strings) + // (map (js/method "toFixed" 2) numbers) + map.set('method', { + value: v.nativeFn('js/method', (key: CljValue, ...partialArgs: CljValue[]) => { + return v.nativeFnCtx( + 'js/method-caller', + (ctx: EvaluationContext, callEnv: Env, obj: CljValue, ...callArgs: CljValue[]) => { + const rawObj = extractJsTarget(obj, 'js/method') as Record + const jsKey = resolveJsKey(key, 'js/method') + const method = rawObj[jsKey] + if (typeof method !== 'function') { + throw new EvaluationError( + `js/method: property '${jsKey}' is not callable`, + { jsKey } + ) + } + const allArgs = [...partialArgs, ...callArgs].map((a) => cljToJs(a, ctx, callEnv)) + return jsToClj((method as (...a: unknown[]) => unknown).apply(rawObj, allArgs)) + } + ) + }), + }) + + // (js/merge obj1 obj2 ...) — Object.assign into a fresh object + map.set('merge', { + value: v.nativeFnCtx( + 'js/merge', + (ctx: EvaluationContext, callEnv: Env, ...args: CljValue[]) => { + const result = Object.assign({}, ...args.map((a) => cljToJs(a, ctx, callEnv))) + return v.jsValue(result) + } + ), + }) + + // (js/seq arr) — JS array → Clojure vector with elements converted via jsToClj + map.set('seq', { + value: v.nativeFn('js/seq', (arr: CljValue) => { + if (arr.kind !== 'js-value' || !Array.isArray(arr.value)) { + throw new EvaluationError( + `js/seq: expected a js-value wrapping an array, got ${arr.kind}`, + { arr } + ) + } + return v.vector((arr.value as unknown[]).map(jsToClj)) + }), + }) + + // (js/array & args) — variadic args → JS array as CljJsValue + map.set('array', { + value: v.nativeFnCtx( + 'js/array', + (ctx: EvaluationContext, callEnv: Env, ...args: CljValue[]) => { + return v.jsValue(args.map((a) => cljToJs(a, ctx, callEnv))) + } + ), + }) + + // (js/obj key val key val ...) — variadic key-val pairs → JS plain object as CljJsValue + map.set('obj', { + value: v.nativeFnCtx( + 'js/obj', + (ctx: EvaluationContext, callEnv: Env, ...args: CljValue[]) => { + if (args.length % 2 !== 0) { + throw new EvaluationError( + 'js/obj: requires even number of arguments', + { count: args.length } + ) + } + const result: Record = {} + for (let i = 0; i < args.length; i += 2) { + const jsKey = resolveJsKey(args[i], 'js/obj') + result[jsKey] = cljToJs(args[i + 1], ctx, callEnv) + } + return v.jsValue(result) + } + ), + }) + + // (js/keys obj) — Object.keys equivalent → Clojure vector of strings + map.set('keys', { + value: v.nativeFn('js/keys', (obj: CljValue) => { + const raw = extractJsTarget(obj, 'js/keys') as Record + return v.vector(Object.keys(raw).map(v.string)) + }), + }) + + // (js/values obj) — Object.values equivalent → Clojure vector, elements via jsToClj + map.set('values', { + value: v.nativeFn('js/values', (obj: CljValue) => { + const raw = extractJsTarget(obj, 'js/values') as Record + return v.vector(Object.values(raw).map(jsToClj)) + }), + }) + + // (js/entries obj) — Object.entries equivalent → vector of [key value] pairs + map.set('entries', { + value: v.nativeFn('js/entries', (obj: CljValue) => { + const raw = extractJsTarget(obj, 'js/entries') as Record + return v.vector( + Object.entries(raw).map(([k, val]) => v.vector([v.string(k), jsToClj(val)])) + ) + }), + }) + + return map + }, + }, + ], + } +} diff --git a/packages/conjure-js/src/core/stdlib/maps-sets.ts b/packages/conjure-js/src/core/stdlib/maps-sets.ts new file mode 100644 index 0000000..5f14083 --- /dev/null +++ b/packages/conjure-js/src/core/stdlib/maps-sets.ts @@ -0,0 +1,322 @@ +// Associative and set operations: hash-map, assoc, dissoc, keys, vals, zipmap, +// hash-set, set, set?, disj +// +// assoc and dissoc handle both maps and vectors (by numeric index). They live +// here because their primary semantic is associative (key→value update/remove); +// the vector branch is an edge case within the same dispatch. + +import { is } from '../assertions' +import { EvaluationError } from '../errors' +import { v } from '../factories' +import { printString } from '../printer' +import { toSeq } from '../transformations' +import { type CljNumber, type CljValue } from '../types' + +export const mapsSetsFunctions: Record = { + 'hash-map': v + .nativeFn('hash-map', function hashMapImpl(...kvals: CljValue[]) { + if (kvals.length === 0) { + return v.map([]) + } + if (kvals.length % 2 !== 0) { + throw new EvaluationError( + `hash-map expects an even number of arguments, got ${kvals.length}`, + { args: kvals } + ) + } + const entries: [CljValue, CljValue][] = [] + for (let i = 0; i < kvals.length; i += 2) { + const key = kvals[i] + const value = kvals[i + 1] + entries.push([key, value]) + } + return v.map(entries) + }) + .doc('Returns a new hash-map containing the given key-value pairs.', [ + ['&', 'kvals'], + ]), + + assoc: v + .nativeFn( + 'assoc', + function assocImpl(collection: CljValue, ...args: CljValue[]) { + if (!collection) { + throw new EvaluationError( + 'assoc expects a collection as first argument', + { collection } + ) + } + // nil is treated as an empty map, matching Clojure: (assoc nil :k v) => {:k v} + if (is.nil(collection)) { + collection = v.map([]) + } + if (is.list(collection)) { + throw new EvaluationError( + 'assoc on lists is not supported, use vectors instead', + { collection } + ) + } + if (!is.collection(collection)) { + throw EvaluationError.atArg( + `assoc expects a collection, got ${printString(collection)}`, + { collection }, + 0 + ) + } + if (args.length < 2) { + throw new EvaluationError('assoc expects at least two arguments', { + args, + }) + } + if (args.length % 2 !== 0) { + throw new EvaluationError( + 'assoc expects an even number of binding arguments', + { + args, + } + ) + } + if (is.vector(collection)) { + const newValues = [...collection.value] + for (let i = 0; i < args.length; i += 2) { + const index = args[i] + if (index.kind !== 'number') { + throw EvaluationError.atArg( + `assoc on vectors expects each key argument to be a index (number), got ${printString(index)}`, + { index }, + i + 1 + ) + } + if (index.value > newValues.length) { + throw EvaluationError.atArg( + `assoc index ${index.value} is out of bounds for vector of length ${newValues.length}`, + { index, collection }, + i + 1 + ) + } + newValues[(index as CljNumber).value] = args[i + 1] + } + return v.vector(newValues) + } + if (is.map(collection)) { + const newEntries: [CljValue, CljValue][] = [...collection.entries] + // need to find the entry with the same key and replace it, if it doesn't exist, add it + for (let i = 0; i < args.length; i += 2) { + const key = args[i] + const value = args[i + 1] + const entryIdx = newEntries.findIndex( + function findEntryByKey(entry) { + return is.equal(entry[0], key) + } + ) + if (entryIdx === -1) { + newEntries.push([key, value]) + } else { + newEntries[entryIdx] = [key, value] + } + } + return v.map(newEntries) + } + throw new EvaluationError( + `unhandled collection type, got ${printString(collection)}`, + { collection } + ) + } + ) + .doc( + 'Associates the value val with the key k in collection. If collection is a map, returns a new map with the same mappings, otherwise returns a vector with the new value at index k.', + [['collection', '&', 'kvals']] + ), + + dissoc: v + .nativeFn( + 'dissoc', + function dissocImpl(collection: CljValue, ...args: CljValue[]) { + if (!collection) { + throw new EvaluationError( + 'dissoc expects a collection as first argument', + { collection } + ) + } + if (is.list(collection)) { + throw EvaluationError.atArg( + 'dissoc on lists is not supported, use vectors instead', + { collection }, + 0 + ) + } + if (!is.collection(collection)) { + throw EvaluationError.atArg( + `dissoc expects a collection, got ${printString(collection)}`, + { collection }, + 0 + ) + } + if (is.vector(collection)) { + if (collection.value.length === 0) { + return collection // return the empty vector + } + const newValues = [...collection.value] + for (let i = 0; i < args.length; i += 1) { + const index = args[i] + if (index.kind !== 'number') { + throw EvaluationError.atArg( + `dissoc on vectors expects each key argument to be a index (number), got ${printString(index)}`, + { index }, + i + 1 + ) + } + if (index.value >= newValues.length) { + throw EvaluationError.atArg( + `dissoc index ${index.value} is out of bounds for vector of length ${newValues.length}`, + { index, collection }, + i + 1 + ) + } + newValues.splice(index.value, 1) + } + return v.vector(newValues) + } + if (is.map(collection)) { + if (collection.entries.length === 0) { + return collection // return the empty map + } + const newEntries: [CljValue, CljValue][] = [...collection.entries] + for (let i = 0; i < args.length; i += 1) { + const key = args[i] + const entryIdx = newEntries.findIndex( + function findEntryByKey(entry) { + return is.equal(entry[0], key) + } + ) + if (entryIdx === -1) { + return collection // not found, unchanged + } + newEntries.splice(entryIdx, 1) + } + return v.map(newEntries) + } + throw new EvaluationError( + `unhandled collection type, got ${printString(collection)}`, + { collection } + ) + } + ) + .doc( + 'Dissociates the key k from collection. If collection is a map, returns a new map with the same mappings, otherwise returns a vector with the value at index k removed.', + [['collection', '&', 'keys']] + ), + + zipmap: v + .nativeFn('zipmap', function zipmapImpl(ks: CljValue, vs: CljValue) { + if (ks === undefined || !is.seqable(ks)) { + throw new EvaluationError( + `zipmap expects a collection or string as first argument${ks !== undefined ? `, got ${printString(ks)}` : ''}`, + { ks } + ) + } + if (vs === undefined || !is.seqable(vs)) { + throw new EvaluationError( + `zipmap expects a collection or string as second argument${vs !== undefined ? `, got ${printString(vs)}` : ''}`, + { vs } + ) + } + const keys = toSeq(ks) + const vals = toSeq(vs) + const len = Math.min(keys.length, vals.length) + const entries: [CljValue, CljValue][] = [] + for (let i = 0; i < len; i++) { + entries.push([keys[i], vals[i]]) + } + return v.map(entries) + }) + .doc( + 'Returns a new map with the keys and values of the given collections.', + [['ks', 'vs']] + ), + + keys: v + .nativeFn('keys', function keysImpl(m: CljValue) { + if (m === undefined || !is.map(m)) { + throw EvaluationError.atArg( + `keys expects a map${m !== undefined ? `, got ${printString(m)}` : ''}`, + { m }, + 0 + ) + } + return v.vector( + m.entries.map(function extractKey([k]) { + return k + }) + ) + }) + .doc('Returns a vector of the keys of the given map.', [['m']]), + + vals: v + .nativeFn('vals', function valsImpl(m: CljValue) { + if (m === undefined || !is.map(m)) { + throw EvaluationError.atArg( + `vals expects a map${m !== undefined ? `, got ${printString(m)}` : ''}`, + { m }, + 0 + ) + } + return v.vector( + m.entries.map(function extractVal([, v]) { + return v + }) + ) + }) + .doc('Returns a vector of the values of the given map.', [['m']]), + + 'hash-set': v + .nativeFn('hash-set', function hashSetImpl(...args: CljValue[]) { + const deduped: CljValue[] = [] + for (const v of args) { + if (!deduped.some((existing) => is.equal(existing, v))) { + deduped.push(v) + } + } + return v.set(deduped) + }) + .doc('Returns a set containing the given values.', [['&', 'xs']]), + + set: v + .nativeFn('set', function setImpl(coll: CljValue) { + if (coll === undefined || coll.kind === 'nil') return v.set([]) + const items = toSeq(coll) + const deduped: CljValue[] = [] + for (const v of items) { + if (!deduped.some((existing) => is.equal(existing, v))) { + deduped.push(v) + } + } + return v.set(deduped) + }) + .doc('Returns a set of the distinct elements of the given collection.', [ + ['coll'], + ]), + + 'set?': v + .nativeFn('set?', function setPredicateImpl(x: CljValue) { + return v.boolean(x !== undefined && x.kind === 'set') + }) + .doc('Returns true if x is a set.', [['x']]), + + disj: v + .nativeFn('disj', function disjImpl(s: CljValue, ...items: CljValue[]) { + if (s === undefined || s.kind === 'nil') return v.set([]) + if (s.kind !== 'set') { + throw EvaluationError.atArg( + `disj expects a set, got ${printString(s)}`, + { s }, + 0 + ) + } + const newValues = s.values.filter( + (v) => !items.some((item) => is.equal(item, v)) + ) + return v.set(newValues) + }) + .doc('Returns a set with the given items removed.', [['s', '&', 'items']]), +} diff --git a/packages/conjure-js/src/core/stdlib/predicates.ts b/packages/conjure-js/src/core/stdlib/predicates.ts index 7e2e857..92e1611 100644 --- a/packages/conjure-js/src/core/stdlib/predicates.ts +++ b/packages/conjure-js/src/core/stdlib/predicates.ts @@ -2,12 +2,11 @@ // number?, string?, boolean?, vector?, list?, map?, keyword?, symbol?, fn?, // coll?, some, every? import { is } from '../assertions' -import { applyFunction } from '../evaluator' import { EvaluationError } from '../errors' import { v } from '../factories' import { printString } from '../printer' import { toSeq } from '../transformations' -import type { CljNumber, CljValue } from '../types' +import type { CljNumber, CljValue, Env, EvaluationContext } from '../types' export const predicateFunctions: Record = { 'nil?': v @@ -163,9 +162,14 @@ export const predicateFunctions: Record = { ['x'], ]), some: v - .nativeFn( + .nativeFnCtx( 'some', - function someImpl(pred: CljValue, coll: CljValue): CljValue { + function someImpl( + ctx: EvaluationContext, + callEnv: Env, + pred: CljValue, + coll: CljValue + ): CljValue { if (pred === undefined || !is.aFunction(pred)) { throw EvaluationError.atArg( `some expects a function as first argument${pred !== undefined ? `, got ${printString(pred)}` : ''}`, @@ -184,7 +188,7 @@ export const predicateFunctions: Record = { ) } for (const item of toSeq(coll)) { - const result = applyFunction(pred, [item]) + const result = ctx.applyFunction(pred, [item], callEnv) if (is.truthy(result)) { return result } @@ -198,9 +202,14 @@ export const predicateFunctions: Record = { ), 'every?': v - .nativeFn( + .nativeFnCtx( 'every?', - function everyPredImpl(pred: CljValue, coll: CljValue): CljValue { + function everyPredImpl( + ctx: EvaluationContext, + callEnv: Env, + pred: CljValue, + coll: CljValue + ): CljValue { if (pred === undefined || !is.aFunction(pred)) { throw EvaluationError.atArg( `every? expects a function as first argument${pred !== undefined ? `, got ${printString(pred)}` : ''}`, @@ -216,7 +225,7 @@ export const predicateFunctions: Record = { ) } for (const item of toSeq(coll)) { - if (is.falsy(applyFunction(pred, [item]))) { + if (is.falsy(ctx.applyFunction(pred, [item], callEnv))) { return v.boolean(false) } } diff --git a/packages/conjure-js/src/core/stdlib/collections.ts b/packages/conjure-js/src/core/stdlib/seq.ts similarity index 55% rename from packages/conjure-js/src/core/stdlib/collections.ts rename to packages/conjure-js/src/core/stdlib/seq.ts index d5ddac3..61d9859 100644 --- a/packages/conjure-js/src/core/stdlib/collections.ts +++ b/packages/conjure-js/src/core/stdlib/seq.ts @@ -1,6 +1,10 @@ -// Collections: list, vector, hash-map, first, rest, seq, cons, conj, count, -// nth, get, assoc, dissoc, keys, vals, zipmap, -// last, reverse, empty?, repeat*, range*, vec, subvec, peek, pop, empty +// Sequence abstraction: list, seq, first, rest, cons, conj, count, empty?, empty, +// nth, get, contains?, last, reverse, repeat*, range* +// +// These are the "core sequence protocol" operations — they apply uniformly across +// all collection types. conj lives here because it implements the sequence +// construction protocol (prepend for lists, append for vectors, kv-pair for maps, +// element dedup for sets). import { is } from '../assertions' import { EvaluationError } from '../errors' @@ -18,7 +22,7 @@ import { type CljVector, } from '../types' -export const collectionFunctions: Record = { +export const seqFunctions: Record = { list: v .nativeFn('list', function listImpl(...args: CljValue[]) { if (args.length === 0) { @@ -27,36 +31,7 @@ export const collectionFunctions: Record = { return v.list(args) }) .doc('Returns a new list containing the given values.', [['&', 'args']]), - vector: v - .nativeFn('vector', function vectorImpl(...args: CljValue[]) { - if (args.length === 0) { - return v.vector([]) - } - return v.vector(args) - }) - .doc('Returns a new vector containing the given values.', [['&', 'args']]), - 'hash-map': v - .nativeFn('hash-map', function hashMapImpl(...kvals: CljValue[]) { - if (kvals.length === 0) { - return v.map([]) - } - if (kvals.length % 2 !== 0) { - throw new EvaluationError( - `hash-map expects an even number of arguments, got ${kvals.length}`, - { args: kvals } - ) - } - const entries: [CljValue, CljValue][] = [] - for (let i = 0; i < kvals.length; i += 2) { - const key = kvals[i] - const value = kvals[i + 1] - entries.push([key, value]) - } - return v.map(entries) - }) - .doc('Returns a new hash-map containing the given key-value pairs.', [ - ['&', 'kvals'], - ]), + seq: v .nativeFn('seq', function seqImpl(coll: CljValue): CljValue { if (coll.kind === 'nil') return v.nil() @@ -80,6 +55,7 @@ export const collectionFunctions: Record = { 'Returns a sequence of the given collection or string. Strings yield a sequence of single-character strings.', [['coll']] ), + first: v .nativeFn('first', function firstImpl(collection: CljValue): CljValue { if (collection.kind === 'nil') return v.nil() @@ -102,6 +78,7 @@ export const collectionFunctions: Record = { .doc('Returns the first element of the given collection or string.', [ ['coll'], ]), + rest: v .nativeFn('rest', function restImpl(collection: CljValue): CljValue { if (collection.kind === 'nil') return v.list([]) @@ -147,6 +124,10 @@ export const collectionFunctions: Record = { 'Returns a sequence of the given collection or string excluding the first element.', [['coll']] ), + + // conj dispatches across all collection types — it belongs here as the primary + // sequence construction operation (cons-cell prepend for lists, append for + // vectors, kv-pair insert for maps, deduplicating add for sets). conj: v .nativeFn( 'conj', @@ -232,6 +213,7 @@ export const collectionFunctions: Record = { 'Appends args to the given collection. Lists append in reverse order to the head, vectors append to the tail, sets add unique elements.', [['collection', '&', 'args']] ), + cons: v .nativeFn('cons', function consImpl(x: CljValue, xs: CljValue) { // When tail is lazy-seq or cons, create a cons cell to preserve laziness @@ -264,175 +246,7 @@ export const collectionFunctions: Record = { .doc('Returns a new collection with x prepended to the head of xs.', [ ['x', 'xs'], ]), - assoc: v - .nativeFn( - 'assoc', - function assocImpl(collection: CljValue, ...args: CljValue[]) { - if (!collection) { - throw new EvaluationError( - 'assoc expects a collection as first argument', - { collection } - ) - } - // nil is treated as an empty map, matching Clojure: (assoc nil :k v) => {:k v} - if (is.nil(collection)) { - collection = v.map([]) - } - if (is.list(collection)) { - throw new EvaluationError( - 'assoc on lists is not supported, use vectors instead', - { collection } - ) - } - if (!is.collection(collection)) { - throw EvaluationError.atArg( - `assoc expects a collection, got ${printString(collection)}`, - { collection }, - 0 - ) - } - if (args.length < 2) { - throw new EvaluationError('assoc expects at least two arguments', { - args, - }) - } - if (args.length % 2 !== 0) { - throw new EvaluationError( - 'assoc expects an even number of binding arguments', - { - args, - } - ) - } - if (is.vector(collection)) { - const newValues = [...collection.value] - for (let i = 0; i < args.length; i += 2) { - const index = args[i] - if (index.kind !== 'number') { - throw EvaluationError.atArg( - `assoc on vectors expects each key argument to be a index (number), got ${printString(index)}`, - { index }, - i + 1 - ) - } - if (index.value > newValues.length) { - throw EvaluationError.atArg( - `assoc index ${index.value} is out of bounds for vector of length ${newValues.length}`, - { index, collection }, - i + 1 - ) - } - newValues[(index as CljNumber).value] = args[i + 1] - } - return v.vector(newValues) - } - if (is.map(collection)) { - const newEntries: [CljValue, CljValue][] = [...collection.entries] - // need to find the entry with the same key and replace it, if it doesn't exist, add it - for (let i = 0; i < args.length; i += 2) { - const key = args[i] - const value = args[i + 1] - const entryIdx = newEntries.findIndex( - function findEntryByKey(entry) { - return is.equal(entry[0], key) - } - ) - if (entryIdx === -1) { - newEntries.push([key, value]) - } else { - newEntries[entryIdx] = [key, value] - } - } - return v.map(newEntries) - } - throw new EvaluationError( - `unhandled collection type, got ${printString(collection)}`, - { collection } - ) - } - ) - .doc( - 'Associates the value val with the key k in collection. If collection is a map, returns a new map with the same mappings, otherwise returns a vector with the new value at index k.', - [['collection', '&', 'kvals']] - ), - dissoc: v - .nativeFn( - 'dissoc', - function dissocImpl(collection: CljValue, ...args: CljValue[]) { - if (!collection) { - throw new EvaluationError( - 'dissoc expects a collection as first argument', - { collection } - ) - } - if (is.list(collection)) { - throw EvaluationError.atArg( - 'dissoc on lists is not supported, use vectors instead', - { collection }, - 0 - ) - } - if (!is.collection(collection)) { - throw EvaluationError.atArg( - `dissoc expects a collection, got ${printString(collection)}`, - { collection }, - 0 - ) - } - if (is.vector(collection)) { - if (collection.value.length === 0) { - return collection // return the empty vector - } - const newValues = [...collection.value] - for (let i = 0; i < args.length; i += 1) { - const index = args[i] - if (index.kind !== 'number') { - throw EvaluationError.atArg( - `dissoc on vectors expects each key argument to be a index (number), got ${printString(index)}`, - { index }, - i + 1 - ) - } - if (index.value >= newValues.length) { - throw EvaluationError.atArg( - `dissoc index ${index.value} is out of bounds for vector of length ${newValues.length}`, - { index, collection }, - i + 1 - ) - } - newValues.splice(index.value, 1) - } - return v.vector(newValues) - } - if (is.map(collection)) { - if (collection.entries.length === 0) { - return collection // return the empty map - } - const newEntries: [CljValue, CljValue][] = [...collection.entries] - for (let i = 0; i < args.length; i += 1) { - const key = args[i] - const entryIdx = newEntries.findIndex( - function findEntryByKey(entry) { - return is.equal(entry[0], key) - } - ) - if (entryIdx === -1) { - return collection // not found, unchanged - } - newEntries.splice(entryIdx, 1) - } - return v.map(newEntries) - } - throw new EvaluationError( - `unhandled collection type, got ${printString(collection)}`, - { collection } - ) - } - ) - .doc( - 'Dissociates the key k from collection. If collection is a map, returns a new map with the same mappings, otherwise returns a vector with the value at index k removed.', - [['collection', '&', 'keys']] - ), + get: v .nativeFn( 'get', @@ -474,6 +288,7 @@ export const collectionFunctions: Record = { ['target', 'key', 'not-found'], ] ), + nth: v .nativeFn( 'nth', @@ -509,33 +324,6 @@ export const collectionFunctions: Record = { [['coll', 'n', 'not-found']] ), - zipmap: v - .nativeFn('zipmap', function zipmapImpl(ks: CljValue, vs: CljValue) { - if (ks === undefined || !is.seqable(ks)) { - throw new EvaluationError( - `zipmap expects a collection or string as first argument${ks !== undefined ? `, got ${printString(ks)}` : ''}`, - { ks } - ) - } - if (vs === undefined || !is.seqable(vs)) { - throw new EvaluationError( - `zipmap expects a collection or string as second argument${vs !== undefined ? `, got ${printString(vs)}` : ''}`, - { vs } - ) - } - const keys = toSeq(ks) - const vals = toSeq(vs) - const len = Math.min(keys.length, vals.length) - const entries: [CljValue, CljValue][] = [] - for (let i = 0; i < len; i++) { - entries.push([keys[i], vals[i]]) - } - return v.map(entries) - }) - .doc( - 'Returns a new map with the keys and values of the given collections.', - [['ks', 'vs']] - ), last: v .nativeFn('last', function lastImpl(coll: CljValue) { if (coll === undefined || (!is.list(coll) && !is.vector(coll))) { @@ -707,38 +495,7 @@ export const collectionFunctions: Record = { ['start', 'end'], ['start', 'end', 'step'], ]), - keys: v - .nativeFn('keys', function keysImpl(m: CljValue) { - if (m === undefined || !is.map(m)) { - throw EvaluationError.atArg( - `keys expects a map${m !== undefined ? `, got ${printString(m)}` : ''}`, - { m }, - 0 - ) - } - return v.vector( - m.entries.map(function extractKey([k]) { - return k - }) - ) - }) - .doc('Returns a vector of the keys of the given map.', [['m']]), - vals: v - .nativeFn('vals', function valsImpl(m: CljValue) { - if (m === undefined || !is.map(m)) { - throw EvaluationError.atArg( - `vals expects a map${m !== undefined ? `, got ${printString(m)}` : ''}`, - { m }, - 0 - ) - } - return v.vector( - m.entries.map(function extractVal([, v]) { - return v - }) - ) - }) - .doc('Returns a vector of the values of the given map.', [['m']]), + count: v .nativeFn('count', function countImpl(countable: CljValue) { if (countable.kind === 'nil') return v.number(0) @@ -785,157 +542,6 @@ export const collectionFunctions: Record = { ['countable'], ]), - 'hash-set': v - .nativeFn('hash-set', function hashSetImpl(...args: CljValue[]) { - const deduped: CljValue[] = [] - for (const v of args) { - if (!deduped.some((existing) => is.equal(existing, v))) { - deduped.push(v) - } - } - return v.set(deduped) - }) - .doc('Returns a set containing the given values.', [['&', 'xs']]), - - set: v - .nativeFn('set', function setImpl(coll: CljValue) { - if (coll === undefined || coll.kind === 'nil') return v.set([]) - const items = toSeq(coll) - const deduped: CljValue[] = [] - for (const v of items) { - if (!deduped.some((existing) => is.equal(existing, v))) { - deduped.push(v) - } - } - return v.set(deduped) - }) - .doc('Returns a set of the distinct elements of the given collection.', [ - ['coll'], - ]), - - 'set?': v - .nativeFn('set?', function setPredicateImpl(x: CljValue) { - return v.boolean(x !== undefined && x.kind === 'set') - }) - .doc('Returns true if x is a set.', [['x']]), - - disj: v - .nativeFn('disj', function disjImpl(s: CljValue, ...items: CljValue[]) { - if (s === undefined || s.kind === 'nil') return v.set([]) - if (s.kind !== 'set') { - throw EvaluationError.atArg( - `disj expects a set, got ${printString(s)}`, - { s }, - 0 - ) - } - const newValues = s.values.filter( - (v) => !items.some((item) => is.equal(item, v)) - ) - return v.set(newValues) - }) - .doc('Returns a set with the given items removed.', [['s', '&', 'items']]), - - vec: v - .nativeFn('vec', function vecImpl(coll: CljValue) { - if (coll === undefined || coll.kind === 'nil') return v.vector([]) - if (is.vector(coll)) return coll - if (!is.seqable(coll)) { - throw EvaluationError.atArg( - `vec expects a collection or string, got ${printString(coll)}`, - { coll }, - 0 - ) - } - return v.vector(toSeq(coll)) - }) - .doc('Creates a new vector containing the contents of coll.', [['coll']]), - - subvec: v - .nativeFn( - 'subvec', - function subvecImpl(vector: CljValue, start: CljValue, end?: CljValue) { - if (vector === undefined || !is.vector(vector)) { - throw EvaluationError.atArg( - `subvec expects a vector, got ${printString(vector)}`, - { v: vector }, - 0 - ) - } - if (start === undefined || start.kind !== 'number') { - throw EvaluationError.atArg( - `subvec expects a number start index`, - { start }, - 1 - ) - } - const s = start.value - const e = - end !== undefined && end.kind === 'number' - ? end.value - : vector.value.length - if (s < 0 || e > vector.value.length || s > e) { - throw new EvaluationError( - `subvec index out of bounds: start=${s}, end=${e}, length=${vector.value.length}`, - { v: vector, start, end } - ) - } - return v.vector(vector.value.slice(s, e)) - } - ) - .doc( - 'Returns a persistent vector of the items in vector from start (inclusive) to end (exclusive).', - [ - ['v', 'start'], - ['v', 'start', 'end'], - ] - ), - - peek: v - .nativeFn('peek', function peekImpl(coll: CljValue) { - if (coll === undefined || coll.kind === 'nil') return v.nil() - if (is.vector(coll)) { - return coll.value.length === 0 - ? v.nil() - : coll.value[coll.value.length - 1] - } - if (is.list(coll)) { - return coll.value.length === 0 ? v.nil() : coll.value[0] - } - throw EvaluationError.atArg( - `peek expects a list or vector, got ${printString(coll)}`, - { coll }, - 0 - ) - }) - .doc('For a list, same as first. For a vector, same as last.', [['coll']]), - - pop: v - .nativeFn('pop', function popImpl(coll: CljValue) { - if (coll === undefined || coll.kind === 'nil') { - throw EvaluationError.atArg("Can't pop empty list", { coll }, 0) - } - if (is.vector(coll)) { - if (coll.value.length === 0) - throw new EvaluationError("Can't pop empty vector", { coll }) - return v.vector(coll.value.slice(0, -1)) - } - if (is.list(coll)) { - if (coll.value.length === 0) - throw new EvaluationError("Can't pop empty list", { coll }) - return v.list(coll.value.slice(1)) - } - throw EvaluationError.atArg( - `pop expects a list or vector, got ${printString(coll)}`, - { coll }, - 0 - ) - }) - .doc( - 'For a list, returns a new list without the first item. For a vector, returns a new vector without the last item.', - [['coll']] - ), - empty: v .nativeFn('empty', function emptyImpl(coll: CljValue) { if (coll === undefined || coll.kind === 'nil') return v.nil() diff --git a/packages/conjure-js/src/core/stdlib/utils.ts b/packages/conjure-js/src/core/stdlib/utils.ts index 6f841e6..76d4392 100644 --- a/packages/conjure-js/src/core/stdlib/utils.ts +++ b/packages/conjure-js/src/core/stdlib/utils.ts @@ -5,7 +5,7 @@ import { tryLookup } from '../env' import { EvaluationError } from '../errors' import { v } from '../factories' import { makeGensym } from '../gensym' -import { joinLines, prettyPrintString, printString } from '../printer' +import { buildPrintContext, joinLines, prettyPrintString, printString, withPrintContext } from '../printer' import { readForms } from '../reader' import { tokenize } from '../tokenizer' import { valueToString } from '../transformations' @@ -320,8 +320,10 @@ export const utilFunctions: Record = { .doc('Returns a string describing the current Clojure version.', [[]]), 'pr-str': v - .nativeFn('pr-str', function prStrImpl(...args: CljValue[]) { - return v.string(args.map(printString).join(' ')) + .nativeFnCtx('pr-str', function prStrImpl(ctx: EvaluationContext, _callEnv, ...args: CljValue[]) { + return withPrintContext(buildPrintContext(ctx), () => + v.string(args.map(printString).join(' ')) + ) }) .doc( 'Returns a readable string representation of the given values (strings are quoted).', @@ -329,9 +331,9 @@ export const utilFunctions: Record = { ), 'pretty-print-str': v - .nativeFn( + .nativeFnCtx( 'pretty-print-str', - function prettyPrintStrImpl(...args: CljValue[]) { + function prettyPrintStrImpl(ctx: EvaluationContext, _callEnv, ...args: CljValue[]) { if (args.length === 0) return v.string('') const form = args[0] const widthArg = args[1] @@ -339,7 +341,9 @@ export const utilFunctions: Record = { widthArg !== undefined && widthArg.kind === 'number' ? widthArg.value : 80 - return v.string(prettyPrintString(form, maxWidth)) + return withPrintContext(buildPrintContext(ctx), () => + v.string(prettyPrintString(form, maxWidth)) + ) } ) .doc('Returns a pretty-printed string representation of form.', [ @@ -367,14 +371,18 @@ export const utilFunctions: Record = { ), 'prn-str': v - .nativeFn('prn-str', function prnStrImpl(...args: CljValue[]) { - return v.string(args.map(printString).join(' ') + '\n') + .nativeFnCtx('prn-str', function prnStrImpl(ctx: EvaluationContext, _callEnv, ...args: CljValue[]) { + return withPrintContext(buildPrintContext(ctx), () => + v.string(args.map(printString).join(' ') + '\n') + ) }) .doc('pr-str to a string, followed by a newline.', [['&', 'args']]), 'print-str': v - .nativeFn('print-str', function printStrImpl(...args: CljValue[]) { - return v.string(args.map(valueToString).join(' ')) + .nativeFnCtx('print-str', function printStrImpl(ctx: EvaluationContext, _callEnv, ...args: CljValue[]) { + return withPrintContext(buildPrintContext(ctx), () => + v.string(args.map(valueToString).join(' ')) + ) }) .doc('print to a string (human-readable, no quotes on strings).', [ ['&', 'args'], diff --git a/packages/conjure-js/src/core/stdlib/vectors.ts b/packages/conjure-js/src/core/stdlib/vectors.ts new file mode 100644 index 0000000..b709116 --- /dev/null +++ b/packages/conjure-js/src/core/stdlib/vectors.ts @@ -0,0 +1,122 @@ +// Vector-specific operations: vector, vec, subvec, peek, pop +// +// These functions are exclusively concerned with vectors (or lists treated as +// a stack for peek/pop). Pure vector construction and stack operations. + +import { is } from '../assertions' +import { EvaluationError } from '../errors' +import { v } from '../factories' +import { printString } from '../printer' +import { toSeq } from '../transformations' +import { type CljValue } from '../types' + +export const vectorFunctions: Record = { + vector: v + .nativeFn('vector', function vectorImpl(...args: CljValue[]) { + if (args.length === 0) { + return v.vector([]) + } + return v.vector(args) + }) + .doc('Returns a new vector containing the given values.', [['&', 'args']]), + + vec: v + .nativeFn('vec', function vecImpl(coll: CljValue) { + if (coll === undefined || coll.kind === 'nil') return v.vector([]) + if (is.vector(coll)) return coll + if (!is.seqable(coll)) { + throw EvaluationError.atArg( + `vec expects a collection or string, got ${printString(coll)}`, + { coll }, + 0 + ) + } + return v.vector(toSeq(coll)) + }) + .doc('Creates a new vector containing the contents of coll.', [['coll']]), + + subvec: v + .nativeFn( + 'subvec', + function subvecImpl(vector: CljValue, start: CljValue, end?: CljValue) { + if (vector === undefined || !is.vector(vector)) { + throw EvaluationError.atArg( + `subvec expects a vector, got ${printString(vector)}`, + { v: vector }, + 0 + ) + } + if (start === undefined || start.kind !== 'number') { + throw EvaluationError.atArg( + `subvec expects a number start index`, + { start }, + 1 + ) + } + const s = start.value + const e = + end !== undefined && end.kind === 'number' + ? end.value + : vector.value.length + if (s < 0 || e > vector.value.length || s > e) { + throw new EvaluationError( + `subvec index out of bounds: start=${s}, end=${e}, length=${vector.value.length}`, + { v: vector, start, end } + ) + } + return v.vector(vector.value.slice(s, e)) + } + ) + .doc( + 'Returns a persistent vector of the items in vector from start (inclusive) to end (exclusive).', + [ + ['v', 'start'], + ['v', 'start', 'end'], + ] + ), + + peek: v + .nativeFn('peek', function peekImpl(coll: CljValue) { + if (coll === undefined || coll.kind === 'nil') return v.nil() + if (is.vector(coll)) { + return coll.value.length === 0 + ? v.nil() + : coll.value[coll.value.length - 1] + } + if (is.list(coll)) { + return coll.value.length === 0 ? v.nil() : coll.value[0] + } + throw EvaluationError.atArg( + `peek expects a list or vector, got ${printString(coll)}`, + { coll }, + 0 + ) + }) + .doc('For a list, same as first. For a vector, same as last.', [['coll']]), + + pop: v + .nativeFn('pop', function popImpl(coll: CljValue) { + if (coll === undefined || coll.kind === 'nil') { + throw EvaluationError.atArg("Can't pop empty list", { coll }, 0) + } + if (is.vector(coll)) { + if (coll.value.length === 0) + throw new EvaluationError("Can't pop empty vector", { coll }) + return v.vector(coll.value.slice(0, -1)) + } + if (is.list(coll)) { + if (coll.value.length === 0) + throw new EvaluationError("Can't pop empty list", { coll }) + return v.list(coll.value.slice(1)) + } + throw EvaluationError.atArg( + `pop expects a list or vector, got ${printString(coll)}`, + { coll }, + 0 + ) + }) + .doc( + 'For a list, returns a new list without the first item. For a vector, returns a new vector without the last item.', + [['coll']] + ), +} diff --git a/packages/conjure-js/src/core/types.ts b/packages/conjure-js/src/core/types.ts index 735a3ba..15a7c98 100644 --- a/packages/conjure-js/src/core/types.ts +++ b/packages/conjure-js/src/core/types.ts @@ -22,6 +22,7 @@ export const valueKeywords = { lazySeq: 'lazy-seq', cons: 'cons', namespace: 'namespace', + jsValue: 'js-value', } as const export type ValueKeywords = (typeof valueKeywords)[keyof typeof valueKeywords] @@ -168,6 +169,17 @@ export type EvaluationContext = { currentFile?: string currentLineOffset?: number currentColOffset?: number + /** + * Optional module loader for string `:require` specs. + * Called by processNsRequiresAsync when it encounters ["specifier" :as Alias]. + * Wired from SessionOptions.importModule in buildSessionFacade. + */ + importModule?: (specifier: string) => unknown | Promise + /** + * Switches the session's current namespace. Wired by buildSessionFacade. + * Called by `in-ns` at runtime. Without this hook, `in-ns` is a no-op. + */ + setCurrentNs?: (name: string) => void } export type CljNativeFunction = { @@ -183,6 +195,8 @@ export type CljNativeFunction = { meta?: CljMap } +export type CljJsValue = { kind: 'js-value'; value: unknown } + // --- ASYNC (experimental, see evaluator/async-evaluator.ts) --- export type CljPending = { kind: 'pending' @@ -218,6 +232,7 @@ export type CljValue = | CljCons | CljNamespace | CljPending + | CljJsValue /** Tokens */ export const tokenKeywords = { diff --git a/packages/conjure-js/src/vite-plugin-clj/nrepl-relay.ts b/packages/conjure-js/src/nrepl/relay.ts similarity index 100% rename from packages/conjure-js/src/vite-plugin-clj/nrepl-relay.ts rename to packages/conjure-js/src/nrepl/relay.ts diff --git a/packages/conjure-js/src/vite-plugin-clj/__tests__/namespace-utils.spec.ts b/packages/conjure-js/src/vite-plugin-clj/__tests__/namespace-utils.spec.ts index 757cf5f..40917b3 100644 --- a/packages/conjure-js/src/vite-plugin-clj/__tests__/namespace-utils.spec.ts +++ b/packages/conjure-js/src/vite-plugin-clj/__tests__/namespace-utils.spec.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import { extractNsName, extractNsRequires, + extractStringRequires, nsToPath, pathToNs, } from '../namespace-utils' @@ -115,6 +116,86 @@ describe('extractNsRequires', () => { }) }) +describe('extractStringRequires', () => { + it('returns empty array when no ns form', () => { + expect(extractStringRequires('(def x 1)')).toEqual([]) + }) + + it('returns empty array when ns has only symbol requires', () => { + expect( + extractStringRequires('(ns my.app (:require [my.utils :as u]))') + ).toEqual([]) + }) + + it('extracts a single string require', () => { + expect( + extractStringRequires('(ns my.app (:require ["react" :as React]))') + ).toEqual(['react']) + }) + + it('extracts multiple string requires', () => { + expect( + extractStringRequires( + '(ns my.app (:require ["react" :as React] ["date-fns" :as d]))' + ) + ).toEqual(['react', 'date-fns']) + }) + + it('mixes symbol and string requires — only returns string ones', () => { + expect( + extractStringRequires( + '(ns my.app (:require [my.utils :as u] ["react" :as React] [other.ns] ["lodash" :as _]))' + ) + ).toEqual(['react', 'lodash']) + }) + + it('deduplicates repeated specifiers', () => { + expect( + extractStringRequires( + '(ns my.app (:require ["react" :as React] ["react" :as R2]))' + ) + ).toEqual(['react']) + }) + + it('resolves relative specifiers when filePath is provided', () => { + const result = extractStringRequires( + '(ns my.app (:require ["./utils" :as u]))', + '/project/src/app/core.clj' + ) + expect(result).toEqual(['/project/src/app/utils']) + }) + + it('resolves parent-relative specifiers when filePath is provided', () => { + const result = extractStringRequires( + '(ns my.app (:require ["../shared/helpers" :as h]))', + '/project/src/app/core.clj' + ) + expect(result).toEqual(['/project/src/shared/helpers']) + }) + + it('leaves relative specifiers as-is when filePath is not provided', () => { + expect( + extractStringRequires('(ns my.app (:require ["./utils" :as u]))') + ).toEqual(['./utils']) + }) + + it('leaves package specifiers unchanged even when filePath provided', () => { + expect( + extractStringRequires( + '(ns my.app (:require ["react" :as React] ["date-fns/format" :as fmt]))', + '/project/src/app/core.clj' + ) + ).toEqual(['react', 'date-fns/format']) + }) + + it('handles multiple :require clauses', () => { + const source = `(ns my.app + (:require ["react" :as React]) + (:require ["date-fns" :as d]))` + expect(extractStringRequires(source)).toEqual(['react', 'date-fns']) + }) +}) + describe('extractNsName', () => { it('returns null when no ns form', () => { expect(extractNsName('(def x 1)')).toBe(null) diff --git a/packages/conjure-js/src/vite-plugin-clj/__tests__/plugin.spec.ts b/packages/conjure-js/src/vite-plugin-clj/__tests__/plugin.spec.ts index 57d1be3..8523fad 100644 --- a/packages/conjure-js/src/vite-plugin-clj/__tests__/plugin.spec.ts +++ b/packages/conjure-js/src/vite-plugin-clj/__tests__/plugin.spec.ts @@ -3,7 +3,6 @@ import { describe, expect, it, beforeEach } from 'vitest' import { cljPlugin, safeJsIdentifier, generateModuleCode, generateDts } from '../index' import type { CodegenContext } from '../codegen' import type { Plugin, ResolvedConfig } from 'vite' -import { createSession } from '../../core/session' const projectRoot = resolve(__dirname, '../../..') @@ -30,7 +29,6 @@ function makePlugin(sourceRoots?: string[]) { function makeCodegenCtx(overrides?: Partial): CodegenContext { return { - session: createSession(), sourceRoots: ['src'], coreIndexPath: '/project/src/core/index.ts', virtualSessionId: 'virtual:clj-session', @@ -155,7 +153,13 @@ describe('cljPlugin', () => { const code = load('\0virtual:clj-session', {}) expect(code).toContain('import { createSession, printString }') expect(code).toContain('export function getSession()') - expect(code).toContain('createSession({ output:') + expect(code).toContain('importModule: (s) => __importMap[s]') + }) + + it('generates an empty import map when no string requires present', () => { + const load = getHookHandler(plugin.load, 'load') + const code = load('\0virtual:clj-session', {}) + expect(code).toContain('const __importMap = {') }) it('returns undefined for non-clj files', () => { @@ -179,8 +183,8 @@ describe('generateModuleCode', () => { expect(code).toContain('export function helper(...args)') expect(code).toContain('__ns.vars.get("helper")') expect(code).toContain('args.map(jsToClj)') - expect(code).toContain('applyFunction(fn, cljArgs)') - expect(code).toContain('cljToJs(result)') + expect(code).toContain('__session.applyFunction(fn, cljArgs)') + expect(code).toContain('cljToJs(result, __session)') }) it('generates const exports for non-function values', () => { @@ -189,6 +193,7 @@ describe('generateModuleCode', () => { const code = generateModuleCode(ctx, 'test.vals', source) expect(code).toContain('export const greeting = cljToJs(') + expect(code).toContain(', __session)') expect(code).toContain('"greeting"') expect(code).toContain('export const my_count = cljToJs(') expect(code).toContain('"my-count"') @@ -204,14 +209,22 @@ describe('generateModuleCode', () => { expect(code).toContain('export const x') }) + it('excludes private vars from exports', () => { + const ctx = makeCodegenCtx() + const source = '(ns test.priv)\n(defn- helper [x] x)\n(defn pub [x] (helper x))' + const code = generateModuleCode(ctx, 'test.priv', source) + + expect(code).not.toContain('export function helper') + expect(code).not.toContain('export const helper') + expect(code).toContain('export function pub(') + }) + it('generates dependency imports for require clauses', () => { const depPath = '/project/src/dep.clj' const ctx = makeCodegenCtx({ resolveDepPath: (depNs) => (depNs === 'dep' ? depPath : null), }) - ctx.session.loadFile('(ns dep)\n(def y 99)') - const source = '(ns test.deps (:require [dep :as d]))\n(def x d/y)' const code = generateModuleCode(ctx, 'test.deps', source) @@ -243,19 +256,29 @@ describe('generateModuleCode', () => { const code = generateModuleCode(ctx, 'test.core', source) expect(code).toContain( - 'import { cljToJs, jsToClj, applyFunction } from "/project/src/core/index.ts"' + 'import { cljToJs, jsToClj } from "/project/src/core/index.ts"' ) }) - it('embeds source as JSON-escaped string for loadFile call', () => { + it('emits sync loadFile call when no string requires', () => { const ctx = makeCodegenCtx() const source = '(ns test.embed)\n(def msg "hello\\nworld")' const code = generateModuleCode(ctx, 'test.embed', source) expect(code).toContain('__session.loadFile(') + expect(code).not.toContain('loadFileAsync') expect(code).toContain('"test.embed"') }) + it('emits await loadFileAsync call when string requires are present', () => { + const ctx = makeCodegenCtx() + const source = '(ns test.async-load (:require ["some-pkg" :as pkg]))\n(def x 1)' + const code = generateModuleCode(ctx, 'test.async-load', source) + + expect(code).toContain('await __session.loadFileAsync(') + expect(code).not.toContain('__session.loadFile(') + }) + it('uses ns name from source over path-derived name', () => { const ctx = makeCodegenCtx() const source = '(ns actual.name)\n(def x 1)' @@ -277,12 +300,21 @@ describe('generateModuleCode', () => { const ctx = makeCodegenCtx({ resolveDepPath: () => null, }) - ctx.session.loadFile('(ns dep)\n(def y 99)') const source = '(ns test.nodep (:require [dep :as d]))\n(def x d/y)' const code = generateModuleCode(ctx, 'test.nodep', source) expect(code).not.toContain('import "') }) + + it('emits minimal module (no exports) for namespace with only macros', () => { + const ctx = makeCodegenCtx() + const source = '(ns test.macros-only)\n(defmacro my-when [test & body] nil)' + const code = generateModuleCode(ctx, 'test.macros-only', source) + + expect(code).not.toContain('export function') + expect(code).not.toContain('export const') + expect(code).toContain('import.meta.hot') + }) }) describe('generateDts', () => { @@ -379,9 +411,9 @@ describe('generateDts', () => { ) }) - it('returns empty string when namespace fails to load', () => { + it('returns empty string when source has no top-level definitions', () => { const ctx = makeCodegenCtx() - const dts = generateDts(ctx, 'nonexistent.ns', '(invalid clojure source !!!)') + const dts = generateDts(ctx, 'empty.ns', '(ns empty.ns)') expect(dts).toBe('') }) diff --git a/packages/conjure-js/src/vite-plugin-clj/__tests__/static-analysis.spec.ts b/packages/conjure-js/src/vite-plugin-clj/__tests__/static-analysis.spec.ts new file mode 100644 index 0000000..c92fe37 --- /dev/null +++ b/packages/conjure-js/src/vite-plugin-clj/__tests__/static-analysis.spec.ts @@ -0,0 +1,290 @@ +import { describe, expect, it } from 'vitest' +import { readNamespaceVars } from '../static-analysis' + +// --------------------------------------------------------------------------- +// Helpers for compact arity assertions +// --------------------------------------------------------------------------- + +function arityShape(fixedCount: number, variadic: boolean) { + return { + params: expect.arrayContaining( + Array.from({ length: fixedCount }, () => expect.objectContaining({ kind: 'symbol' })) + ), + restParam: variadic ? expect.objectContaining({ kind: 'symbol' }) : null, + body: [], + } +} + +// --------------------------------------------------------------------------- +// defn +// --------------------------------------------------------------------------- + +describe('readNamespaceVars — defn', () => { + it('single-arity defn produces a fn var', () => { + const vars = readNamespaceVars('(defn add [a b] (+ a b))') + expect(vars).toHaveLength(1) + expect(vars[0]).toMatchObject({ name: 'add', kind: 'fn', isPrivate: false, isMacro: false }) + expect(vars[0].arities).toHaveLength(1) + expect(vars[0].arities![0].params).toHaveLength(2) + expect(vars[0].arities![0].restParam).toBeNull() + }) + + it('zero-arity defn', () => { + const vars = readNamespaceVars('(defn greet [] "hello")') + expect(vars[0]).toMatchObject({ name: 'greet', kind: 'fn' }) + expect(vars[0].arities![0].params).toHaveLength(0) + expect(vars[0].arities![0].restParam).toBeNull() + }) + + it('single-arity defn with docstring', () => { + const vars = readNamespaceVars('(defn foo "does foo" [x] x)') + expect(vars).toHaveLength(1) + expect(vars[0]).toMatchObject({ name: 'foo', kind: 'fn' }) + expect(vars[0].arities).toHaveLength(1) + expect(vars[0].arities![0].params).toHaveLength(1) + }) + + it('multi-arity defn', () => { + const vars = readNamespaceVars('(defn foo ([a] a) ([a b] (+ a b)))') + expect(vars).toHaveLength(1) + expect(vars[0]).toMatchObject({ name: 'foo', kind: 'fn' }) + expect(vars[0].arities).toHaveLength(2) + expect(vars[0].arities![0].params).toHaveLength(1) + expect(vars[0].arities![1].params).toHaveLength(2) + }) + + it('multi-arity defn with docstring', () => { + const vars = readNamespaceVars('(defn foo "docstring" ([a] a) ([a b] b))') + expect(vars[0].arities).toHaveLength(2) + }) + + it('variadic defn with & rest param', () => { + const vars = readNamespaceVars('(defn log [level & args] nil)') + expect(vars[0]).toMatchObject({ name: 'log', kind: 'fn' }) + expect(vars[0].arities![0].params).toHaveLength(1) + expect(vars[0].arities![0].restParam).not.toBeNull() + expect((vars[0].arities![0].restParam as { kind: string; name: string }).name).toBe('args') + }) + + it('variadic defn with no fixed params', () => { + const vars = readNamespaceVars('(defn all [& xs] xs)') + expect(vars[0].arities![0].params).toHaveLength(0) + expect(vars[0].arities![0].restParam).not.toBeNull() + }) + + it('defn- marks var as private', () => { + const vars = readNamespaceVars('(defn- helper [x] x)') + expect(vars[0]).toMatchObject({ name: 'helper', kind: 'fn', isPrivate: true, isMacro: false }) + }) + + it('defn with ^:private metadata marks var as private', () => { + const vars = readNamespaceVars('(defn ^:private helper [x] x)') + expect(vars[0]).toMatchObject({ name: 'helper', kind: 'fn', isPrivate: true, isMacro: false }) + }) + + it('defmacro marks var as macro', () => { + const vars = readNamespaceVars('(defmacro when-let [bindings & body] nil)') + expect(vars[0]).toMatchObject({ name: 'when-let', kind: 'fn', isPrivate: false, isMacro: true }) + expect(vars[0].arities).toHaveLength(1) + }) +}) + +// --------------------------------------------------------------------------- +// def +// --------------------------------------------------------------------------- + +describe('readNamespaceVars — def', () => { + it('def with number literal → const with tsType number', () => { + const vars = readNamespaceVars('(def max-retries 3)') + expect(vars[0]).toMatchObject({ name: 'max-retries', kind: 'const', tsType: 'number', isPrivate: false, isMacro: false }) + }) + + it('def with negative number literal', () => { + const vars = readNamespaceVars('(def offset -1)') + expect(vars[0]).toMatchObject({ name: 'offset', kind: 'const', tsType: 'number' }) + }) + + it('def with string literal → const with tsType string', () => { + const vars = readNamespaceVars('(def greeting "hello")') + expect(vars[0]).toMatchObject({ name: 'greeting', kind: 'const', tsType: 'string' }) + }) + + it('def with boolean true → const with tsType boolean', () => { + const vars = readNamespaceVars('(def debug? true)') + expect(vars[0]).toMatchObject({ name: 'debug?', kind: 'const', tsType: 'boolean' }) + }) + + it('def with boolean false → const with tsType boolean', () => { + const vars = readNamespaceVars('(def disabled? false)') + expect(vars[0]).toMatchObject({ name: 'disabled?', kind: 'const', tsType: 'boolean' }) + }) + + it('def with nil → const with tsType null', () => { + const vars = readNamespaceVars('(def nothing nil)') + expect(vars[0]).toMatchObject({ name: 'nothing', kind: 'const', tsType: 'null' }) + }) + + it('def with keyword → const with tsType string', () => { + const vars = readNamespaceVars('(def status :active)') + expect(vars[0]).toMatchObject({ name: 'status', kind: 'const', tsType: 'string' }) + }) + + it('def with computed expression → unknown', () => { + const vars = readNamespaceVars('(def result (+ 1 2))') + expect(vars[0]).toMatchObject({ name: 'result', kind: 'unknown' }) + expect(vars[0].tsType).toBeUndefined() + }) + + it('def with symbol reference → unknown', () => { + const vars = readNamespaceVars('(def alias other-var)') + expect(vars[0]).toMatchObject({ name: 'alias', kind: 'unknown' }) + }) + + it('def with no value (declaration only) → unknown', () => { + const vars = readNamespaceVars('(def unbound)') + expect(vars[0]).toMatchObject({ name: 'unbound', kind: 'unknown' }) + }) + + it('def with ^:private metadata marks var as private', () => { + const vars = readNamespaceVars('(def ^:private secret 42)') + expect(vars[0]).toMatchObject({ name: 'secret', kind: 'const', tsType: 'number', isPrivate: true }) + }) + + it('def with inline fn → fn with extracted arity', () => { + const vars = readNamespaceVars('(def transform (fn [x] (* x 2)))') + expect(vars[0]).toMatchObject({ name: 'transform', kind: 'fn' }) + expect(vars[0].arities).toHaveLength(1) + expect(vars[0].arities![0].params).toHaveLength(1) + }) + + it('def with named inline fn', () => { + const vars = readNamespaceVars('(def transform (fn my-fn [x] x))') + expect(vars[0]).toMatchObject({ name: 'transform', kind: 'fn' }) + expect(vars[0].arities![0].params).toHaveLength(1) + }) + + it('def with multi-arity inline fn', () => { + const vars = readNamespaceVars('(def f (fn ([x] x) ([x y] y)))') + expect(vars[0]).toMatchObject({ name: 'f', kind: 'fn' }) + expect(vars[0].arities).toHaveLength(2) + }) +}) + +// --------------------------------------------------------------------------- +// defonce +// --------------------------------------------------------------------------- + +describe('readNamespaceVars — defonce', () => { + it('defonce with literal → same as def const', () => { + const vars = readNamespaceVars('(defonce counter 0)') + expect(vars[0]).toMatchObject({ name: 'counter', kind: 'const', tsType: 'number' }) + }) + + it('defonce with expression → unknown', () => { + const vars = readNamespaceVars('(defonce state (atom {}))') + expect(vars[0]).toMatchObject({ name: 'state', kind: 'unknown' }) + }) +}) + +// --------------------------------------------------------------------------- +// declare +// --------------------------------------------------------------------------- + +describe('readNamespaceVars — declare', () => { + it('declare produces unknown var', () => { + const vars = readNamespaceVars('(declare forward-ref)') + expect(vars[0]).toMatchObject({ name: 'forward-ref', kind: 'unknown', isPrivate: false, isMacro: false }) + expect(vars[0].arities).toBeUndefined() + expect(vars[0].tsType).toBeUndefined() + }) +}) + +// --------------------------------------------------------------------------- +// Skipped forms +// --------------------------------------------------------------------------- + +describe('readNamespaceVars — skipped forms', () => { + it('ns form is skipped', () => { + const vars = readNamespaceVars('(ns my.app (:require [clojure.string :as str]))') + expect(vars).toHaveLength(0) + }) + + it('non-def top-level call is skipped', () => { + const vars = readNamespaceVars('(println "hello")') + expect(vars).toHaveLength(0) + }) + + it('plain symbol is skipped', () => { + const vars = readNamespaceVars('foo') + expect(vars).toHaveLength(0) + }) +}) + +// --------------------------------------------------------------------------- +// Multiple top-level forms +// --------------------------------------------------------------------------- + +describe('readNamespaceVars — multiple forms', () => { + it('extracts vars from a full realistic namespace', () => { + const source = ` +(ns my.app + (:require [clojure.string :as str])) + +(def version "1.0.0") + +(defn- internal-helper [x] x) + +(defn greet + "Returns a greeting string." + [name] + (str "Hello, " name "!")) + +(defn add + ([a] (add a 0)) + ([a b] (+ a b))) + +(defmacro when-debug [& body] nil) + +(declare lazy-var) +` + const vars = readNamespaceVars(source) + expect(vars.map(v => v.name)).toEqual([ + 'version', 'internal-helper', 'greet', 'add', 'when-debug', 'lazy-var' + ]) + + const version = vars.find(v => v.name === 'version')! + expect(version).toMatchObject({ kind: 'const', tsType: 'string', isPrivate: false }) + + const helper = vars.find(v => v.name === 'internal-helper')! + expect(helper).toMatchObject({ kind: 'fn', isPrivate: true, isMacro: false }) + + const greet = vars.find(v => v.name === 'greet')! + expect(greet).toMatchObject({ kind: 'fn', isPrivate: false }) + expect(greet.arities).toHaveLength(1) + expect(greet.arities![0].params).toHaveLength(1) + + const add = vars.find(v => v.name === 'add')! + expect(add).toMatchObject({ kind: 'fn' }) + expect(add.arities).toHaveLength(2) + + const macro = vars.find(v => v.name === 'when-debug')! + expect(macro).toMatchObject({ kind: 'fn', isMacro: true }) + + const lazy = vars.find(v => v.name === 'lazy-var')! + expect(lazy).toMatchObject({ kind: 'unknown' }) + }) + + it('preserves declaration order', () => { + const source = `(def z 1) (def a 2) (def m 3)` + const vars = readNamespaceVars(source) + expect(vars.map(v => v.name)).toEqual(['z', 'a', 'm']) + }) + + it('private and public vars are both returned (caller decides to filter)', () => { + const source = `(defn pub [x] x) (defn- priv [x] x)` + const vars = readNamespaceVars(source) + expect(vars).toHaveLength(2) + expect(vars.find(v => v.name === 'pub')!.isPrivate).toBe(false) + expect(vars.find(v => v.name === 'priv')!.isPrivate).toBe(true) + }) +}) diff --git a/packages/conjure-js/src/vite-plugin-clj/codegen.ts b/packages/conjure-js/src/vite-plugin-clj/codegen.ts index f42d9bf..c85ce52 100644 --- a/packages/conjure-js/src/vite-plugin-clj/codegen.ts +++ b/packages/conjure-js/src/vite-plugin-clj/codegen.ts @@ -1,10 +1,8 @@ -import { isMacro } from '../core/assertions' -import type { Session } from '../core/session' -import type { Arity, CljValue } from '../core/types' -import { extractNsName, extractNsRequires } from './namespace-utils' +import type { Arity, DestructurePattern } from '../core/types' +import { extractNsName, extractNsRequires, extractStringRequires } from './namespace-utils' +import { readNamespaceVars } from './static-analysis' export interface CodegenContext { - session: Session sourceRoots: string[] coreIndexPath: string virtualSessionId: string @@ -14,11 +12,13 @@ export interface CodegenContext { export function generateModuleCode( ctx: CodegenContext, nsNameFromPath: string, - source: string + source: string, + filePath?: string ): string { const nsName = extractNsName(source) ?? nsNameFromPath - ctx.session.loadFile(source, nsName) + // Detect string requires from AST — determines sync vs async load call. + const hasStringRequires = extractStringRequires(source, filePath).length > 0 const requires = extractNsRequires(source) const depImports = requires @@ -30,44 +30,62 @@ export function generateModuleCode( .filter(Boolean) .join('\n') - const nsData = ctx.session.getNs(nsName) - if (!nsData) { - return `throw new Error('Namespace ${nsName} failed to load');` - } - + // Static analysis: pure AST walk, no execution. + const vars = readNamespaceVars(source) const exportLines: string[] = [] - for (const [name, v] of nsData.vars) { - const value = v.value - if (isMacro(value)) continue - const safeName = safeJsIdentifier(name) + for (const descriptor of vars) { + if (descriptor.isMacro) continue + if (descriptor.isPrivate) continue + + const safeName = safeJsIdentifier(descriptor.name) // At runtime, vars.get() returns a CljVar; deref with .value - const deref = `__ns.vars.get(${JSON.stringify(name)}).value` - if (isAFunction(value)) { + const deref = `__ns.vars.get(${JSON.stringify(descriptor.name)}).value` + + if (descriptor.kind === 'fn') { exportLines.push( `export function ${safeName}(...args) {` + ` const fn = ${deref};` + ` const cljArgs = args.map(jsToClj);` + - ` const result = applyFunction(fn, cljArgs);` + - ` return cljToJs(result);` + + ` const result = __session.applyFunction(fn, cljArgs);` + + ` return cljToJs(result, __session);` + `}` ) } else { exportLines.push( - `export const ${safeName} = cljToJs(${deref});` + `export const ${safeName} = cljToJs(${deref}, __session);` ) } } const escapedSource = JSON.stringify(source) + // Files with string requires need async loading (top-level await, requires target: esnext). + // Files without string requires use the sync path — no top-level await overhead. + const loadCall = hasStringRequires + ? `await __session.loadFileAsync(${escapedSource}, ${JSON.stringify(nsName)});` + : `__session.loadFile(${escapedSource}, ${JSON.stringify(nsName)});` + + if (exportLines.length === 0) { + // No public exports — emit a minimal module that loads the namespace at runtime. + // Namespace will be available in the session even without JS-side exports. + return [ + `import { getSession } from ${JSON.stringify(ctx.virtualSessionId)};`, + depImports, + ``, + `const __session = getSession();`, + loadCall, + ``, + `if (import.meta.hot) { import.meta.hot.accept() }`, + ].join('\n') + } return [ `import { getSession } from ${JSON.stringify(ctx.virtualSessionId)};`, - `import { cljToJs, jsToClj, applyFunction } from ${JSON.stringify(ctx.coreIndexPath)};`, + `import { cljToJs, jsToClj } from ${JSON.stringify(ctx.coreIndexPath)};`, depImports, ``, `const __session = getSession();`, - `__session.loadFile(${escapedSource}, ${JSON.stringify(nsName)});`, + loadCall, `const __ns = __session.getNs(${JSON.stringify(nsName)});`, ``, ...exportLines, @@ -78,47 +96,54 @@ export function generateModuleCode( ].join('\n') } -function isAFunction(value: CljValue): boolean { - return value.kind === 'function' || value.kind === 'native-function' -} +export function generateDts( + _ctx: CodegenContext, + nsNameFromPath: string, + source: string +): string { + const nsName = extractNsName(source) ?? nsNameFromPath + const vars = readNamespaceVars(source) -function cljValueToTsType(value: CljValue): string { - switch (value.kind) { - case 'number': - return 'number' - case 'string': - return 'string' - case 'boolean': - return 'boolean' - case 'nil': - return 'null' - case 'keyword': - return 'string' - case 'symbol': - return 'string' - case 'list': - case 'vector': - return 'unknown[]' - case 'map': - return 'Record' - case 'function': - case 'native-function': - return '(...args: unknown[]) => unknown' - case 'macro': - return 'never' - case 'var': - return 'unknown' - default: - throw new Error(`Unknown CljValue kind: ${value.kind}`) + const declarations: string[] = [] + for (const descriptor of vars) { + if (descriptor.isMacro) continue + if (descriptor.isPrivate) continue + + const safeName = safeJsIdentifier(descriptor.name) + + if (descriptor.kind === 'fn') { + if (descriptor.arities && descriptor.arities.length > 0) { + for (const arity of descriptor.arities) { + declarations.push(`export function ${safeName}${arityToSignature(arity)};`) + } + } else { + declarations.push(`export function ${safeName}(...args: unknown[]): unknown;`) + } + } else { + // 'const' with inferred type, or 'unknown' + const tsType = descriptor.tsType ?? 'unknown' + declarations.push(`export const ${safeName}: ${tsType};`) + } } + + // Suppress the unused-variable warning — nsName is used for documentation only here + void nsName + + return declarations.join('\n') } +// --------------------------------------------------------------------------- +// Signature helpers +// --------------------------------------------------------------------------- + +type ArityShape = { params: DestructurePattern[]; restParam: DestructurePattern | null } + function patternName(p: Arity['params'][number], index: number): string { if (p.kind === 'symbol') return safeJsIdentifier(p.name) return `arg${index}` } -function arityToSignature(arity: Arity): string { +function arityToSignature(arity: ArityShape): string { const fixedParams = arity.params .map((p, i) => `${patternName(p, i)}: unknown`) .join(', ') @@ -137,46 +162,9 @@ function arityToSignature(arity: Arity): string { return `(${fixedParams}): unknown` } -export function generateDts( - ctx: CodegenContext, - nsNameFromPath: string, - source: string -): string { - const nsName = extractNsName(source) ?? nsNameFromPath - - try { - ctx.session.loadFile(source, nsName) - } catch { - return '' - } - - const nsData = ctx.session.getNs(nsName) - if (!nsData) return '' - - const declarations: string[] = [] - for (const [name, v] of nsData.vars) { - const value = v.value - if (isMacro(value)) continue - - const safeName = safeJsIdentifier(name) - - if (value.kind === 'function') { - for (const arity of value.arities) { - declarations.push( - `export function ${safeName}${arityToSignature(arity)};` - ) - } - } else if (value.kind === 'native-function') { - declarations.push( - `export function ${safeName}(...args: unknown[]): unknown;` - ) - } else { - declarations.push(`export const ${safeName}: ${cljValueToTsType(value)};`) - } - } - - return declarations.join('\n') -} +// --------------------------------------------------------------------------- +// Identifier sanitization +// --------------------------------------------------------------------------- const JS_RESERVED_WORDS = new Set([ 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', diff --git a/packages/conjure-js/src/vite-plugin-clj/index.ts b/packages/conjure-js/src/vite-plugin-clj/index.ts index 66c7a52..63ac982 100644 --- a/packages/conjure-js/src/vite-plugin-clj/index.ts +++ b/packages/conjure-js/src/vite-plugin-clj/index.ts @@ -1,18 +1,31 @@ import { execFileSync } from 'node:child_process' import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs' import { resolve, relative, join, dirname } from 'node:path' -import { fileURLToPath } from 'node:url' +import { fileURLToPath, pathToFileURL } from 'node:url' +import { createRequire } from 'node:module' import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite' import { createSession } from '../core/session' import type { Session } from '../core/session' -import { nsToPath, pathToNs } from './namespace-utils' +import { nsToPath, pathToNs, extractStringRequires } from './namespace-utils' import { generateModuleCode, generateDts, safeJsIdentifier } from './codegen' import type { CodegenContext } from './codegen' -import { startBrowserNreplRelay } from './nrepl-relay' +import { startBrowserNreplRelay } from '../nrepl/relay' interface CljPluginOptions { sourceRoots?: string[] nreplPort?: number + /** + * Path to a user-defined session factory (relative to project root). + * The factory must be the default export with signature: + * (importMap: Record) => SessionOptions | null | undefined + * + * The plugin automatically injects `importModule` (wired to the import map) and + * `output` (wired to nREPL output capture + console.log). Do NOT provide these. + * Return only what you need: hostBindings, modules, entries, stderr, etc. + * + * Example: entrypoint: 'src/conjure.ts' + */ + entrypoint?: string } // Resolve the conjure-js core index path regardless of whether this plugin @@ -42,6 +55,12 @@ export function cljPlugin(options?: CljPluginOptions): Plugin { let codegenCtx: CodegenContext let generatorScriptPath: string let serveMode = false + // Collected during configResolved: original CLJ source string → resolved path/package name. + // Original string = what the CLJ runtime passes to importModule(s). + // Resolved = what Vite should actually import (absolute path for relative, unchanged for pkgs). + let stringRequires: Array<{ original: string; resolved: string }> = [] + // Resolved absolute path to the user-defined session entrypoint (Mode 2), or null (Mode 1). + let entrypointPath: string | null = null function writeFileIfChanged(path: string, content: string) { try { @@ -88,7 +107,7 @@ export function cljPlugin(options?: CljPluginOptions): Plugin { const source = readFileSync(filePath, 'utf-8') const nsNameFromPath = pathToNs(relative(projectRoot, filePath), sourceRoots) const dts = generateDts(codegenCtx, nsNameFromPath, source) - writeFileIfChanged(filePath + '.d.ts', dts) + if (dts) writeFileIfChanged(filePath + '.d.ts', dts) } catch { continue } @@ -97,14 +116,45 @@ export function cljPlugin(options?: CljPluginOptions): Plugin { } function initServerSession() { + // Use a require resolver anchored to the project root so that the server session + // can import npm packages from the consuming project's node_modules (e.g. date-fns), + // not just from the plugin's own node_modules. + const projectRequire = createRequire(resolve(projectRoot, 'package.json')) + serverSession = createSession({ sourceRoots, readFile: (filePath: string) => readFileSync(resolve(projectRoot, filePath), 'utf-8'), output: () => {}, + // Node dynamic import — used only during server-side code generation (DTS inference). + // In the browser bundle, importModule is a synchronous import map lookup instead. + importModule: async (s: string) => { + if (!s.startsWith('.') && !s.startsWith('/')) { + // Package import: resolve from project root context so the consuming project's + // node_modules are searched instead of (or in addition to) the plugin's. + try { + const resolved = projectRequire.resolve(s) + return import(pathToFileURL(resolved).href) + } catch { + try { + return await import(s) + } catch { + // Package not importable in Node context (browser-only) — return stub + // so that namespace loading succeeds and codegen can infer exported vars. + return {} + } + } + } + // Local/absolute path: may be a browser-only file (e.g. local .ts with DOM deps). + // Return a stub so the CLJ namespace still loads for server-side var inference. + try { + return await import(s) + } catch { + return {} + } + }, }) codegenCtx = { - session: serverSession, sourceRoots, coreIndexPath, virtualSessionId: VIRTUAL_SESSION_ID, @@ -123,6 +173,32 @@ export function cljPlugin(options?: CljPluginOptions): Plugin { } } + function scanStringRequires() { + // original string (what CLJ runtime passes to importModule) → resolved path/pkg + const seen = new Map() + for (const root of sourceRoots) { + const rootPath = resolve(projectRoot, root) + for (const filePath of collectCljFiles(rootPath)) { + try { + const source = readFileSync(filePath, 'utf-8') + // Call twice: without filePath to get original CLJ source strings, + // with filePath to get resolved absolute paths for relative specifiers. + const originals = extractStringRequires(source) + const resolved = extractStringRequires(source, filePath) + for (let i = 0; i < originals.length; i++) { + seen.set(originals[i], resolved[i]) + } + } catch { + continue + } + } + } + stringRequires = [...seen.entries()].map(([original, resolved]) => ({ + original, + resolved, + })) + } + function regenerateBuiltInNamespaceSources() { try { statSync(generatorScriptPath) @@ -142,6 +218,23 @@ export function cljPlugin(options?: CljPluginOptions): Plugin { } } + /** + * Generate the static import table lines for the virtual session module. + * Each specifier gets a unique variable name. Returns { importLines, mapEntries }. + */ + function buildImportTable(): { importLines: string[]; mapEntries: string[] } { + const importLines: string[] = [] + const mapEntries: string[] = [] + stringRequires.forEach(({ original, resolved }, i) => { + const varName = `_imp_${i}` + // Import statement uses the resolved path (absolute for local files, pkg name for packages). + // Map key uses the original CLJ source string — this is what importModule(s) receives at runtime. + importLines.push(`import * as ${varName} from ${JSON.stringify(resolved)};`) + mapEntries.push(` ${JSON.stringify(original)}: ${varName},`) + }) + return { importLines, mapEntries } + } + return { name: 'vite-plugin-clj', @@ -152,6 +245,22 @@ export function cljPlugin(options?: CljPluginOptions): Plugin { regenerateBuiltInNamespaceSources() coreIndexPath = resolveCoreIndexPath() initServerSession() + + // Detect Mode 2: explicit user entrypoint + if (options?.entrypoint) { + const ep = resolve(projectRoot, options.entrypoint) + try { + statSync(ep) + entrypointPath = ep + } catch { + // Configured entrypoint doesn't exist — fall through to Mode 1 with a warning + console.warn( + `[vite-plugin-clj] entrypoint not found: ${options.entrypoint} — falling back to auto-generated session` + ) + } + } + + scanStringRequires() eagerlyGenerateDts() }, @@ -176,30 +285,67 @@ export function cljPlugin(options?: CljPluginOptions): Plugin { load(id: string) { if (id === RESOLVED_VIRTUAL_SESSION_ID) { - const lines = [ + const { importLines, mapEntries } = buildImportTable() + + // All static imports must be at the top of the module (ESM hoist semantics) + const lines: string[] = [ `import { createSession, printString } from ${JSON.stringify(coreIndexPath)};`, + ...importLines, + ...(entrypointPath + ? [`import __conjureFactory from ${JSON.stringify(entrypointPath)};`] + : []), + ``, + `const __importMap = {`, + ...mapEntries, + `};`, ``, `let _session = null;`, `let _outputLines = [];`, - `export function getSession() {`, - ` if (!_session) {`, - ` _session = createSession({ output: (text) => { _outputLines.push(text); console.log(text.replace(/\\n$/, '')); } });`, - ` }`, - ` return _session;`, - `}`, ] + if (entrypointPath) { + // Mode 2: user-defined factory returns SessionOptions (without output/importModule). + // The plugin owns output capture (for nREPL relay) and importModule (import map wiring). + // User factory controls hostBindings, modules, entries, etc. + lines.push( + `export function getSession() {`, + ` if (!_session) {`, + ` const __userOpts = __conjureFactory(__importMap) ?? {};`, + ` _session = createSession({`, + ` ...(__userOpts),`, + ` importModule: (s) => __importMap[s],`, + ` output: (text) => { _outputLines.push(text); console.log(text.replace(/\\n$/, '')); },`, + ` });`, + ` }`, + ` return _session;`, + `}`, + ) + } else { + // Mode 1: auto-generate session with import map wired in + lines.push( + `export function getSession() {`, + ` if (!_session) {`, + ` _session = createSession({`, + ` importModule: (s) => __importMap[s],`, + ` output: (text) => { _outputLines.push(text); console.log(text.replace(/\\n$/, '')); },`, + ` });`, + ` }`, + ` return _session;`, + `}`, + ) + } + if (serveMode) { lines.push( ``, `// Browser nREPL relay — active only in Vite dev server`, `if (import.meta.hot) {`, - ` import.meta.hot.on('conjure:eval', ({ id, code, ns }) => {`, + ` import.meta.hot.on('conjure:eval', async ({ id, code, ns }) => {`, ` const session = getSession();`, ` _outputLines = [];`, ` try {`, ` if (ns && ns !== session.currentNs) session.setNs(ns);`, - ` const result = session.evaluate(code);`, + ` const result = await session.evaluateAsync(code);`, ` const out = _outputLines.join('');`, ` import.meta.hot.send('conjure:eval-result', { id, value: printString(result), ns: session.currentNs, ...(out ? { out } : {}) });`, ` } catch (err) {`, @@ -209,11 +355,11 @@ export function cljPlugin(options?: CljPluginOptions): Plugin { ` }`, ` });`, ``, - ` import.meta.hot.on('conjure:load-file', ({ id, source, nsHint, filePath }) => {`, + ` import.meta.hot.on('conjure:load-file', async ({ id, source, nsHint, filePath }) => {`, ` const session = getSession();`, ` _outputLines = [];`, ` try {`, - ` const loadedNs = session.loadFile(source, nsHint, filePath || undefined);`, + ` const loadedNs = await session.loadFileAsync(source, nsHint, filePath || undefined);`, ` if (loadedNs) session.setNs(loadedNs);`, ` const out = _outputLines.join('');`, ` import.meta.hot.send('conjure:load-file-result', { id, value: 'nil', ns: session.currentNs, ...(out ? { out } : {}) });`, @@ -233,9 +379,9 @@ export function cljPlugin(options?: CljPluginOptions): Plugin { if (id.endsWith('.clj') && !id.includes('?')) { const source = readFileSync(id, 'utf-8') const nsNameFromPath = pathToNs(relative(projectRoot, id), sourceRoots) - const code = generateModuleCode(codegenCtx, nsNameFromPath, source) + const code = generateModuleCode(codegenCtx, nsNameFromPath, source, id) const dts = generateDts(codegenCtx, nsNameFromPath, source) - writeFileIfChanged(id + '.d.ts', dts) + if (dts) writeFileIfChanged(id + '.d.ts', dts) return code } }, @@ -250,7 +396,7 @@ export function cljPlugin(options?: CljPluginOptions): Plugin { const source = await read() try { const nsNameFromPath = pathToNs(relative(projectRoot, file), sourceRoots) - serverSession.loadFile(source, nsNameFromPath) + await serverSession.loadFileAsync(source, nsNameFromPath) const dts = generateDts(codegenCtx, nsNameFromPath, source) writeFileIfChanged(file + '.d.ts', dts) } catch { diff --git a/packages/conjure-js/src/vite-plugin-clj/namespace-utils.ts b/packages/conjure-js/src/vite-plugin-clj/namespace-utils.ts index 375a567..ee2d724 100644 --- a/packages/conjure-js/src/vite-plugin-clj/namespace-utils.ts +++ b/packages/conjure-js/src/vite-plugin-clj/namespace-utils.ts @@ -1,3 +1,4 @@ +import { resolve, dirname } from 'node:path' import { isKeyword, isList, isSymbol, isVector } from '../core/assertions' import { readForms } from '../core/reader' import { tokenize } from '../core/tokenizer' @@ -63,6 +64,44 @@ export function extractNsRequires(source: string): string[] { return requires } +/** + * Parse Clojure source and extract all string module specifiers from (:require ...) clauses. + * These are the JS/npm imports written as string literals: (:require ["react" :as React]). + * Returns an array of resolved specifier strings (deduplicated). + * + * If filePath is provided, relative specifiers (starting with ./ or ../) are resolved + * to absolute paths using the file's directory. Package specifiers are returned as-is. + */ +export function extractStringRequires(source: string, filePath?: string): string[] { + const forms = readForms(tokenize(source)) + const nsForm = forms.find( + (f) => isList(f) && isSymbol(f.value[0]) && f.value[0].name === 'ns' + ) + if (!nsForm || !isList(nsForm)) return [] + + const specifiers: string[] = [] + for (let i = 2; i < nsForm.value.length; i++) { + const clause = nsForm.value[i] + if ( + isList(clause) && + isKeyword(clause.value[0]) && + clause.value[0].name === ':require' + ) { + for (let j = 1; j < clause.value.length; j++) { + const spec = clause.value[j] + const first = isVector(spec) && spec.value.length > 0 ? spec.value[0] : null + if (!first || first.kind !== 'string') continue + let specifier = first.value + if (filePath && (specifier.startsWith('./') || specifier.startsWith('../'))) { + specifier = resolve(dirname(filePath), specifier) + } + specifiers.push(specifier) + } + } + } + return [...new Set(specifiers)] +} + /** * Extract the namespace name from the (ns ...) form in a Clojure source string. * Returns null if no ns form is found. diff --git a/packages/conjure-js/src/vite-plugin-clj/static-analysis.ts b/packages/conjure-js/src/vite-plugin-clj/static-analysis.ts new file mode 100644 index 0000000..010cb48 --- /dev/null +++ b/packages/conjure-js/src/vite-plugin-clj/static-analysis.ts @@ -0,0 +1,211 @@ +import { isList, isSymbol, isVector } from '../core/assertions' +import { readForms } from '../core/reader' +import { tokenize } from '../core/tokenizer' +import type { Arity, CljList, CljMap, CljValue, DestructurePattern } from '../core/types' + +// --------------------------------------------------------------------------- +// Public types +// --------------------------------------------------------------------------- + +export interface VarDescriptor { + name: string + kind: 'fn' | 'const' | 'unknown' + arities?: Arity[] // present when kind === 'fn' + tsType?: string // present when kind === 'const' and value type is inferrable + isPrivate: boolean + isMacro: boolean +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** + * Parse Clojure source and return descriptors for all top-level var definitions. + * Pure function — no session, no execution, no side effects. + * + * Handles: defn, defn-, defmacro, def, defonce, declare. + * Skips: ns, and any other top-level form that is not a definition. + * + * Private vars (defn-) are included with isPrivate: true so callers can decide + * whether to export them or not. + */ +export function readNamespaceVars(source: string): VarDescriptor[] { + const forms = readForms(tokenize(source)) + const descriptors: VarDescriptor[] = [] + + for (const form of forms) { + if (!isList(form) || form.value.length < 2) continue + const head = form.value[0] + if (!isSymbol(head)) continue + + const descriptor = parseTopLevelDef(form, head.name) + if (descriptor) descriptors.push(descriptor) + } + + return descriptors +} + +// --------------------------------------------------------------------------- +// Metadata helpers +// --------------------------------------------------------------------------- + +function hasPrivateMeta(meta: CljMap | undefined): boolean { + return (meta?.entries ?? []).some( + ([k, val]) => + k.kind === 'keyword' && k.name === ':private' && + val.kind === 'boolean' && val.value === true + ) +} + +// --------------------------------------------------------------------------- +// Per-form dispatch +// --------------------------------------------------------------------------- + +function parseTopLevelDef(form: CljList, op: string): VarDescriptor | null { + switch (op) { + case 'defn': + return parseDefn(form, false, false) + case 'defn-': + return parseDefn(form, true, false) + case 'defmacro': + return parseDefn(form, false, true) + case 'def': + case 'defonce': + return parseDef(form) + case 'declare': + return parseDeclare(form) + default: + return null + } +} + +// --------------------------------------------------------------------------- +// defn / defn- / defmacro +// --------------------------------------------------------------------------- + +function parseDefn(form: CljList, isPrivate: boolean, isMacro: boolean): VarDescriptor | null { + const nameSym = form.value[1] + if (!isSymbol(nameSym)) return null + + const private_ = isPrivate || hasPrivateMeta(nameSym.meta) + + // Elements after the name: optional docstring, then params or arity clauses + const rest = form.value.slice(2) + // Skip optional docstring + const start = rest.length > 0 && rest[0].kind === 'string' ? 1 : 0 + const bodyForms = rest.slice(start) + + if (bodyForms.length === 0) { + // Bare defn with no param list — treat as unknown function + return { name: nameSym.name, kind: 'fn', arities: [], isPrivate: private_, isMacro } + } + + const arities: Arity[] = isList(bodyForms[0]) + ? // Multi-arity: each clause is a list whose first element is the params vector + bodyForms.filter(isList).map(parseArityClause) + : // Single-arity: bodyForms[0] is the params vector + isVector(bodyForms[0]) ? [vectorToArity(bodyForms[0])] : [] + + return { name: nameSym.name, kind: 'fn', arities, isPrivate: private_, isMacro } +} + +function parseArityClause(clause: CljList): Arity { + const paramVec = clause.value[0] + return isVector(paramVec) ? vectorToArity(paramVec) : { params: [], restParam: null, body: [] } +} + +function vectorToArity(paramVec: { value: CljValue[] }): Arity { + const params: DestructurePattern[] = [] + let restParam: DestructurePattern | null = null + + for (let i = 0; i < paramVec.value.length; i++) { + const p = paramVec.value[i] + if (isSymbol(p) && p.name === '&') { + const next = paramVec.value[i + 1] + if (next) restParam = next as DestructurePattern + break + } + params.push(p as DestructurePattern) + } + + return { params, restParam, body: [] } +} + +// --------------------------------------------------------------------------- +// def / defonce +// --------------------------------------------------------------------------- + +function parseDef(form: CljList): VarDescriptor | null { + const nameSym = form.value[1] + if (!isSymbol(nameSym)) return null + + const isPrivate = hasPrivateMeta(nameSym.meta) + const value = form.value[2] // may be undefined for bare (def name) + + if (!value) { + return { name: nameSym.name, kind: 'unknown', isPrivate, isMacro: false } + } + + // Inline function literal + const fnArities = tryExtractFnArities(value) + if (fnArities !== null) { + return { name: nameSym.name, kind: 'fn', arities: fnArities, isPrivate, isMacro: false } + } + + // Literal values with inferrable TypeScript types + const tsType = inferLiteralTsType(value) + if (tsType !== null) { + return { name: nameSym.name, kind: 'const', tsType, isPrivate, isMacro: false } + } + + return { name: nameSym.name, kind: 'unknown', isPrivate, isMacro: false } +} + +function inferLiteralTsType(value: CljValue): string | null { + switch (value.kind) { + case 'number': return 'number' + case 'string': return 'string' + case 'boolean': return 'boolean' + case 'nil': return 'null' + case 'keyword': return 'string' + case 'vector': + case 'set': return 'unknown[]' + case 'map': return 'Record' + default: return null + } +} + +// --------------------------------------------------------------------------- +// declare +// --------------------------------------------------------------------------- + +function parseDeclare(form: CljList): VarDescriptor | null { + const nameSym = form.value[1] + if (!isSymbol(nameSym)) return null + return { name: nameSym.name, kind: 'unknown', isPrivate: false, isMacro: false } +} + +// --------------------------------------------------------------------------- +// (fn ...) extraction for def with inline function +// --------------------------------------------------------------------------- + +function tryExtractFnArities(value: CljValue): Arity[] | null { + if (!isList(value)) return null + const head = value.value[0] + if (!isSymbol(head) || head.name !== 'fn') return null + + // After fn: optional name symbol, then params-vector or arity-clauses + let rest = value.value.slice(1) + if (rest.length > 0 && isSymbol(rest[0])) rest = rest.slice(1) // skip optional fn name + + if (rest.length === 0) return [] + + if (isVector(rest[0])) { + // Single arity: (fn [params] body) + return [vectorToArity(rest[0])] + } + + // Multi arity: (fn ([params] body) ([params] body)) + return rest.filter(isList).map(parseArityClause) +}