From 1da6fce1ab4a80ffd5c6c41134d20490458aa2fc Mon Sep 17 00:00:00 2001 From: Yogthos Date: Fri, 5 Jun 2026 18:14:29 -0400 Subject: [PATCH] ci: add GitHub Actions test workflow + document eval pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .github/workflows/tests.yml: build Janet (cached) + jpm, init vendor/sci submodule, run jpm test on every push and PR. - README: explain the per-form eval router — interpreted (default) vs compiled (:compile?), the always-interpret carve-out, and shared context. --- .github/workflows/tests.yml | 47 +++++++++++++++++++++++++++++++++++++ README.md | 42 ++++++++++++++++++++++++++++++--- 2 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..c5ad2e9 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,47 @@ +name: tests + +# Run the full test suite (jpm test) on every push and pull request. +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + env: + JANET_VERSION: v1.41.2 # bump to match the version Jolt is developed against + steps: + - uses: actions/checkout@v4 + with: + # vendor/sci is needed by the SCI bootstrap/runtime integration tests. + submodules: recursive + + - name: Cache Janet build + id: cache-janet + uses: actions/cache@v4 + with: + path: /tmp/janet + key: janet-${{ env.JANET_VERSION }}-${{ runner.os }} + + - name: Build Janet + if: steps.cache-janet.outputs.cache-hit != 'true' + run: | + git clone --depth 1 --branch "$JANET_VERSION" https://github.com/janet-lang/janet.git /tmp/janet + make -C /tmp/janet + + - name: Install Janet + run: sudo make -C /tmp/janet install + + - name: Install jpm + run: | + git clone --depth 1 https://github.com/janet-lang/jpm.git /tmp/jpm + sudo janet /tmp/jpm/bootstrap.janet + + - name: Janet version + run: janet -v + + - name: Build executable + run: jpm build + + - name: Run tests + run: jpm test diff --git a/README.md b/README.md index f649ca8..362ad03 100644 --- a/README.md +++ b/README.md @@ -51,9 +51,36 @@ hello 42 `(init)` returns a context with `clojure.core` loaded. Each context is isolated; use separate contexts for separate environments. -### Compilation +### Evaluation pipeline: interpreted and compiled -By default Jolt tree-walks the interpreter. Passing `:compile?` compiles each form to Janet — `def`/`defn` persist in a per-context Janet environment and resolve across forms, hot numeric primitives (`+ - * < > <= >=`) emit native Janet ops, and function calls compile to direct calls (keyword/map/set still dispatch as IFn). For compute-heavy code this is dramatically faster — recursive `fib(30)` runs in ~0.08 s compiled vs ~50 s interpreted (≈600×), at native Janet speed: +Every form Jolt evaluates passes through one router (`eval-one`), which decides +*per form* whether to tree-walk it or compile it to Janet. There are two modes: + +**Interpreted (default).** Without `:compile?`, every form is evaluated by the +tree-walking interpreter (`eval-form`). This is the live, fully-featured path: +all of Clojure's semantics — macros, multimethods, protocols, dynamic vars, +lazy seqs, destructuring — go through here. + +**Compiled (`:compile? true`).** With compilation enabled, the router splits each +top-level form two ways: + +- **Context-modifying forms always interpret.** `ns`, `defmacro`, `deftype`, + `defmulti`/`defmethod`, `require`, `in-ns`, `set!`, `var`, `.`, `new`, `eval`, + and syntax-quote mutate the evaluation context (namespaces, the macro table, + type/method registries, dynamic vars), so they are routed to the interpreter + unchanged. +- **Everything else compiles to Janet.** The form is macro-expanded, lowered to + a Janet AST, and `eval`'d in a **per-context Janet environment**. `def`/`defn` + bindings live in that environment so they persist and resolve across forms + (and self-recurse via a named-fn rewrite); hot numeric primitives + (`+ - * < > <= >=`) emit native Janet ops so the JIT-free Janet VM runs them at + full speed; and function calls compile to direct Janet calls (keyword/map/set + in call position still dispatch through the IFn runtime). + +The two paths **share one context.** Compiled `def`/`defn` results are both +evaluated into the Janet environment *and* interned into the Jolt namespace, so +an interpreted form can call a compiled function and vice-versa within the same +context — which is what makes the always-interpret carve-out above safe. ```janet (def ctx (init {:compile? true})) @@ -61,7 +88,16 @@ By default Jolt tree-walks the interpreter. Passing `:compile?` compiles each fo (eval-string ctx "(fib 30)") ; → 832040, fast ``` -Compile mode is opt-in and still maturing: context-modifying forms (`ns`/`defmacro`/`deftype`/multimethods/…) always interpret, and the numeric-op inlining relaxes the strict non-number checks (e.g. `(< nil 1)`). Constructs the compiler doesn't yet handle fall back to errors rather than the interpreter (a hybrid fallback is planned). +For compute-heavy code the compiled path is dramatically faster — recursive +`fib(30)` runs in ~0.08 s compiled vs ~50 s interpreted (≈600×), at native Janet +speed. + +Compile mode is opt-in and still maturing. The numeric-op inlining relaxes the +strict non-number checks (e.g. `(< nil 1)` doesn't throw), and constructs the +compiler doesn't yet handle currently **error** rather than transparently +falling back to the interpreter — a per-form hybrid fallback (compile what we +can, interpret the rest) is the next step toward making compilation safe to +turn on by default. ## Host interop