Compare commits

..

No commits in common. "main" and "release/windows-v011" have entirely different histories.

30 changed files with 626 additions and 1438 deletions

View file

@ -96,15 +96,11 @@ jobs:
with: with:
msystem: MINGW64 msystem: MINGW64
update: false update: false
# inherit the runner PATH so GITHUB_PATH additions (the chez wrapper
# dir) are visible inside the msys2 shell
path-type: inherit
install: >- install: >-
git make vim unzip zip git make vim unzip zip
mingw-w64-x86_64-gcc mingw-w64-x86_64-gcc
mingw-w64-x86_64-lz4 mingw-w64-x86_64-lz4
mingw-w64-x86_64-zlib mingw-w64-x86_64-zlib
mingw-w64-x86_64-ntldd
- name: Cache Chez Scheme (Windows) - name: Cache Chez Scheme (Windows)
if: runner.os == 'Windows' if: runner.os == 'Windows'
@ -119,32 +115,25 @@ jobs:
run: | run: |
git clone --depth 1 --branch v10.4.1 https://github.com/cisco/ChezScheme.git /tmp/chez-src git clone --depth 1 --branch v10.4.1 https://github.com/cisco/ChezScheme.git /tmp/chez-src
cd /tmp/chez-src cd /tmp/chez-src
./configure --threads ./configure --threads --installprefix="$GITHUB_WORKSPACE/chez-install"
make -j"$(nproc)" make -j"$(nproc)"
# `make install` drives the unix installsh through cmd and dies; the make install
# build tree has everything — assemble the layout by hand. Boot files
# sit next to scheme.exe (that's where the Windows kernel looks).
inst="$GITHUB_WORKSPACE/chez-install"
mkdir -p "$inst/bin" "$inst/csv"
cp ta6nt/bin/ta6nt/*.exe "$inst/bin/"
cp ta6nt/bin/ta6nt/*.dll "$inst/bin/" 2>/dev/null || true
cp ta6nt/boot/ta6nt/petite.boot ta6nt/boot/ta6nt/scheme.boot "$inst/bin/"
cp ta6nt/boot/ta6nt/petite.boot ta6nt/boot/ta6nt/scheme.boot "$inst/csv/"
cp ta6nt/boot/ta6nt/scheme.h "$inst/csv/"
cp ta6nt/boot/ta6nt/equates.h "$inst/csv/" 2>/dev/null || true
cp ta6nt/boot/ta6nt/libkernel.a "$inst/csv/" || { echo "libkernel.a not found:"; find ta6nt -name "*.a" -o -name "kernel*"; exit 1; }
- name: Put chez on PATH (Windows) - name: Put chez on PATH (Windows)
if: runner.os == 'Windows' if: runner.os == 'Windows'
run: | run: |
bindir="$GITHUB_WORKSPACE/chez-install/bin" printf '#!/bin/sh
{ echo '#!/bin/sh'; echo "exec \"$bindir/scheme.exe\" \"\$@\""; } > "$bindir/chez" exec "%s/chez-install/bin/scheme" "$@"
chmod +x "$bindir/chez" ' "$GITHUB_WORKSPACE" > "$GITHUB_WORKSPACE/chez-install/bin/chez"
echo "$bindir" >> "$GITHUB_PATH" chmod +x "$GITHUB_WORKSPACE/chez-install/bin/chez"
echo "JOLT_CHEZ_CSV=$GITHUB_WORKSPACE/chez-install/csv" >> "$GITHUB_ENV" echo "$GITHUB_WORKSPACE/chez-install/bin" >> "$GITHUB_PATH"
# cc is the build's compiler name; alias it to mingw gcc # cc is the build's compiler name; alias it to mingw gcc
{ echo '#!/bin/sh'; echo 'exec gcc "$@"'; } > "$bindir/cc" printf '#!/bin/sh
chmod +x "$bindir/cc" exec gcc "$@"
' > /usr/local/bin/cc || printf '#!/bin/sh
exec gcc "$@"
' > "$GITHUB_WORKSPACE/chez-install/bin/cc"
chmod +x /usr/local/bin/cc 2>/dev/null || chmod +x "$GITHUB_WORKSPACE/chez-install/bin/cc"
- name: Show Chez version - name: Show Chez version
run: chez --version run: chez --version
@ -154,19 +143,6 @@ jobs:
# dev-machine check — see jolt-8479). `make joltc-release`, not `make joltc`. # dev-machine check — see jolt-8479). `make joltc-release`, not `make joltc`.
- name: Build joltc (release) - name: Build joltc (release)
run: make joltc-release run: make joltc-release
env:
# Bake the release tag into the binary (build-joltc falls back to
# `git describe` when this is empty, e.g. a workflow_dispatch dry run).
JOLT_VERSION: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || '' }}
- name: Inspect the binary (Windows)
if: runner.os == 'Windows'
run: |
set +e
ls -la target/release/
ntldd target/release/joltc.exe 2>&1 | head -20
./target/release/joltc.exe -e '(+ 1 2)'
echo "exit=$?"
# Sanity: the built binary runs (no Chez needed) and self-reports a value. # Sanity: the built binary runs (no Chez needed) and self-reports a value.
- name: Smoke the binary - name: Smoke the binary

View file

@ -4,23 +4,18 @@
# build step. `make test` is the full gate. `make remint` rebuilds the seed after a # build step. `make test` is the full gate. `make remint` rebuilds the seed after a
# source change. # source change.
.PHONY: test ci values corpus unit smoke buildsmoke staticnativesmoke selfhost sci cts certify ffi transient infer wp devirt fieldread numwp fieldnum protoret narrow directlink numeric inline shakesmoke remint joltc joltc-release joltc-debug joltcsmoke submodules .PHONY: test ci values corpus unit smoke buildsmoke staticnativesmoke selfhost sci cts certify ffi transient infer wp devirt fieldread numwp fieldnum protoret narrow directlink numeric inline shakesmoke remint joltc joltc-release joltc-debug joltcsmoke
# Every target needs the vendored submodules; fail with the fix, not a load error.
submodules:
@test -f vendor/irregex/irregex.scm || { \
echo "vendor submodules missing; run: git submodule update --init --recursive"; exit 1; }
# Full gate (dev machine). Includes the self-host byte-fixpoint, which only holds # Full gate (dev machine). Includes the self-host byte-fixpoint, which only holds
# on the same Chez that minted the seed. # on the same Chez that minted the seed.
test: submodules selfhost ci test: selfhost ci
@echo "OK: all gates passed" @echo "OK: all gates passed"
# CI gate: behavior only. The checked-in seed is a minted artifact (like a # CI gate: behavior only. The checked-in seed is a minted artifact (like a
# lockfile) — it RUNS correctly on any Chez, but `selfhost` rebuilds it and a # lockfile) — it RUNS correctly on any Chez, but `selfhost` rebuilds it and a
# different Chez version may emit byte-different (gensym/order) output, so the # different Chez version may emit byte-different (gensym/order) output, so the
# byte-fixpoint is a dev-machine check, not a CI one (jolt-8479). # byte-fixpoint is a dev-machine check, not a CI one (jolt-8479).
ci: submodules values corpus unit smoke buildsmoke staticnativesmoke sci cts ffi transient infer wp devirt fieldread numwp fieldnum protoret narrow directlink numeric inline certify ci: values corpus unit smoke buildsmoke staticnativesmoke sci cts ffi transient infer wp devirt fieldread numwp fieldnum protoret narrow directlink numeric inline certify
@echo "OK: CI gates passed" @echo "OK: CI gates passed"
# Self-host fixpoint: bootstrap.ss rebuild == checked-in seed. # Self-host fixpoint: bootstrap.ss rebuild == checked-in seed.

View file

@ -9,12 +9,8 @@ runs it on Chez. The compiler is self-hosted: it is written in Clojure
## Install ## Install
Grab the self-contained `joltc` binary (Linux/macOS/Windows) — it bundles the Grab the self-contained `joltc` binary (Linux/macOS) — it bundles the runtime,
runtime, compiler, and standard library, so there is nothing else to install. compiler, and standard library, so there is nothing else to install.
Download the binary archive for your platform from the
[releases page](https://github.com/jolt-lang/jolt/releases) (`joltc-<ver>-<platform>.tar.gz`,
or the `.zip` on Windows). The "Source code" archives GitHub attaches to every
release are not binaries — see [Build](#build) before using one.
With Homebrew: With Homebrew:
@ -49,18 +45,6 @@ cd jolt
bin/joltc -e '(+ 1 2)' # => 3 bin/joltc -e '(+ 1 2)' # => 3
``` ```
The `--recurse-submodules` matters: jolt vendors its regex engine and test
suites as git submodules. In a checkout that's missing them (a plain
`git clone`, or after pulling a commit that adds one), fetch them with:
```bash
git submodule update --init --recursive
```
Note that GitHub's auto-generated "Source code (zip/tar.gz)" archives on the
releases page do **not** contain submodules, so they can't run or build —
clone the repo instead (or grab a prebuilt binary from the same page).
After changing a compiler source — the reader (`host/chez/reader.ss`), the After changing a compiler source — the reader (`host/chez/reader.ss`), the
analyzer/IR/backend (`jolt-core/jolt/*.clj`), or the `clojure.core` overlay analyzer/IR/backend (`jolt-core/jolt/*.clj`), or the `clojure.core` overlay
(`jolt-core/clojure/core/*.clj`) — re-mint the seed: (`jolt-core/clojure/core/*.clj`) — re-mint the seed:

View file

@ -14,28 +14,5 @@
# JOLT_PWD. # JOLT_PWD.
root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
export JOLT_PWD="${JOLT_PWD:-$PWD}" export JOLT_PWD="${JOLT_PWD:-$PWD}"
# Identify the Chez Scheme executable
while read -r CHEZ
do
if [ `which ${CHEZ}` ]
then
break;
fi
done <<EOF
chez
chezscheme
EOF
# If we failed to find one, whinge and exit.
if [ ! `which ${CHEZ}` ]
then
echo "No valid Chez Scheme executable found: please install Chez Scheme."
exit 1
fi
# Version for --version / banners: git describe of this checkout, else "dev".
export JOLT_VERSION="${JOLT_VERSION:-$(git -C "$root" describe --tags --always --dirty 2>/dev/null || echo dev)}"
cd "$root" || exit 1 cd "$root" || exit 1
exec ${CHEZ} --script host/chez/cli.ss "$@" exec chez --script host/chez/cli.ss "$@"

View file

@ -182,30 +182,6 @@ a root, transitively.
- Source only; compiled `.class` files in a git dep are ignored. - Source only; compiled `.class` files in a git dep are ignored.
- git `:git/sha` must be a full SHA (`git fetch` can't resolve a short one). - git `:git/sha` must be a full SHA (`git fetch` can't resolve a short one).
## Stack traces
An uncaught error prints the message, the top-level source location, and — when
frames are available — a `trace:` backtrace. In an AOT `jolt build --direct-link`
binary the frames map to `ns/name (file:line)`; on the runtime eval path they are
the surviving fn names. Tail-call optimization erases tail-called frames, so the
default trace shows only the non-tail spine.
A fuller **tail-frame history** recovers the frames TCO erases: each compiled fn
records itself on entry into a bounded ring-of-rings buffer, so the trace shows
TCO-elided frames (including the immediate error site) while a tight tail loop
stays bounded and its non-tail caller context is preserved.
It is **on by default in REPL-driven development** — a `repl` or nREPL session
turns it on, so an error in code you evaluate or reload shows a tail-frame trace
with no setup. Because the recording is baked in at compile time, only code
compiled while a session is live is traced; reload a namespace to trace code that
was already loaded (e.g. an app's initial `-M:run` load before its nREPL started).
Elsewhere it is off (a small per-call cost, and never emitted into a `jolt build`
binary). Override with the environment: `JOLT_TRACE=1` forces it on for a whole
run — including a plain `-M:run`, so the app's own load is traced — and
`JOLT_TRACE=0` forces it off, even in a REPL/nREPL session.
## Conformance ## Conformance
The known-working libraries (see [libraries.md](libraries.md)) and the The known-working libraries (see [libraries.md](libraries.md)) and the

View file

@ -46,15 +46,6 @@
(unless (or jb-release? (string=? jb-profile "debug")) (unless (or jb-release? (string=? jb-profile "debug"))
(error 'build-joltc "profile must be \"release\" or \"debug\"" jb-profile)) (error 'build-joltc "profile must be \"release\" or \"debug\"" jb-profile))
;; Version baked into the binary's saved heap. Prefer $JOLT_VERSION (CI sets it to
;; the release tag); else derive it from git in this checkout; else "dev".
(define jb-version
(let ((env (getenv "JOLT_VERSION")))
(if (and env (> (string-length env) 0))
env
(let ((s (bld-sh-capture "git describe --tags --always --dirty 2>/dev/null")))
(if (> (string-length s) 0) s "dev")))))
(define jb-build (string-append jb-out ".build")) (define jb-build (string-append jb-out ".build"))
(bld-check-toolchain) (bld-check-toolchain)
(bld-system (string-append "mkdir -p '" (path-parent jb-out) "' '" jb-build "'")) (bld-system (string-append "mkdir -p '" (path-parent jb-out) "' '" jb-build "'"))
@ -145,10 +136,11 @@
(scheme-start (scheme-start
(lambda args (lambda args
(set-source-roots! (list \"jolt-core\" \"stdlib\")) (set-source-roots! (list \"jolt-core\" \"stdlib\"))
;; JOLT_TRACE at RUNTIME (the env is unset at heap-build), before any app ns (guard (v (#t (jolt-report-throwable v (current-error-port))
;; compiles, so a `-M:run` traces the app's own code. (let ((bt (jolt-backtrace-string v)))
(jolt-trace-init-from-env!) (when bt (display \" trace:\\n\" (current-error-port))
(guard (v (#t (jolt-report-throwable v (current-error-port)) (exit 1))) (display bt (current-error-port))))
(exit 1)))
(cond (cond
((and (= (length args) 2) (string=? (car args) \"-e\")) ((and (= (length args) 2) (string=? (car args) \"-e\"))
(let ((result (jolt-final-str (let ((result (jolt-final-str
@ -172,10 +164,6 @@
(jb-emit-runtime-embeds out) (jb-emit-runtime-embeds out)
(put-string out "\n;; === embedded jolt-core + stdlib source ===\n") (put-string out "\n;; === embedded jolt-core + stdlib source ===\n")
(jb-emit-source-embeds out) (jb-emit-source-embeds out)
;; Bake the version into the saved heap (runs at heap-build; loader.ss defined
;; jolt-baked-version above, so this set! resolves).
(put-string out (string-append "\n;; === baked version ===\n(set! jolt-baked-version "
(ei-str-lit jb-version) ")\n"))
(put-string out "\n;; === joltc launcher ===\n") (put-string out "\n;; === joltc launcher ===\n")
(jb-emit-launcher out) (jb-emit-launcher out)
(close-port out)) (close-port out))
@ -257,8 +245,6 @@
;; table so `build` can foreign-entry them to spill the bundled Chez boots. On ;; table so `build` can foreign-entry them to spill the bundled Chez boots. On
;; Linux dlsym can't see executable symbols otherwise (macOS exports them anyway). ;; Linux dlsym can't see executable symbols otherwise (macOS exports them anyway).
(bld-system (string-append (bld-system (string-append
;; the embedded jolt_* arrays must be foreign-entry-visible at runtime: "cc -O2 " (if bld-nt? "" "-rdynamic ") "-I'" bld-csv-dir "' -I'" jb-build "' '" jb-main-c "' '"
;; -rdynamic on ELF; on Windows an exe needs an export table (GetProcAddress).
"cc -O2 " (if bld-nt? "-Wl,--export-all-symbols " "-rdynamic ") "-I'" bld-csv-dir "' -I'" jb-build "' '" jb-main-c "' '"
bld-csv-dir "/libkernel.a' -o '" jb-out "' " (bld-link-libs))) bld-csv-dir "/libkernel.a' -o '" jb-out "' " (bld-link-libs)))
(display (string-append "build-joltc: wrote " jb-out "\n")) (display (string-append "build-joltc: wrote " jb-out "\n"))

View file

@ -94,28 +94,6 @@ for frame in 'app.util/deep-boom' 'app.util/mid-boom' 'app.core/-main'; do
exit 1 exit 1
fi fi
done done
# A built binary runs -main with *ns* = user, like clojure.main — so a runtime
# resolve of an aliased symbol is nil (the alias lives in the entry ns, not user),
# matching the JVM and interpreted joltc rather than the entry ns's alias table. A
# separate app: `resolve` defeats tree-shaking, so keep it out of the shake test's
# app above.
nsp="$(dirname "$out")/nsparity"
mkdir -p "$nsp/src/nsp"
printf '{:paths ["src"]}\n' > "$nsp/deps.edn"
printf '(ns nsp.lib)\n(defn thing [] 1)\n' > "$nsp/src/nsp/lib.clj"
printf '(ns nsp.main (:require [nsp.lib :as l]))\n(defn -main [& _]\n (println "ns:" (str *ns*))\n (println "resolve:" (pr-str (resolve (quote l/thing))))\n (println "ns-resolve:" (pr-str (ns-resolve (quote nsp.lib) (quote thing)))))\n' > "$nsp/src/nsp/main.clj"
nspout="$(dirname "$out")/nsparity-bin"
if ! JOLT_PWD="$nsp" bin/joltc build -m nsp.main -o "$nspout" >/dev/null 2>&1; then
echo " FAIL: jolt build of the ns-parity app exited non-zero"; exit 1
fi
nsp_out="$(cd / && "$nspout" 2>&1)"
if ! printf '%s' "$nsp_out" | grep -q 'ns: user' \
|| ! printf '%s' "$nsp_out" | grep -q '^resolve: nil' \
|| ! printf '%s' "$nsp_out" | grep -q "ns-resolve: #'nsp.lib/thing"; then
echo " FAIL: built binary -main ns parity — want 'ns: user', 'resolve: nil', ns-resolve found"
echo "--- got ----"; echo "$nsp_out"
exit 1
fi
# Tree-shaking (opt-in): same result, and an unreachable def (the `twice` macro, # Tree-shaking (opt-in): same result, and an unreachable def (the `twice` macro,
# expanded at AOT and never called at runtime) is dropped. # expanded at AOT and never called at runtime) is dropped.
if ! JOLT_PWD="$app" bin/joltc build -m app.core -o "$out" --tree-shake >/dev/null 2>&1; then if ! JOLT_PWD="$app" bin/joltc build -m app.core -o "$out" --tree-shake >/dev/null 2>&1; then
@ -151,20 +129,4 @@ if [ "$got_dr" != "42" ]; then
echo " FAIL: built #code data reader — want 42, got \`$got_dr\`"; exit 1 echo " FAIL: built #code data reader — want 42, got \`$got_dr\`"; exit 1
fi fi
# A script namespace with no -main (just top-level side effects) must build and echo "build smoke: passed (release + optimized + direct-link + tree-shake + compiler+core shake + data-reader)"
# run its top-level forms, then exit cleanly — not crash calling a nil -main.
nomain="$(dirname "$out")/nomain"
mkdir -p "$nomain/src"
printf '{:paths ["src"]}\n' > "$nomain/deps.edn"
printf '(ns script)\n(println "no-main script ran")\n' > "$nomain/src/script.clj"
nmout="$(dirname "$out")/nomain-bin"
if ! JOLT_PWD="$nomain" bin/joltc build -m script -o "$nmout" >/dev/null 2>&1; then
echo " FAIL: jolt build of a no-main script exited non-zero"; exit 1
fi
got_nm="$(cd / && "$nmout" 2>&1)"; rc_nm=$?
if [ "$got_nm" != "no-main script ran" ] || [ "$rc_nm" != "0" ]; then
echo " FAIL: no-main script binary — want 'no-main script ran' rc 0, got \`$got_nm\` rc $rc_nm"
exit 1
fi
echo "build smoke: passed (release + optimized + direct-link + tree-shake + compiler+core shake + data-reader + no-main)"

View file

@ -23,7 +23,7 @@
;; --- shell helpers ---------------------------------------------------------- ;; --- shell helpers ----------------------------------------------------------
;; Run a command, return its stdout as one trimmed string ("" on no output). ;; Run a command, return its stdout as one trimmed string ("" on no output).
(define (bld-sh-capture cmd) (define (bld-sh-capture cmd)
(let* ((p (process (bld-sh-wrap cmd))) (in (car p))) (let* ((p (process cmd)) (in (car p)))
(let loop ((acc '())) (let loop ((acc '()))
(let ((l (get-line in))) (let ((l (get-line in)))
(if (eof-object? l) (if (eof-object? l)
@ -37,7 +37,7 @@
(loop (cons l acc))))))) (loop (cons l acc)))))))
(define (bld-system cmd) (define (bld-system cmd)
(let ((rc (system (bld-sh-wrap cmd)))) (let ((rc (system cmd)))
(unless (zero? rc) (unless (zero? rc)
(error 'jolt-build (string-append "command failed (" (number->string rc) "): " cmd))))) (error 'jolt-build (string-append "command failed (" (number->string rc) "): " cmd)))))
@ -59,23 +59,6 @@
(define bld-osx? (bld-contains? bld-machine "osx")) (define bld-osx? (bld-contains? bld-machine "osx"))
(define bld-nt? (bld-contains? bld-machine "nt")) (define bld-nt? (bld-contains? bld-machine "nt"))
;; Chez's system/process run through cmd.exe on Windows; every build command
;; here is written for sh (MSYS2 provides it). On nt, spill the command to a
;; script and run `sh <file>` — workspace paths carry no spaces, and the
;; script file sidesteps cmd's quoting entirely. Identity elsewhere.
(define bld-shell-counter 0)
(define (bld-sh-wrap cmd)
(if bld-nt?
(let* ((tmp (or (getenv "TEMP") (getenv "TMP") "."))
(f (begin (set! bld-shell-counter (+ bld-shell-counter 1))
(string-append tmp "\\jolt-sh-"
(number->string bld-shell-counter) ".sh"))))
(let ((p (open-output-file f 'replace)))
(put-string p cmd)
(close-port p))
(string-append "sh " f))
cmd))
;; The Chez executable, for the isolated compile pass (see build-binary step 4). ;; The Chez executable, for the isolated compile pass (see build-binary step 4).
(define bld-chez (define bld-chez
(let ((p (bld-sh-capture "command -v chez || command -v scheme || command -v petite"))) (let ((p (bld-sh-capture "command -v chez || command -v scheme || command -v petite")))
@ -121,9 +104,7 @@
;; Windows (ta6nt, MinGW-w64 under MSYS2): the Chez kernel pulls in ;; Windows (ta6nt, MinGW-w64 under MSYS2): the Chez kernel pulls in
;; compression, winsock, COM/UUID, and the registry. ;; compression, winsock, COM/UUID, and the registry.
(bld-nt? (bld-nt?
;; -static: a single-file exe (no libwinpthread/libgcc/lz4 DLL deps) — "-llz4 -lz -lws2_32 -lrpcrt4 -lole32 -ladvapi32 -luser32 -lshell32 -lm")
;; required for a distributable binary and for TLS init consistency.
"-static -llz4 -lz -lws2_32 -lrpcrt4 -lole32 -luuid -ladvapi32 -luser32 -lshell32 -lm")
;; Linux: the Chez kernel pulls in compression (lz4/z), the expression ;; Linux: the Chez kernel pulls in compression (lz4/z), the expression
;; editor (ncurses + terminfo), threads, dlopen, libuuid, and clock_gettime. ;; editor (ncurses + terminfo), threads, dlopen, libuuid, and clock_gettime.
(else "-llz4 -lz -lncurses -ltinfo -ldl -lm -lpthread -luuid -lrt"))) (else "-llz4 -lz -lncurses -ltinfo -ldl -lm -lpthread -luuid -lrt")))
@ -571,23 +552,11 @@
"))\n" "))\n"
" (list \"jolt-core\" \"stdlib\"))))\n")) " (list \"jolt-core\" \"stdlib\"))))\n"))
(put-string out (string-append (put-string out (string-append
;; Call -main only if the entry namespace defines one; " (let ((mainv (var-deref " (ei-str-lit entry-ns) " \"-main\")))\n"
;; a script ns (top-level side effects, no -main) has
;; already run its forms at heap build, so invoking a nil
;; -main would crash ("nil cannot be cast to IFn") — just
;; exit cleanly instead.
" (let ((maincell (var-cell-lookup " (ei-str-lit entry-ns) " \"-main\")))\n"
;; render an uncaught throw (+ Clojure backtrace) instead ;; render an uncaught throw (+ Clojure backtrace) instead
;; of Chez's opaque dump, then exit non-zero. ;; of Chez's opaque dump, then exit non-zero.
" (guard (v (#t (jolt-report-throwable v (current-error-port)) (exit 1)))\n" " (guard (v (#t (jolt-report-throwable v (current-error-port)) (exit 1)))\n"
;; Loading the app left the current ns at the entry ns; reset " (apply jolt-invoke mainv args)))\n"
;; it to `user` before -main, matching clojure.main (*ns* is
;; `user` when a `-m` -main runs, so a runtime resolve of an
;; aliased symbol behaves the same as on the JVM / interpreted
;; joltc, not off the entry ns's alias table).
" (set-chez-ns! \"user\")\n"
" (when (and maincell (var-cell-defined? maincell))\n"
" (apply jolt-invoke (var-cell-root maincell) args))))\n"
" (exit 0)))\n")) " (exit 0)))\n"))
(close-port out)) (close-port out))
;; 4. compile -> boot -> link. Two paths, chosen by whether this process ;; 4. compile -> boot -> link. Two paths, chosen by whether this process

View file

@ -11,26 +11,6 @@
(define cli-args (cdr (command-line))) ; drop the script name (define cli-args (cdr (command-line))) ; drop the script name
;; Fail early and actionably when the vendored submodules aren't checked out —
;; a plain `git clone` or GitHub's auto-generated "Source code" release archive
;; lacks them, and the raw failure ("load failed for vendor/irregex/irregex.scm")
;; doesn't say how to fix it. (The self-contained joltc binary embeds these and
;; never runs this file.)
(unless (file-exists? "vendor/irregex/irregex.scm")
(display "jolt: vendor submodules are missing (vendor/irregex).
" (current-error-port))
(display "GitHub's 'Source code' release archives don't include submodules.
" (current-error-port))
(display "Clone the repo instead:
" (current-error-port))
(display " git clone --recurse-submodules https://github.com/jolt-lang/jolt.git
" (current-error-port))
(display "or, in an existing checkout:
" (current-error-port))
(display " git submodule update --init --recursive
" (current-error-port))
(exit 1))
(load "host/chez/rt.ss") (load "host/chez/rt.ss")
(set-chez-ns! "clojure.core") (set-chez-ns! "clojure.core")
(load "host/chez/seed/prelude.ss") (load "host/chez/seed/prelude.ss")
@ -66,9 +46,6 @@
(when bt (display " trace:\n" port) (display bt port))) (when bt (display " trace:\n" port) (display bt port)))
(exit 1))) (exit 1)))
;; JOLT_TRACE opt-in, at runtime (before any app ns compiles) so the app is traced.
(jolt-trace-init-from-env!)
(guard (v (#t (jolt-report-uncaught v))) (guard (v (#t (jolt-report-uncaught v)))
(cond (cond
;; -e EXPR — evaluate one expression and print it (blank for nil). Wrapped in ;; -e EXPR — evaluate one expression and print it (blank for nil). Wrapped in

View file

@ -298,21 +298,8 @@
(define empty-pmap-hash (make-pmap empty-hnode 0 #f)) ; hash-order backing (sets) (define empty-pmap-hash (make-pmap empty-hnode 0 #f)) ; hash-order backing (sets)
(define pmap-absent (list 'absent)) ; unique missing-key sentinel (define pmap-absent (list 'absent)) ; unique missing-key sentinel
;; PersistentArrayMap threshold: assoc of a new key promotes to hash mode once the ;; PersistentArrayMap threshold: assoc of a new key promotes to hash mode once the
;; map already holds 8 entries (array.length >= 16 in the reference). Clojure 1.13 ;; map already holds 8 entries (array.length >= 16 in the reference).
;; raised the limit to 64 for maps whose keys are ALL keywords (the common
;; keyword-map case); mixed-key maps still cap at 8.
(define array-map-limit 8) (define array-map-limit 8)
(define array-map-limit-kw 64)
(define (all-keywords? ks)
(or (null? ks) (and (keyword? (car ks)) (all-keywords? (cdr ks)))))
;; Should a map of `cnt` entries with insertion order `ord` stay in array mode
;; when key `k` is added? Under 8 always; a keyword-only map (existing keys + the
;; new key all keywords) grows to 64; otherwise it caps at 8.
(define (pmap-array-keep? cnt ord k)
(cond ((fx<? cnt array-map-limit) #t)
((fx>=? cnt array-map-limit-kw) #f)
((and (keyword? k) (all-keywords? ord)) #t)
(else #f)))
(define (append-key ord k) (append ord (list k))) (define (append-key ord k) (append ord (list k)))
(define (remove-key ord k) (let loop ((o ord)) (cond ((null? o) '()) ((jolt= (car o) k) (cdr o)) (else (cons (car o) (loop (cdr o))))))) (define (remove-key ord k) (let loop ((o ord)) (cond ((null? o) '()) ((jolt= (car o) k) (cdr o)) (else (cons (car o) (loop (cdr o)))))))
@ -323,7 +310,7 @@
(let* ((added (box #f)) (r (node-assoc (pmap-root m) 0 (key-hash k) k v added)) (let* ((added (box #f)) (r (node-assoc (pmap-root m) 0 (key-hash k) k v added))
(cnt (pmap-cnt m)) (ord (pmap-order m))) (cnt (pmap-cnt m)) (ord (pmap-order m)))
(if (unbox added) (if (unbox added)
(if (and ord (pmap-array-keep? cnt ord k)) (if (and ord (fx<? cnt array-map-limit))
(make-pmap r (fx+ cnt 1) (append-key ord k)) (make-pmap r (fx+ cnt 1) (append-key ord k))
(make-pmap r (fx+ cnt 1) #f)) (make-pmap r (fx+ cnt 1) #f))
(make-pmap r cnt ord)))) (make-pmap r cnt ord))))
@ -365,14 +352,10 @@
(let loop ((ks ord) (a acc)) (let loop ((ks ord) (a acc))
(if (null? ks) a (loop (cdr ks) (proc (car ks) (pmap-get m (car ks) jolt-nil) a)))) (if (null? ks) a (loop (cdr ks) (proc (car ks) (pmap-get m (car ks) jolt-nil) a))))
(node-fold (pmap-root m) proc acc)))) (node-fold (pmap-root m) proc acc))))
;; map LITERAL ({...}): array map up to 8 entries (64 if keyword-only, per 1.13), ;; map LITERAL ({...}): array map up to 8 entries, hash map beyond (RT.map).
;; hash map beyond (RT.map).
(define (jolt-hash-map . kvs) (define (jolt-hash-map . kvs)
(let loop ((m empty-pmap) (kvs kvs)) (let loop ((m empty-pmap) (kvs kvs))
(cond ((null? kvs) (cond ((null? kvs) (if (fx>? (pmap-cnt m) array-map-limit) (pmap->hash m) m))
(let ((cnt (pmap-cnt m)) (ord (pmap-order m)))
(if (fx>? cnt (if (all-keywords? ord) array-map-limit-kw array-map-limit))
(pmap->hash m) m)))
((null? (cdr kvs)) (error 'hash-map "odd number of map literal entries")) ((null? (cdr kvs)) (error 'hash-map "odd number of map literal entries"))
(else (loop (pmap-put-ordered m (car kvs) (cadr kvs)) (cddr kvs)))))) (else (loop (pmap-put-ordered m (car kvs) (cadr kvs)) (cddr kvs))))))
;; array-map ctor: insertion-ordered regardless of size (createAsIfByAssoc). ;; array-map ctor: insertion-ordered regardless of size (createAsIfByAssoc).

View file

@ -110,38 +110,6 @@
;; older seed during the first re-mint pass. ;; older seed during the first re-mint pass.
(let ((scv (var-deref "jolt.backend-scheme" "set-var-cache!"))) (let ((scv (var-deref "jolt.backend-scheme" "set-var-cache!")))
(when (procedure? scv) (scv #t))) (when (procedure? scv) (scv #t)))
;; JOLT_TRACE is a falsey value (case-insensitive) — the single predicate both the
;; dev-mode enable and the whole-run enable consult, so "off" never accidentally
;; means "on". An empty / unset value is NOT falsey here — it carries no signal, so
;; dev mode still traces and a whole run still doesn't.
(define (jolt-trace-env-off? e)
(and (string? e)
(let ((s (string-downcase e)))
(or (string=? s "0") (string=? s "false") (string=? s "no")
(string=? s "off") (string=? s "n")))))
;; Tail-frame history. Turning it on makes the emitter add a per-fn history push to
;; every fn compiled AFTERWARD, and allocates this thread's ring. Suppressed when
;; JOLT_TRACE is a falsey value, so JOLT_TRACE=0 / off / no disables it in dev mode.
(define (jolt-enable-trace!)
(unless (jolt-trace-env-off? (getenv "JOLT_TRACE"))
(let ((stf (var-deref "jolt.backend-scheme" "set-trace-frames!")))
(when (procedure? stf) (stf #t)))
(jolt-trace-enable!)))
;; Exposed so the REPL / nREPL entrypoints (jolt.main, jolt.nrepl) can turn tracing
;; on for REPL-driven development without the user setting JOLT_TRACE. Because the
;; push is baked in at compile time, only code compiled after this call is traced —
;; which is exactly the code you eval / reload in a live session.
(def-var! "jolt.host" "enable-trace!" jolt-enable-trace!)
;; Explicit opt-in for a whole run (JOLT_TRACE=1): turn tracing on BEFORE any app
;; namespace is compiled, so a plain `-M:run` traces the app's own code too. Called
;; from the runtime entrypoints (cli.ss, and the built joltc launcher) — NOT at load
;; time: a built joltc runs top-level forms at heap-build time, where JOLT_TRACE is
;; always unset, so a load-time check would never see the user's runtime env. Only an
;; affirmative value (set, non-empty, not falsey) forces it on.
(define (jolt-trace-init-from-env!)
(let ((e (getenv "JOLT_TRACE")))
(when (and e (fx>? (string-length e) 0) (not (jolt-trace-env-off? e)))
(jolt-enable-trace!))))
;; (with-meta sym m) -> sym, else x — an (ns ^:no-doc name …) yields the name with ;; (with-meta sym m) -> sym, else x — an (ns ^:no-doc name …) yields the name with
;; reader metadata as a with-meta form; strip it to read the bare ns symbol. ;; reader metadata as a with-meta form; strip it to read the bare ns symbol.
@ -267,9 +235,6 @@
;; record this form's source location first, so a compile- or run-time error ;; record this form's source location first, so a compile- or run-time error
;; in it reports the right place. ;; in it reports the right place.
(jolt-enter-form! form) (jolt-enter-form! form)
;; drop tail-frame history from earlier top-level forms, so an error's trace
;; shows only this form's own call history (a no-op unless JOLT_TRACE is on).
(jolt-trace-reset!)
(eval (read (open-input-string (jolt-analyze-emit-form form ns))) (eval (read (open-input-string (jolt-analyze-emit-form form ns)))
(interaction-environment))))) (interaction-environment)))))

View file

@ -46,10 +46,6 @@
;; after it). Guarded for the first re-mint pass off an older seed. ;; after it). Guarded for the first re-mint pass off an older seed.
(let ((scv (var-deref "jolt.backend-scheme" "set-var-cache!"))) (let ((scv (var-deref "jolt.backend-scheme" "set-var-cache!")))
(when (procedure? scv) (scv #f))) (when (procedure? scv) (scv #f)))
;; Tail-frame tracing off for the mint + `jolt build`: the seed must stay a
;; byte-fixpoint, and a built app should carry no per-call trace overhead.
(let ((stf (var-deref "jolt.backend-scheme" "set-trace-frames!")))
(when (procedure? stf) (stf #f)))
(define (ei-compile-form ctx f optimize?) (define (ei-compile-form ctx f optimize?)
(let ((ir (jolt-ce-analyze ctx f))) (let ((ir (jolt-ce-analyze ctx f)))
(jolt-ce-emit-top (if optimize? (jolt-ce-run-passes ir ctx) ir)))) (jolt-ce-emit-top (if optimize? (jolt-ce-run-passes ir ctx) ir))))

View file

@ -579,9 +579,9 @@
;; pthread_sigmask"). Guard so a non-POSIX host yields #f; jolt-set-sigint-blocked ;; pthread_sigmask"). Guard so a non-POSIX host yields #f; jolt-set-sigint-blocked
;; then no-ops (Windows delivers ^C through the console, not a per-thread mask). ;; then no-ops (Windows delivers ^C through the console, not a per-thread mask).
(define c-pthread-sigmask (define c-pthread-sigmask
(jolt-foreign-proc-safe "pthread_sigmask" '(int u8* u8*) 'int)) (guard (e (#t #f)) (foreign-procedure "pthread_sigmask" (int u8* u8*) int)))
(define c-sigemptyset (jolt-foreign-proc-safe "sigemptyset" '(u8*) 'int)) (define c-sigemptyset (guard (e (#t #f)) (foreign-procedure "sigemptyset" (u8*) int)))
(define c-sigaddset (jolt-foreign-proc-safe "sigaddset" '(u8* int) 'int)) (define c-sigaddset (guard (e (#t #f)) (foreign-procedure "sigaddset" (u8* int) int)))
;; POSIX SIG_BLOCK/SIG_UNBLOCK numerics differ by platform: Linux/glibc 0/1, ;; POSIX SIG_BLOCK/SIG_UNBLOCK numerics differ by platform: Linux/glibc 0/1,
;; Darwin/macOS 1/2 (SIG_UNBLOCK is SIG_BLOCK+1 on both). Resolve SIG_BLOCK for ;; Darwin/macOS 1/2 (SIG_UNBLOCK is SIG_BLOCK+1 on both). Resolve SIG_BLOCK for
;; this host from the machine-type symbol — macOS builds contain "osx". ;; this host from the machine-type symbol — macOS builds contain "osx".

View file

@ -55,7 +55,9 @@
(lambda () (lambda ()
(unless tried? (unless tried?
(set! tried? #t) (set! tried? #t)
(set! fp (jolt-foreign-proc-safe "sched_yield" '() 'int))) (set! fp (guard (e (#t #f))
(load-shared-object #f)
(foreign-procedure "sched_yield" () int))))
(if fp (fp) (sleep (make-time 'time-duration 0 0))) (if fp (fp) (sleep (make-time 'time-duration 0 0)))
jolt-nil))) jolt-nil)))

View file

@ -80,18 +80,13 @@
;; every Chez binary) — no external toolchain. Falls back to /bin/sh chmod if the ;; every Chez binary) — no external toolchain. Falls back to /bin/sh chmod if the
;; symbol can't be resolved. ;; symbol can't be resolved.
(define jolt-chmod-755 (define jolt-chmod-755
(let ((c (jolt-foreign-proc-safe "chmod" '(string int) 'int))) (let ((c (guard (e (#t #f))
(load-shared-object #f)
(foreign-procedure "chmod" (string int) int))))
(lambda (path) (lambda (path)
(cond (if c
(c (c path #o755)) (c path #o755)
;; Windows has no chmod and needs none (execute is by extension) (system (string-append "chmod 755 '" path "'"))))))
((let ((m (symbol->string (machine-type))))
(let loop ((i 0))
(cond ((> (+ i 2) (string-length m)) #f)
((string=? (substring m i (+ i 2)) "nt") #t)
(else (loop (+ i 1))))))
0)
(else (system (string-append "chmod 755 '" path "'")))))))
;; A user-facing relative path resolves against JOLT_PWD — the user's cwd before ;; A user-facing relative path resolves against JOLT_PWD — the user's cwd before
;; the launcher cd'd to the jolt repo root — matching the JVM, where io/file is ;; the launcher cd'd to the jolt repo root — matching the JVM, where io/file is

View file

@ -42,20 +42,6 @@ if [ "$got_e" != "45" ]; then
exit 1 exit 1
fi fi
# 2b. JOLT_TRACE must take effect in the BUILT binary. The env check runs at
# runtime (the launcher), NOT at heap-build where JOLT_TRACE is always unset — so
# an uncaught error shows a tail-frame trace recovering the TCO-elided chain, and
# exactly ONE trace block (the launcher must not double-print it).
got_tr="$(env -i HOME="$HOME" JOLT_TRACE=1 "$joltc" -e '(defn a [x] (+ x 1)) (defn b [x] (a x)) (b :x)' 2>&1)"
if ! printf '%s' "$got_tr" | grep -q ' trace:' || ! printf '%s' "$got_tr" | grep -q 'b'; then
echo " FAIL: JOLT_TRACE=1 in the built joltc produced no tail-frame trace"
echo "--- got ---"; echo "$got_tr"; exit 1
fi
if [ "$(printf '%s' "$got_tr" | grep -c ' trace:')" != "1" ]; then
echo " FAIL: built joltc double-printed the trace block"
echo "--- got ---"; echo "$got_tr"; exit 1
fi
# 3. Build an app through the distributed joltc with an EMPTY environment — no # 3. Build an app through the distributed joltc with an EMPTY environment — no
# PATH at all, so no chez, no cc, no shell tools are reachable. This is the core # PATH at all, so no chez, no cc, no shell tools are reachable. This is the core
# guarantee: joltc compiles apps entirely on its own. # guarantee: joltc compiles apps entirely on its own.

View file

@ -384,14 +384,3 @@
(def-var! "jolt.host" "load-namespace" (lambda (n) (load-namespace n) jolt-nil)) (def-var! "jolt.host" "load-namespace" (lambda (n) (load-namespace n) jolt-nil))
(def-var! "jolt.host" "file-exists?" (lambda (p) (if (file-exists? p) #t #f))) (def-var! "jolt.host" "file-exists?" (lambda (p) (if (file-exists? p) #t #f)))
(def-var! "jolt.host" "getenv" (lambda (n) (let ((v (getenv n))) (if v v jolt-nil)))) (def-var! "jolt.host" "getenv" (lambda (n) (let ((v (getenv n))) (if v v jolt-nil))))
;; jolt version string. A self-contained binary build bakes the real tag into the
;; saved heap by emitting (set! jolt-baked-version "…") in flat.ss; a dev run off
;; the seed leaves it #f and falls back to $JOLT_VERSION (bin/joltc sets it from
;; `git describe`), then "dev".
(define jolt-baked-version #f)
(def-var! "jolt.host" "jolt-version"
(lambda ()
(or jolt-baked-version
(let ((v (getenv "JOLT_VERSION"))) (and v (> (string-length v) 0) v))
"dev")))

View file

@ -11,17 +11,6 @@
;; Emitted programs do `(load "host/chez/rt.ss")`; this loads values.ss in turn. ;; Emitted programs do `(load "host/chez/rt.ss")`; this loads values.ss in turn.
(load "host/chez/values.ss") (load "host/chez/values.ss")
;; Resolve a libc entry point at RUN time. A literal (foreign-procedure "name" …)
;; in COMPILED code becomes a fasl relocation resolved when the boot loads — on a
;; platform lacking the symbol (chmod/sigaddset on Windows) that kills the boot
;; before any guard can run. eval defers the lookup to evaluation time, where the
;; guard works; returns #f when the entry doesn't exist.
(define (jolt-foreign-proc-safe name args res)
(guard (e (#t #f))
(load-shared-object #f)
(and (foreign-entry? name)
(eval `(foreign-procedure ,name ,args ,res)))))
(load "host/chez/collections.ss") (load "host/chez/collections.ss")
(load "host/chez/seq.ss") (load "host/chez/seq.ss")
@ -60,112 +49,6 @@
;; stack trace (source-registry.ss). call/cc is paid only on a throw, never per ;; stack trace (source-registry.ss). call/cc is paid only on a throw, never per
;; call; the captured k is walked, never invoked. ;; call; the captured k is walked, never invoked.
(define jolt-throw-cont (make-thread-parameter #f)) (define jolt-throw-cont (make-thread-parameter #f))
;; --- tail-frame history: a ring of rings (opt-in) ----------------------------
;; TCO erases tail-called frames from the native continuation, so an uncaught
;; error's backtrace shows only the surviving non-tail spine — the immediate error
;; site is often a tail call and is missing. When tracing is enabled (JOLT_TRACE,
;; wired in compile-eval.ss), each compiled fn records its frame-name on entry, and
;; the reporter reads this history to recover TCO-elided frames.
;;
;; The store is MIT-Scheme's "history" shape — a ring of rings. The OUTER ring
;; holds one RIB per non-tail subproblem (the real call spine); each rib's INNER
;; ring holds the recent tail-calls made AT that subproblem. A non-tail entry
;; advances the outer ring (a fresh rib); a tail entry rotates the current rib's
;; inner ring. So a tight tail loop (mutual recursion, a non-recur self-tail-call)
;; churns ONE rib's small inner ring instead of flushing the outer spine — the
;; caller context that led into the loop survives. Both rings are fixed-size, so
;; the whole history is bounded: a constant space factor, NOT a change to the
;; asymptotic space TCO guarantees.
;;
;; Whether an entry is tail or non-tail is set by the CALLER: the emitter marks a
;; tail call with (jolt-trace-mark! #t) right before it; a non-tail entry is the
;; default. NOTE this is best-effort: a tail call routed through jolt-invoke to a
;; target that has no entry prologue (a core/native fn, an anonymous fn held in a
;; var) does not consume the mark, so a following non-tail frame can be mislabeled
;; as a tail rotation — a cosmetic mis-grouping in the trace, never a wrong result.
(define jolt-trace-outer-size 48) ; ribs (non-tail spine depth kept)
(define jolt-trace-inner-size 6) ; tail-calls kept per subproblem
;; A history: #(ribs-vector outer-head outer-count). A rib: #(name-vector head count).
(define (jolt-make-rib) (vector (make-vector jolt-trace-inner-size #f) 0 0))
(define (jolt-make-history)
(let ((ribs (make-vector jolt-trace-outer-size #f)))
(let loop ((i 0))
(when (fx<? i jolt-trace-outer-size)
(vector-set! ribs i (jolt-make-rib)) (loop (fx+ i 1))))
(vector ribs 0 0)))
;; A global switch (all threads) plus a per-thread ring, lazily created on first
;; use — so code run on a spawned thread (a future/agent) records into ITS OWN
;; history, not the enabling thread's (make-thread-parameter hands a new thread the
;; initial #f, so we can't rely on inheritance).
(define jolt-trace-on? #f)
(define jolt-trace-ring (make-thread-parameter #f))
(define jolt-trace-tail? (make-thread-parameter #f)) ; caller-set, consumed per entry
(define (jolt-trace-enable!) (set! jolt-trace-on? #t) (jolt-trace-ring (jolt-make-history)))
;; this thread's ring, created on demand while tracing is on
(define (jolt-trace-cur-ring)
(or (jolt-trace-ring)
(and jolt-trace-on? (let ((h (jolt-make-history))) (jolt-trace-ring h) h))))
;; Drop accumulated history at a top-level boundary (compile-eval.ss calls this per
;; top-level form) so an error's trace shows only the forms that led to it, not the
;; frames of earlier, already-returned REPL/eval forms.
(define (jolt-trace-reset!)
(when (jolt-trace-ring) (jolt-trace-ring (jolt-make-history)) (jolt-trace-tail? #f)))
(define (jolt-trace-mark! t) (jolt-trace-tail? t))
;; push name into a rib's inner ring
(define (jolt-rib-push! rib name)
(let ((buf (vector-ref rib 0)) (i (vector-ref rib 1)) (cnt (vector-ref rib 2)))
(vector-set! buf i name)
(vector-set! rib 1 (fxmod (fx+ i 1) jolt-trace-inner-size))
(when (fx<? cnt jolt-trace-inner-size) (vector-set! rib 2 (fx+ cnt 1)))))
;; a non-tail entry: advance the outer ring, reset the new rib, seed it with name
(define (jolt-history-nontail! h name)
(let* ((ribs (vector-ref h 0)) (oh (vector-ref h 1)) (oc (vector-ref h 2))
(rib (vector-ref ribs oh)))
(vector-set! rib 1 0) (vector-set! rib 2 0)
(jolt-rib-push! rib name)
(vector-set! h 1 (fxmod (fx+ oh 1) jolt-trace-outer-size))
(when (fx<? oc jolt-trace-outer-size) (vector-set! h 2 (fx+ oc 1)))))
;; a tail entry: rotate the CURRENT rib's inner ring (bootstrap a rib if none yet)
(define (jolt-history-tail! h name)
(if (fx=? (vector-ref h 2) 0)
(jolt-history-nontail! h name)
(let* ((ribs (vector-ref h 0))
(cur (fxmod (fx+ (fx- (vector-ref h 1) 1) jolt-trace-outer-size)
jolt-trace-outer-size)))
(jolt-rib-push! (vector-ref ribs cur) name))))
;; Record a frame entry, routed by the caller's tail mark; then reset the mark so a
;; subsequent entry reached WITHOUT a mark (e.g. via apply) defaults to non-tail.
(define (jolt-trace-push! name)
(let ((h (jolt-trace-cur-ring)))
(when h
(if (jolt-trace-tail?) (jolt-history-tail! h name) (jolt-history-nontail! h name))
(jolt-trace-tail? #f)))
jolt-nil)
;; a rib's inner names, most-recent (deepest) tail first
(define (jolt-rib-names rib)
(let ((buf (vector-ref rib 0)) (head (vector-ref rib 1)) (cnt (vector-ref rib 2)))
(let loop ((k 1) (acc '()))
(if (fx>? k cnt)
(reverse acc)
(loop (fx+ k 1)
(cons (vector-ref buf (fxmod (fx+ (fx- head k) jolt-trace-inner-size)
jolt-trace-inner-size))
acc))))))
;; The whole history flattened to frame-names, most-recent (deepest) first:
;; current rib's tail-history, then its non-tail caller's, and so on outward.
(define (jolt-trace-snapshot)
(let ((h (jolt-trace-ring)))
(if (not h) '()
(let* ((ribs (vector-ref h 0)) (oh (vector-ref h 1)) (oc (vector-ref h 2)))
(let loop ((k 1) (acc '()))
(if (fx>? k oc)
(apply append (reverse acc))
(let ((idx (fxmod (fx+ (fx- oh k) jolt-trace-outer-size) jolt-trace-outer-size)))
(loop (fx+ k 1) (cons (jolt-rib-names (vector-ref ribs idx)) acc)))))))))
(define-condition-type &jolt-throw &condition (define-condition-type &jolt-throw &condition
make-jolt-throw-condition jolt-throw-condition? make-jolt-throw-condition jolt-throw-condition?
(value jolt-throw-condition-value)) (value jolt-throw-condition-value))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -30,39 +30,6 @@ check_loc() {
fi fi
} }
# An uncaught error's stack trace must name the runtime-eval'd fn frames that
# survive TCO (the non-tail spine), even though the eval path registers no source
# map — "print what is available". Asserts a substring appears under " trace:".
check_trace() {
err="$(bin/joltc -e "$1" 2>&1 >/dev/null)"
if printf '%s' "$err" | grep -q ' trace:' && printf '%s' "$err" | grep -q "$2"; then
pass=$((pass + 1))
else
echo " FAIL (trace): $1"
echo " want stderr trace to contain \`$2\`, got \`$err\`"
fails=$((fails + 1))
fi
}
# JOLT_TRACE opts into the tail-frame history (the ring of rings): every $2 (an
# ERE) must match the " trace:" block. Used to assert TCO-elided frames are
# recovered and non-tail caller context survives a tail loop.
check_trace_on() {
err="$(JOLT_TRACE=1 bin/joltc -e "$1" 2>&1 >/dev/null)"
ok=1
printf '%s' "$err" | grep -q ' trace:' || ok=0
shift
for want in "$@"; do
printf '%s' "$err" | grep -Eq "$want" || ok=0
done
if [ "$ok" = 1 ]; then
pass=$((pass + 1))
else
echo " FAIL (trace-on): want [$*] in trace, got \`$err\`"
fails=$((fails + 1))
fi
}
check '(+ 1 2)' '3' check '(+ 1 2)' '3'
check '(defn fib [n] (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))) (fib 15)' '610' check '(defn fib [n] (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))) (fib 15)' '610'
check '(->> (range 10) (filter even?) (map (fn [x] (* x x))) (reduce +))' '120' check '(->> (range 10) (filter even?) (map (fn [x] (* x x))) (reduce +))' '120'
@ -93,53 +60,6 @@ check '(try (load-string "(+") (catch :default e (ex-message e)))' 'EOF while re
check_loc '(throw (ex-info "boom" {}))' 'boom' check_loc '(throw (ex-info "boom" {}))' 'boom'
check_loc '(do (+ 1 1) (/ 1 0))' ' at 1:' check_loc '(do (+ 1 1) (/ 1 0))' ' at 1:'
# Runtime-eval'd fns aren't source-mapped, but their native frame names survive on
# the non-tail spine; the trace must show them. deepest/+ are tail calls (erased);
# middle and outer wait on a non-tail (inc …) so their frames are live at the throw.
trace_prog='(defn deepest [x] (+ x 1)) (defn middle [x] (inc (deepest x))) (defn outer [x] (inc (middle x))) (outer :nan)'
check_trace "$trace_prog" 'middle'
check_trace "$trace_prog" 'outer'
# JOLT_TRACE (tail-frame history / ring of rings). An all-tail chain is entirely
# TCO-erased from the continuation, but the history recovers every frame — incl.
# `deepest`, the actual error site.
check_trace_on '(defn deepest [x] (+ x 1)) (defn middle [x] (deepest x)) (defn outer [x] (middle x)) (outer :nan)' \
'deepest' 'middle' 'outer'
# A tail loop (a<->b) under a NON-tail caller: the loop is confined to one rib's
# bounded inner ring, so the caller context (`driver`, `top`) is NOT flushed out —
# the point of the ring of rings.
check_trace_on '(declare b) (defn a [n] (if (zero? n) (+ :x 1) (b (dec n)))) (defn b [n] (a n)) (defn driver [] (inc (a 6))) (defn top [] (inc (driver))) (top)' \
'driver' 'top'
# A ^long/^double return hint wraps the body in a coercion, so the hinted fn's call
# is NOT a tail call — its own frame is still live and must appear (not be elided).
check_trace_on '(defn g [n] (+ :x n)) (defn ^long f [n] (g n)) (f 3)' 'f' 'g'
# History is per top-level form: a later form's error trace shows its own frames
# (h2/u2), not frames from an earlier, already-returned form (h1/u1).
check_trace_on '(defn h1 [x] (inc x)) (defn u1 [] (inc (h1 5))) (u1) (defn h2 [x] (+ :x x)) (defn u2 [] (inc (h2 5))) (u2)' \
'h2' 'u2'
err_stale="$(JOLT_TRACE=1 bin/joltc -e '(defn h1 [x] (inc x)) (defn u1 [] (inc (h1 5))) (u1) (defn h2 [x] (+ :x x)) (defn u2 [] (inc (h2 5))) (u2)' 2>&1 >/dev/null)"
if printf '%s' "$err_stale" | grep -q 'h1'; then
echo " FAIL (trace-on): stale frame h1 from an earlier form leaked into the trace"
fails=$((fails + 1))
else
pass=$((pass + 1))
fi
# A file-backed project run maps each runtime-compiled frame to ns/name (file:line)
# — the eval path registers source in trace mode, so the trace isn't bare names.
tr_proj="$(mktemp -d)"
mkdir -p "$tr_proj/src/tp"
printf '{:paths ["src"] :aliases {:run {:main-opts ["-m" "tp.core"]}}}\n' > "$tr_proj/deps.edn"
printf '(ns tp.core)\n(defn deep [x] (+ x 1))\n(defn mid [x] (inc (deep x)))\n(defn -main [& _] (mid :nan))\n' > "$tr_proj/src/tp/core.clj"
tr_out="$(JOLT_TRACE=1 JOLT_PWD="$tr_proj" bin/joltc -M:run 2>&1)"
if printf '%s' "$tr_out" | grep -Eq 'tp\.core/deep \(.*/tp/core\.clj:2\)'; then
pass=$((pass + 1))
else
echo " FAIL: JOLT_TRACE trace should map a frame to ns/name (file:line)"
printf '%s\n' "$tr_out" | sed 's/^/ | /'
fails=$((fails + 1))
fi
rm -rf "$tr_proj"
# --help prints usage, and lists the nREPL server under its real flag name. # --help prints usage, and lists the nREPL server under its real flag name.
help_out="$(bin/joltc --help 2>/dev/null)" help_out="$(bin/joltc --help 2>/dev/null)"
if printf '%s' "$help_out" | grep -q -- '--nrepl-server'; then if printf '%s' "$help_out" | grep -q -- '--nrepl-server'; then
@ -239,25 +159,5 @@ else
fails=$((fails + 1)) fails=$((fails + 1))
fi fi
# REPL-driven development traces by default: an error in an evaluated form shows a
# tail-frame backtrace with no JOLT_TRACE set. rb tail-calls ra tail-calls +, all
# TCO-elided from the continuation — only the history recovers them.
repl_err="$(printf '(defn ra [x] (+ x 1))\n(defn rb [x] (ra x))\n(rb :nan)\n:exit\n' | bin/joltc repl 2>&1)"
if printf '%s' "$repl_err" | grep -q ' trace:' && printf '%s' "$repl_err" | grep -q 'rb'; then
pass=$((pass + 1))
else
echo " FAIL: a REPL error should show a tail-frame trace by default"
printf '%s\n' "$repl_err" | sed 's/^/ | /'
fails=$((fails + 1))
fi
# JOLT_TRACE=0 opts out — no trace in the REPL.
repl_off="$(printf '(defn ra [x] (+ x 1))\n(defn rb [x] (ra x))\n(rb :nan)\n:exit\n' | JOLT_TRACE=0 bin/joltc repl 2>&1)"
if printf '%s' "$repl_off" | grep -q ' trace:'; then
echo " FAIL: JOLT_TRACE=0 should suppress the REPL trace"
fails=$((fails + 1))
else
pass=$((pass + 1))
fi
echo "cli smoke: $pass passed, $fails failed" echo "cli smoke: $pass passed, $fails failed"
[ "$fails" -eq 0 ] [ "$fails" -eq 0 ]

View file

@ -57,36 +57,10 @@
((symbol? nm) (symbol->string nm)) ((symbol? nm) (symbol->string nm))
(else #f))))))) (else #f)))))))
;; Frame names that are pure Chez / jolt-runtime plumbing — the eval boundary, ;; Walk a continuation, returning the registered jolt frames (innermost first) as
;; the var-cell trampoline, continuation/winder internals. They carry no Clojure ;; (frame-name . record) pairs, where record is #(ns name file line) or the symbol
;; meaning, so an unmapped frame with one of these names is dropped from the trace ;; 'ambiguous. Unmapped frames (host spine, anonymous lambdas) are skipped; raw
;; (a MAPPED frame is always kept — a jolt fn that happens to share the name still ;; depth is capped.
;; resolves to its source). Any name Chez prefixes with `$` (system) or that jolt
;; prefixes with `jolt-` (host runtime) is plumbing too.
(define srcreg-plumbing-names
(let ((h (make-hashtable string-hash string=?)))
(for-each (lambda (s) (hashtable-set! h s #t))
'("dynamic-wind" "winder-dummy" "ksrc" "invoke" "apply"
"call-with-values" "call/cc" "call-with-current-continuation"
"raise" "raise-continuable" "with-exception-handler" "guard"
"eval" "compile" "interpret" "expand" "read" "load"
;; host dispatch/coercion helpers (not `jolt-` prefixed) that carry
;; no Clojure meaning in a trace
"record-method-dispatch" "protocol-resolve" "devirt-resolve"
"list->cseq" "host-static-call" "host-call"))
h))
(define (srcreg-plumbing-name? nm)
(or (hashtable-ref srcreg-plumbing-names nm #f)
(and (fx>? (string-length nm) 0) (char=? (string-ref nm 0) #\$))
(and (fx>=? (string-length nm) 5) (string=? (substring nm 0 5) "jolt-"))))
;; Walk a continuation, returning its frames (innermost first) as (frame-name .
;; record) pairs. record is a source vector #(ns name file line) for a frame that
;; maps to registered Clojure source, the symbol 'ambiguous for a short name shared
;; across namespaces, or #f for an unmapped-but-named frame (the common case on the
;; open-world eval path, where nothing is registered — the bare frame name is still
;; a useful trace line). Plumbing frames (host spine, eval boundary) and unnamed
;; frames are skipped; raw depth is capped.
(define (jolt-frame-records k) (define (jolt-frame-records k)
;; read the env at call time, not load time: a built binary runs top-level forms ;; read the env at call time, not load time: a built binary runs top-level forms
;; at heap-build time, where this would always be unset. ;; at heap-build time, where this would always be unset.
@ -96,32 +70,26 @@
(if (or (not io) (fx>=? n 400)) (if (or (not io) (fx>=? n 400))
(reverse acc) (reverse acc)
(let* ((nm (srcreg-frame-name io)) (let* ((nm (srcreg-frame-name io))
(src (and nm (hashtable-ref source-registry nm #f))) (src (and nm (hashtable-ref source-registry nm #f))))
;; keep a frame that maps, or any named frame that isn't plumbing
(keep? (and nm (or src (not (srcreg-plumbing-name? nm))))))
(when (and debug? nm) (when (and debug? nm)
(display (string-append " [frame] " nm (if src " *MAPPED*" (display (string-append " [frame] " nm (if src " *MAPPED*" "") "\n")
(if keep? "" " (skipped)")) "\n")
(current-error-port))) (current-error-port)))
(loop (guard (e (#t #f)) (io 'link)) (fx+ n 1) (loop (guard (e (#t #f)) (io 'link)) (fx+ n 1)
(if keep? (cons (cons nm src) acc) acc)))))))) (if src (cons (cons nm src) acc) acc))))))))
;; Render a list of (frame-name . record) pairs (innermost/deepest first) to a ;; Multi-line backtrace for an uncaught value — " ns/name (file:line)" for a
;; backtrace string. record is a source vector #(ns name file line) -> "ns/name ;; mapped frame, the bare frame name for an ambiguous one — or #f when no jolt
;; (file:line)", or 'ambiguous / #f -> the bare frame name. A run of the same ;; frame maps (the caller then prints just the top-level location). Capped to the
;; frame-name collapses to one "name (xN)" line (deep recursion, or a hot fn a ;; innermost frames.
;; loop re-enters), and the number of distinct lines is capped. (define (jolt-backtrace-string v)
(define (jolt-render-recs recs) (let ((k (jolt-error-continuation v)))
(and k
(let ((recs (jolt-frame-records k)))
(and (pair? recs)
(let ((port (open-output-string))) (let ((port (open-output-string)))
(let loop ((rs recs) (shown 0)) (let loop ((rs recs) (shown 0))
(if (or (null? rs) (fx>=? shown 30)) (when (and (pair? rs) (fx<? shown 30))
(get-output-string port)
(let* ((p (car rs)) (frame-name (car p)) (r (cdr p))) (let* ((p (car rs)) (frame-name (car p)) (r (cdr p)))
;; count a maximal run of the same frame-name
(let run ((tail (cdr rs)) (cnt 1))
(if (and (pair? tail) (string=? (car (car tail)) frame-name))
(run (cdr tail) (fx+ cnt 1))
(begin
(put-string port " ") (put-string port " ")
(if (vector? r) (if (vector? r)
(let ((ns (vector-ref r 0)) (nm (vector-ref r 1)) (let ((ns (vector-ref r 0)) (nm (vector-ref r 1))
@ -131,49 +99,10 @@
(put-string port " (") (put-string port file) (put-string port " (") (put-string port file)
(put-string port ":") (put-string port (number->string line)) (put-string port ":") (put-string port (number->string line))
(put-string port ")"))) (put-string port ")")))
(put-string port frame-name)) ; 'ambiguous / unmapped: bare name (put-string port frame-name)) ; 'ambiguous: bare name
(when (fx>? cnt 1) (put-char port #\newline))
(put-string port " (x") (put-string port (number->string cnt)) (put-string port ")")) (loop (cdr rs) (fx+ shown 1))))
(put-char port #\newline) (get-output-string port)))))))
(loop tail (fx+ shown 1))))))))))
;; Multi-line backtrace for an uncaught value. Two sources, in preference order:
;; 1. The tail-frame history ring (rt.ss), when JOLT_TRACE enabled it — an
;; execution history of the runtime-compiled fns entered before the throw,
;; INCLUDING ones TCO erased from the live continuation. Most-recent first.
;; 2. Otherwise the live continuation (jolt-frame-records) — the accurate but
;; TCO-truncated non-tail spine.
;; Each frame maps to "ns/name (file:line)" when registered, else its bare name.
;; #f when neither source yields a frame (the caller then prints just the location).
;; The tail-frame history ring rendered as a backtrace, or #f when tracing is off /
;; empty. A mapped frame is kept; else drop plumbing (same rule as the continuation
;; path) so the two sources read consistently.
(define (jolt-history-backtrace)
(let* ((hist (jolt-trace-snapshot))
(recs (let loop ((ns hist) (acc '()))
(if (null? ns)
(reverse acc)
(let* ((nm (car ns)) (src (hashtable-ref source-registry nm #f)))
(loop (cdr ns)
(if (or src (not (srcreg-plumbing-name? nm)))
(cons (cons nm src) acc) acc)))))))
(and (pair? recs) (jolt-render-recs recs))))
(define (jolt-backtrace-string v)
(or (jolt-history-backtrace)
(let ((k (jolt-error-continuation v)))
(and k
(let ((recs (jolt-frame-records k)))
(and (pair? recs) (jolt-render-recs recs)))))))
;; Exposed for the REPL / nREPL error paths, which catch errors themselves instead
;; of going through the uncaught reporter. Returns the " trace:\n<frames>" block
;; from the tail-frame HISTORY only — the live continuation in a REPL is just the
;; REPL's own machinery — or nil when tracing is off (so a caller can when-let).
(def-var! "jolt.host" "backtrace-string"
(lambda ()
(let ((bt (jolt-history-backtrace)))
(if bt (string-append " trace:\n" bt) jolt-nil))))
;; Render an uncaught jolt throw (any value, not just a Chez condition) to a port: ;; Render an uncaught jolt throw (any value, not just a Chez condition) to a port:
;; an ex-info shows its message + ex-data (+ a host cause); anything else is ;; an ex-info shows its message + ex-data (+ a host cause); anything else is

View file

@ -86,11 +86,7 @@
(if (fx<? i cnt) (begin (vector-set! out i (vector-ref buf i)) (loop (fx+ i 1))) (if (fx<? i cnt) (begin (vector-set! out i (vector-ref buf i)) (loop (fx+ i 1)))
(make-pvec out))))))) (make-pvec out)))))))
((map) ((map)
(let* ((ht (jolt-transient-buf t)) (cnt (hashtable-size ht)) (cap (jolt-transient-n t)) (let* ((ht (jolt-transient-buf t)) (cnt (hashtable-size ht)) (cap (jolt-transient-n t)))
;; Clojure 1.13: a keyword-only map stays an array map up to 64 entries,
;; so a keyword map built through a transient (into {} …) keeps insertion
;; order to 64, matching the literal/assoc paths.
(cap (if (all-keywords? (jolt-transient-ord t)) (fxmax array-map-limit-kw cap) cap)))
(if (fx>? cnt cap) (if (fx>? cnt cap)
;; promoted past the array capacity: hash order ;; promoted past the array capacity: hash order
(let ((m empty-pmap-hash)) (let ((m empty-pmap-hash))

View file

@ -188,23 +188,9 @@
[false nil] [false nil]
(if or-map (keys or-map) []))) (if or-map (keys or-map) [])))
amp? (fn* [x] (and (symbol? x) (= "&" (name x)))) amp? (fn* [x] (and (symbol? x) (= "&" (name x))))
;; split a :keys/:syms/:strs name list at & into [sym bind?] pairs. Names
;; before & bind normally (bind? true); names after & are declared-only
;; (bind? false) — accepted keys (:keys) or required keys (:keys!), per
;; CLJ-2961.
classify
(fn* [names]
(nth (reduce (fn* [st x]
(if (amp? x)
[(nth st 0) false]
[(conj (nth st 0) [x (nth st 1)]) (nth st 1)]))
[[] true] names)
0))
proc proc
(fn* proc [pat init acc] (fn* proc [pat init acc]
(cond (cond
;; CLJ-2954: & is reserved for destructuring rest, never a binding.
(amp? pat) (throw (new IllegalArgumentException "Can't use & as a local binding"))
(symbol? pat) (conj (conj acc pat) init) (symbol? pat) (conj (conj acc pat) init)
(vector? pat) (vector? pat)
(let* [g (symbol (str (gensym))) (let* [g (symbol (str (gensym)))
@ -245,45 +231,30 @@
;; group binds a :keys/:strs/:syms list. dnsp is the destructuring ;; group binds a :keys/:strs/:syms list. dnsp is the destructuring
;; namespace from a qualified key like :ns/keys — it both prefixes ;; namespace from a qualified key like :ns/keys — it both prefixes
;; the lookup key and overrides a bare symbol's namespace. ;; the lookup key and overrides a bare symbol's namespace.
;; group binds a :keys/:strs/:syms list. checked? marks the
;; :keys!/:strs!/:syms! variants (CLJ-2961): lookups use req!
;; (throw on missing) instead of get. A pair is [sym bind?];
;; bind? false (names after &) is declared-only — for checked
;; groups it still runs req! (bound to a throwaway gensym) to
;; enforce the key, for unchecked groups it's a no-op.
group group
(fn* group [a names kind dnsp checked?] (fn* group [a names kind dnsp]
(if names (if names
(reduce (reduce
;; s is a symbol (a b) or a keyword (:a :b); name/ ;; s is a symbol (a b) or a keyword (:a :b); name/
;; namespace handle both, so :keys [:major] binds ;; namespace handle both, so :keys [:major] binds
;; `major` looking up :major (str would keep the colon). ;; `major` looking up :major (str would keep the colon).
(fn* [aa pair] (fn* [aa s]
(let* [s (nth pair 0) (let* [local (name s)
bind? (nth pair 1)
local (name s)
nsp (or (namespace s) dnsp) nsp (or (namespace s) dnsp)
keyform (cond keyform (cond
(= kind :kw) (keyword (if nsp (str nsp "/" local) local)) (= kind :kw) (keyword (if nsp (str nsp "/" local) local))
(= kind :str) local (= kind :str) local
:else `(quote ~(symbol nsp local))) :else `(quote ~(symbol nsp local)))
fo (find-or or-map local) fo (find-or or-map local)]
lookup (cond (conj (conj aa (symbol local))
checked? `(req! ~gm ~keyform) (if (nth fo 0)
(nth fo 0) `(get ~gm ~keyform ~(nth fo 1)) `(get ~gm ~keyform ~(nth fo 1))
:else `(get ~gm ~keyform))] `(get ~gm ~keyform)))))
(cond a names)
bind? (conj (conj aa (symbol local)) lookup)
checked? (conj (conj aa (symbol (str (gensym)))) lookup)
:else aa)))
a (classify names))
a)) a))
g1 (group base (get pat :keys) :kw nil false) g1 (group base (get pat :keys) :kw nil)
g2 (group g1 (get pat :strs) :str nil false) g2 (group g1 (get pat :strs) :str nil)
g3 (group g2 (get pat :syms) :sym nil false) g3 (group g2 (get pat :syms) :sym nil)]
g4 (group g3 (get pat :keys!) :kw nil true)
g5 (group g4 (get pat :strs!) :str nil true)
g6 (group g5 (get pat :syms!) :sym nil true)]
;; remaining keys: a qualified :ns/keys|:ns/strs|:ns/syms groups under ;; remaining keys: a qualified :ns/keys|:ns/strs|:ns/syms groups under
;; its namespace; any other keyword is skipped; a non-keyword is a ;; its namespace; any other keyword is skipped; a non-keyword is a
;; nested binding pattern. ;; nested binding pattern.
@ -291,12 +262,9 @@
(if (keyword? k) (if (keyword? k)
(let* [kn (name k) kns (namespace k)] (let* [kn (name k) kns (namespace k)]
(cond (cond
(and kns (= kn "keys")) (group a (get pat k) :kw kns false) (and kns (= kn "keys")) (group a (get pat k) :kw kns)
(and kns (= kn "strs")) (group a (get pat k) :str kns false) (and kns (= kn "strs")) (group a (get pat k) :str kns)
(and kns (= kn "syms")) (group a (get pat k) :sym kns false) (and kns (= kn "syms")) (group a (get pat k) :sym kns)
(and kns (= kn "keys!")) (group a (get pat k) :kw kns true)
(and kns (= kn "strs!")) (group a (get pat k) :str kns true)
(and kns (= kn "syms!")) (group a (get pat k) :sym kns true)
:else a)) :else a))
;; a direct binding {x :x}: apply its :or default ;; a direct binding {x :x}: apply its :or default
;; (keyed by the local symbol) when the key is absent. ;; (keyed by the local symbol) when the key is absent.
@ -305,7 +273,7 @@
`(get ~gm ~(get pat k) ~(nth fo 1)) `(get ~gm ~(get pat k) ~(nth fo 1))
`(get ~gm ~(get pat k))) `(get ~gm ~(get pat k)))
a)))) a))))
g6 (keys pat))) g3 (keys pat)))
:else (throw (str "unsupported destructuring pattern: " (pr-str pat))))) :else (throw (str "unsupported destructuring pattern: " (pr-str pat)))))
ploop ploop
(fn* ploop [i acc] (fn* ploop [i acc]

View file

@ -89,21 +89,6 @@
(recur nxt (next ks)))) (recur nxt (next ks))))
m))))) m)))))
(defn req!
"Returns the value mapped to key k in map m, like `get`, but throws
IllegalArgumentException when k is not present. Unlike `get`, does not nil-pun:
a key present with a nil value returns nil, an absent key throws. The primitive
behind checked-keys destructuring (:keys! / :syms! / :strs!)."
{:added "1.13"}
[m k]
;; a fresh map is its own identity, so a present-but-nil value is distinguished
;; from an absent key (same trick as get-in's sentinel).
(let [sentinel (hash-map)
v (get m k sentinel)]
(if (identical? sentinel v)
(throw (new IllegalArgumentException (str "Expected key: " k)))
v)))
;; find-based, so nil RESULTS are cached too; args canonicalize as a collection key. ;; find-based, so nil RESULTS are cached too; args canonicalize as a collection key.
(defn memoize [f] (defn memoize [f]
(let [mem (atom (hash-map))] (let [mem (atom (hash-map))]

View file

@ -172,14 +172,6 @@
(def var-cache? (atom false)) (def var-cache? (atom false))
(defn set-var-cache! [on] (reset! var-cache? on)) (defn set-var-cache! [on] (reset! var-cache? on))
;; Opt-in tail-frame history (JOLT_TRACE): emit a (jolt-trace-push! "name") at the
;; head of every named fn body, so an entry records the frame into the runtime ring
;; buffer (rt.ss) and a TCO-elided frame still shows in an error's backtrace. OFF
;; during the seed mint and `jolt build` (byte-determinism + no runtime cost);
;; compile-eval.ss turns it on for runtime-eval'd user code when JOLT_TRACE is set.
(def trace-frames? (atom false))
(defn set-trace-frames! [on] (reset! trace-frames? on))
;; A direct-link Scheme binding name for a var. The fqn maps to a unique identifier ;; A direct-link Scheme binding name for a var. The fqn maps to a unique identifier
;; jv$<ns>$<name>; chars that break a Scheme identifier or the `$` separator are ;; jv$<ns>$<name>; chars that break a Scheme identifier or the `$` separator are
;; escaped so distinct vars never collide. ;; escaped so distinct vars never collide.
@ -199,13 +191,6 @@
;; recursion auto-restores them (no manual save/restore, no throw-leak). ;; recursion auto-restores them (no manual save/restore, no throw-leak).
(def ^:dynamic *recur-target* nil) (def ^:dynamic *recur-target* nil)
(def ^:dynamic *known-procs* #{}) (def ^:dynamic *known-procs* #{})
;; True while emitting a node in TAIL position. Only used, in trace mode, to mark a
;; tail call so the runtime routes its callee into the current history rib instead
;; of a new one (rt.ss). It never affects semantics — a wrong value only mislabels
;; a debug trace line — so partial propagation is safe. `emit` (the wrapper below)
;; clears it by default; the tail-transparent forms (fn body, if/do/let/loop) pass
;; it to their tail child. Default false so a top-level form is treated non-tail.
(def ^:dynamic *tail?* false)
(def ^:private gensym-counter (atom 0)) (def ^:private gensym-counter (atom 0))
(defn- fresh-label [prefix] (str prefix (swap! gensym-counter inc))) (defn- fresh-label [prefix] (str prefix (swap! gensym-counter inc)))
@ -268,17 +253,6 @@
(if (or (contains? scheme-reserved s) (contains? bare-native-names s)) (str "_" s) s))) (if (or (contains? scheme-reserved s) (contains? bare-native-names s)) (str "_" s) s)))
(declare emit) (declare emit)
(declare emit*)
;; Ops that pass tail position through to a child (the child can itself be a tail
;; call): if/do carry it to their tail branch/last form, let/loop to their body,
;; and invoke reads it to decide whether the call is tail. Every other op's
;; children are non-tail, so `emit` clears *tail?* before dispatching them — that
;; way a stray true can't leak into, say, a call sitting in a vector literal.
(def ^:private tail-transparent-ops #{:if :do :let :loop :invoke})
(defn emit [node]
(if (and *tail?* (not (tail-transparent-ops (:op node))))
(binding [*tail?* false] (emit* node))
(emit* node)))
;; A Chez string literal. Every char outside printable ASCII becomes a ;; A Chez string literal. Every char outside printable ASCII becomes a
;; codepoint hex escape \x<cp>; ; the named escapes (\n \t \r \" \\) match what ;; codepoint hex escape \x<cp>; ; the named escapes (\n \t \r \" \\) match what
@ -439,10 +413,9 @@
;; letfn lowers to a :let flagged :letrec (mutually-recursive named local fns): ;; letfn lowers to a :let flagged :letrec (mutually-recursive named local fns):
;; Scheme `letrec*` binds them so each sees its siblings. A plain let uses let*. ;; Scheme `letrec*` binds them so each sees its siblings. A plain let uses let*.
(defn- emit-let [node] (defn- emit-let [node]
(let [kw (if (:letrec node) "letrec*" "let*") (let [kw (if (:letrec node) "letrec*" "let*")]
;; bindings are non-tail; the body inherits the let's tail position (str "(" kw " (" (str/join " " (map emit-binding (:bindings node))) ") "
binds (binding [*tail?* false] (str/join " " (mapv emit-binding (:bindings node))))] (emit (:body node)) ")")))
(str "(" kw " (" binds ") " (emit (:body node)) ")")))
(defn- emit-loop [node] (defn- emit-loop [node]
(let [label (fresh-label "loop") (let [label (fresh-label "loop")
@ -450,10 +423,9 @@
names (map #(munge-name (nth % 0)) pairs) names (map #(munge-name (nth % 0)) pairs)
;; inits evaluate in the OUTER scope (recur-target unchanged) and, like ;; inits evaluate in the OUTER scope (recur-target unchanged) and, like
;; Clojure loop/let, SEQUENTIALLY — wrap a let* around the named let. ;; Clojure loop/let, SEQUENTIALLY — wrap a let* around the named let.
inits (binding [*tail?* false] (mapv #(emit (nth % 1)) pairs)) inits (map #(emit (nth % 1)) pairs)
seq-bs (str/join " " (map (fn [n i] (str "(" n " " i ")")) names inits)) seq-bs (str/join " " (map (fn [n i] (str "(" n " " i ")")) names inits))
rebinds (str/join " " (map (fn [n] (str "(" n " " n ")")) names)) rebinds (str/join " " (map (fn [n] (str "(" n " " n ")")) names))
;; the loop body inherits the loop's tail position
body (binding [*recur-target* label] (emit (:body node)))] body (binding [*recur-target* label] (emit (:body node)))]
(str "(let* (" seq-bs ") (let " label " (" rebinds ") " body "))"))) (str "(let* (" seq-bs ") (let " label " (" rebinds ") " body "))")))
@ -514,11 +486,7 @@
params (map munge-name orig) params (map munge-name orig)
restp (when-let [r (:rest a)] (munge-name r)) restp (when-let [r (:rest a)] (munge-name r))
label (fresh-label "fnrec") label (fresh-label "fnrec")
ret (:ret-nhint a) body (binding [*recur-target* label] (emit (:body a)))
;; the body is the fn's tail position — UNLESS a ^double/^long return hint
;; wraps it in a coercion below, which puts the body back in non-tail.
body-tail? (not (or (= ret :double) (= ret :long)))
body (binding [*recur-target* label *tail?* body-tail?] (emit (:body a)))
paramlist (cond paramlist (cond
(and restp (empty? params)) restp (and restp (empty? params)) restp
restp (str "(" (str/join " " params) " . " restp ")") restp (str "(" (str/join " " params) " . " restp ")")
@ -543,16 +511,6 @@
self (when-let [nm (:name node)] (munge-name nm)) self (when-let [nm (:name node)] (munge-name nm))
clauses (binding [*known-procs* (if self (conj *known-procs* self) *known-procs*)] clauses (binding [*known-procs* (if self (conj *known-procs* self) *known-procs*)]
(mapv emit-arity-clause arities)) (mapv emit-arity-clause arities))
;; trace mode: record this frame on entry (before the body), so a frame
;; the body then tail-calls away is still in the ring at throw time. A
;; `recur` re-enters via the named-let, not the lambda, so a tight loop
;; records once, not per iteration.
clauses (if (and @trace-frames? self)
(mapv (fn [c] [(nth c 0)
(str "(begin (jolt-trace-push! " (chez-str-lit self) ") "
(nth c 1) ")")])
clauses)
clauses)
lambda (if (= 1 (count clauses)) lambda (if (= 1 (count clauses))
(let [c (first clauses)] (str "(lambda " (nth c 0) " " (nth c 1) ")")) (let [c (first clauses)] (str "(lambda " (nth c 0) " " (nth c 1) ")"))
(str "(case-lambda " (str "(case-lambda "
@ -615,30 +573,7 @@
(= (nth shape i) kw) i (= (nth shape i) kw) i
:else (recur (inc i)))))) :else (recur (inc i))))))
;; A plain Scheme application: (callee op ...).
(defn- plain-call [callee operand-strs]
(str "(" callee (if (seq operand-strs) (str " " (str/join " " operand-strs)) "") ")"))
;; A tail call in trace mode. Force-bind the operands to temps FIRST (so any
;; operand whose own evaluation records a trace entry runs before our mark), THEN
;; set the tail mark, THEN apply — the callee's entry prologue consumes the mark
;; with nothing in between, so it can't be clobbered. Still a tail call: the let*'s
;; last form is the application, so TCO is preserved.
(defn- tail-marked-call [callee operand-strs]
(let [tmps (mapv (fn [_] (fresh-label "_tt$")) operand-strs)
binds (str/join " " (map (fn [t a] (str "(" t " " a ")")) tmps operand-strs))]
(str "(let* (" binds ") (jolt-trace-mark! #t) " (plain-call callee tmps) ")")))
;; Emit a call, tail-marked when we're in tail position and tracing is on; a plain
;; application otherwise. The mark is consumed by the callee's entry prologue —
;; direct calls (:local known-proc, direct-link) always have one; a jolt-invoke
;; call usually reaches one but not always (see the best-effort note in rt.ss).
(defn- emit-call [tail? callee operand-strs]
(if (and @trace-frames? tail?)
(tail-marked-call callee operand-strs)
(plain-call callee operand-strs)))
(defn- emit-invoke [node] (defn- emit-invoke [node]
(let [tail? *tail?*] ; capture: children below emit non-tail
(binding [*tail?* false]
(let [fnode (:fn node) (let [fnode (:fn node)
arg-nodes (:args node) arg-nodes (:args node)
args (mapv emit arg-nodes) args (mapv emit arg-nodes)
@ -651,7 +586,8 @@
;; order [callee & args] together when ordering is observable. ;; order [callee & args] together when ordering is observable.
invoke (fn [] invoke (fn []
(ordered-call (cons fnode arg-nodes) (cons (emit fnode) args) (ordered-call (cons fnode arg-nodes) (cons (emit fnode) args)
(fn [operands] (emit-call tail? "jolt-invoke" operands))))] (fn [[f & as]]
(str "(jolt-invoke " f (if (seq as) (str " " (str/join " " as)) "") ")"))))]
(cond (cond
;; devirtualized protocol call: the inference proved the receiver (arg 0) is ;; devirtualized protocol call: the inference proved the receiver (arg 0) is
;; one record type, so resolve the impl by that static tag instead of routing ;; one record type, so resolve the impl by that static tag instead of routing
@ -726,7 +662,8 @@
;; holds an arbitrary IFn -> dynamic dispatch. ;; holds an arbitrary IFn -> dynamic dispatch.
(= :local (:op fnode)) (= :local (:op fnode))
(if (*known-procs* (munge-name (:name fnode))) (if (*known-procs* (munge-name (:name fnode)))
(order-args (fn [as] (emit-call tail? (munge-name (:name fnode)) as))) (order-args (fn [as] (str "(" (munge-name (:name fnode))
(if (seq as) (str " " (str/join " " as)) "") ")")))
(invoke)) (invoke))
;; closed-world direct call: the callee var is an app fn def already emitted ;; closed-world direct call: the callee var is an app fn def already emitted
;; with a Scheme binding — apply it directly, no var lookup, no jolt-invoke. ;; with a Scheme binding — apply it directly, no var lookup, no jolt-invoke.
@ -735,7 +672,8 @@
;; below (which still uses the direct binding as the invoke target). ;; below (which still uses the direct binding as the invoke target).
(and (= :var (:op fnode)) (direct-linkable? (:ns fnode) (:name fnode)) (and (= :var (:op fnode)) (direct-linkable? (:ns fnode) (:name fnode))
(direct-link-fn? (:ns fnode) (:name fnode))) (direct-link-fn? (:ns fnode) (:name fnode)))
(order-args (fn [as] (emit-call tail? (dl-name (:ns fnode) (:name fnode)) as))) (order-args (fn [as] (str "(" (dl-name (:ns fnode) (:name fnode))
(if (seq as) (str " " (str/join " " as)) "") ")")))
;; a late-bound :var call head can hold a procedure OR a non-applicable ;; a late-bound :var call head can hold a procedure OR a non-applicable
;; value the RT dispatches (multimethod, keyword/coll IFn) — route via ;; value the RT dispatches (multimethod, keyword/coll IFn) — route via
;; jolt-invoke (transparent for a procedure). ;; jolt-invoke (transparent for a procedure).
@ -743,7 +681,7 @@
(invoke) (invoke)
;; a computed callee can yield ANY IFn — route through jolt-invoke. ;; a computed callee can yield ANY IFn — route through jolt-invoke.
:else :else
(invoke)))))) (invoke))))
;; try/catch/finally. throw raises a Chez condition wrapping the jolt value ;; try/catch/finally. throw raises a Chez condition wrapping the jolt value
;; (jolt-throw = Scheme `raise` of a &jolt-throw condition); catch lowers to ;; (jolt-throw = Scheme `raise` of a &jolt-throw condition); catch lowers to
@ -790,22 +728,7 @@
(returns-scheme-bool? (:body node) bools')) (returns-scheme-bool? (:body node) bools'))
:else false))) :else false)))
;; In trace mode, a fn def also registers its source so the tail-frame history maps (defn emit [node]
;; the recorded frame-name to "ns/name (file:line)" instead of a bare name. Keyed by
;; the SAME munged name the entry push records (emit-fn's letrec self-binding = the
;; fn's own name). Returns "" when off / not a positioned fn def, so trace-off output
;; (seed mint, `jolt build`) is byte-identical. Direct-link builds already register
;; via emit-def-cached; this covers the open-world eval path.
(defn- trace-source-reg [node]
(let [init (:init node) pos (:pos node)]
(if (and @trace-frames? (= :fn (:op init)) (:name init) pos)
(str " (jolt-register-source! " (chez-str-lit (munge-name (:name init))) " "
(chez-str-lit (:ns node)) " " (chez-str-lit (:name node)) " "
(if (:file pos) (chez-str-lit (:file pos)) "jolt-nil") " "
(or (:line pos) 0) ")")
"")))
(defn emit* [node]
(case (:op node) (case (:op node)
:const (emit-const (:val node)) :const (emit-const (:val node))
:local (munge-name (:name node)) :local (munge-name (:name node))
@ -853,14 +776,11 @@
:host-new (str "(host-new " (chez-str-lit (:class node)) :host-new (str "(host-new " (chez-str-lit (:class node))
(let [args (map emit (:args node))] (let [args (map emit (:args node))]
(if (empty? args) "" (str " " (str/join " " args)))) ")") (if (empty? args) "" (str " " (str/join " " args)))) ")")
;; the test is non-tail; then/else inherit the if's tail position
:if (let [test (:test node) :if (let [test (:test node)
t (binding [*tail?* false] t (if (returns-scheme-bool? test) (emit test)
(if (returns-scheme-bool? test) (emit test) (str "(jolt-truthy? " (emit test) ")"))]
(str "(jolt-truthy? " (emit test) ")")))]
(str "(if " t " " (emit (:then node)) " " (emit (:else node)) ")")) (str "(if " t " " (emit (:then node)) " " (emit (:else node)) ")"))
;; non-last statements are non-tail; the ret inherits the do's tail position :do (str "(begin " (str/join " " (map emit (:statements node)))
:do (str "(begin " (binding [*tail?* false] (str/join " " (mapv emit (:statements node))))
(if (empty? (:statements node)) "" " ") (emit (:ret node)) ")") (if (empty? (:statements node)) "" " ") (emit (:ret node)) ")")
:invoke (emit-invoke node) :invoke (emit-invoke node)
;; collection literals -> rt constructors (collections.ss). Elements are ;; collection literals -> rt constructors (collections.ss). Elements are
@ -904,8 +824,7 @@
:fn (emit-fn node) :fn (emit-fn node)
;; (def name) with no init (declare): reserve the cell. A def with non-empty ;; (def name) with no init (declare): reserve the cell. A def with non-empty
;; reader metadata lowers to def-var-with-meta! (ported in a later increment). ;; reader metadata lowers to def-var-with-meta! (ported in a later increment).
:def (let [reg (trace-source-reg node) :def (cond
d (cond
(:no-init node) (:no-init node)
(str "(declare-var! " (chez-str-lit (:ns node)) " " (chez-str-lit (:name node)) ")") (str "(declare-var! " (chez-str-lit (:ns node)) " " (chez-str-lit (:name node)) ")")
(jmeta-nonempty? (:meta node)) (jmeta-nonempty? (:meta node))
@ -913,8 +832,7 @@
(emit-with-cells #(emit (:init node))) " " (emit-def-meta node) ")") (emit-with-cells #(emit (:init node))) " " (emit-def-meta node) ")")
:else :else
(str "(def-var! " (chez-str-lit (:ns node)) " " (chez-str-lit (:name node)) " " (str "(def-var! " (chez-str-lit (:ns node)) " " (chez-str-lit (:name node)) " "
(emit-with-cells #(emit (:init node))) ")"))] (emit-with-cells #(emit (:init node))) ")"))
(if (= reg "") d (str "(begin " d reg ")")))
(throw (ex-info (str "emit: op not yet ported / unhandled: " (pr-str (:op node))) {})))) (throw (ex-info (str "emit: op not yet ported / unhandled: " (pr-str (:op node))) {}))))
;; ^:dynamic / ^:redef on a def opts it out of direct-linking: it stays redefinable, ;; ^:dynamic / ^:redef on a def opts it out of direct-linking: it stays redefinable,

View file

@ -66,10 +66,6 @@
(if-let [root (:deps/root spec)] (str checkout "/" root) checkout)) (if-let [root (:deps/root spec)] (str checkout "/" root) checkout))
(:jolt/module spec) (:jolt/module spec)
(do (warn "skipping janet dependency " coord " (:jolt/module is obsolete on Chez)") nil) (do (warn "skipping janet dependency " coord " (:jolt/module is obsolete on Chez)") nil)
;; jolt IS Clojure — a dependency on org.clojure/clojure is satisfied
;; intrinsically, so skip it silently rather than warning about the (unusable)
;; :mvn/version coordinate.
(= coord 'org.clojure/clojure) nil
:else :else
(do (warn "skipping unsupported coordinate " coord " " (pr-str spec)) nil))) (do (warn "skipping unsupported coordinate " coord " " (pr-str spec)) nil)))

View file

@ -8,8 +8,6 @@
(defn- project-dir [] (or (jolt.host/getenv "JOLT_PWD") ".")) (defn- project-dir [] (or (jolt.host/getenv "JOLT_PWD") "."))
(defn- version [] (jolt.host/jolt-version))
(defn- current-platform [] (defn- current-platform []
(let [os (str/lower-case (or (System/getProperty "os.name") ""))] (let [os (str/lower-case (or (System/getProperty "os.name") ""))]
(cond (str/includes? os "mac") :darwin (cond (str/includes? os "mac") :darwin
@ -147,10 +145,7 @@
;; loaded — same context a run gets, so (require '[some.lib]) works in the REPL. ;; loaded — same context a run gets, so (require '[some.lib]) works in the REPL.
(try (apply-project! (deps/resolve-project (project-dir))) (try (apply-project! (deps/resolve-project (project-dir)))
(catch :default _ nil)) (catch :default _ nil))
;; REPL-driven development: trace by default so an uncaught error in evaluated (println ";; jolt repl — :repl/quit or ^D to exit")
;; code shows a tail-frame backtrace, no JOLT_TRACE needed (JOLT_TRACE=0 opts out).
(jolt.host/enable-trace!)
(println (str ";; jolt " (version) " repl — :repl/quit or ^D to exit"))
(loop [] (loop []
(let [form (repl-read-form)] (let [form (repl-read-form)]
(when form (when form
@ -163,9 +158,7 @@
(catch :default e (catch :default e
(println "error:" (or (ex-message e) (println "error:" (or (ex-message e)
(try ((resolve 'jolt.host/condition-message) e) (catch :default _ nil)) (try ((resolve 'jolt.host/condition-message) e) (catch :default _ nil))
(pr-str e))) (pr-str e)))))
(when-let [bt (jolt.host/backtrace-string)]
(print bt))))
(recur))))))) (recur)))))))
;; A deps.edn :tasks entry: a string is a shell command; a map is {:main-opts …}. ;; A deps.edn :tasks entry: a string is a shell command; a map is {:main-opts …}.
@ -301,9 +294,7 @@
(when stop (stop)))))) (when stop (stop))))))
(defn- usage [] (defn- usage []
(println (str "jolt " (version)))
(println "usage: jolt <command> [args]") (println "usage: jolt <command> [args]")
(println " -e EXPR evaluate EXPR and print the result")
(println " run -m NS [args] resolve deps.edn, load NS, call its -main") (println " run -m NS [args] resolve deps.edn, load NS, call its -main")
(println " run FILE load a Clojure file") (println " run FILE load a Clojure file")
(println " build -m NS [-o OUT] [--opt|--dev] [--direct-link] [--tree-shake] [--dynamic] compile a standalone binary") (println " build -m NS [-o OUT] [--opt|--dev] [--direct-link] [--tree-shake] [--dynamic] compile a standalone binary")
@ -313,7 +304,6 @@
(println " --nrepl-server [port] start an nREPL server (default 7888) for editors") (println " --nrepl-server [port] start an nREPL server (default 7888) for editors")
(println " path print the resolved source roots") (println " path print the resolved source roots")
(println " <task> run a deps.edn :tasks entry") (println " <task> run a deps.edn :tasks entry")
(println " --version print the jolt version")
(println " --help print this message")) (println " --help print this message"))
(defn -main [& args] (defn -main [& args]
@ -322,7 +312,6 @@
(nil? cmd) (usage) (nil? cmd) (usage)
(= cmd "--help") (usage) (= cmd "--help") (usage)
(= cmd "-h") (usage) (= cmd "-h") (usage)
(#{"--version" "-V"} cmd) (println (str "jolt " (version)))
(= cmd "run") (cmd-run more) (= cmd "run") (cmd-run more)
(= cmd "repl") (repl) (= cmd "repl") (repl)
(= cmd "--nrepl-server") (nrepl more) (= cmd "--nrepl-server") (nrepl more)

View file

@ -21,66 +21,25 @@
[jolt.ffi :as ffi])) [jolt.ffi :as ffi]))
;; --- sockets (loopback server) --------------------------------------------- ;; --- sockets (loopback server) ---------------------------------------------
(def ^:private os-name ;; Load libc (the running process's symbols) BEFORE the foreign-fn bindings below
(str/lower-case (or (System/getProperty "os.name") ""))) ;; — defcfn resolves the C entry point when the def is evaluated (at ns load), so
(def ^:private macos? (str/includes? os-name "mac")) ;; the socket symbols must already be available.
(def ^:private windows? (str/includes? os-name "win")) (ffi/load-library)
;; Load the library that provides the socket symbols BEFORE the foreign-fn
;; bindings below — defcfn resolves the C entry point when the def is evaluated
;; (at ns load), so the symbols must already be available. POSIX: the running
;; process's own libc symbols. Windows: the Winsock DLL (ws2_32), whose symbols
;; are NOT in joltc.exe's export table even though it's linked in — without this
;; explicit load, (ffi/defcfn c-socket "socket" ...) fails at load with
;; "no entry for socket".
(if windows?
(ffi/load-library "ws2_32.dll")
(ffi/load-library))
;; A socket is an int fd on POSIX; on Win64 it's a SOCKET (uintptr_t) handle, but
;; those are small kernel handle values that round-trip through :int, and the
;; INVALID_SOCKET error sentinel (~0) reads back as -1 — so the fd checks below
;; work unchanged on both.
(ffi/defcfn c-socket "socket" [:int :int :int] :int) (ffi/defcfn c-socket "socket" [:int :int :int] :int)
(ffi/defcfn c-bind "bind" [:int :pointer :int] :int) (ffi/defcfn c-bind "bind" [:int :pointer :int] :int)
(ffi/defcfn c-listen "listen" [:int :int] :int) (ffi/defcfn c-listen "listen" [:int :int] :int)
(ffi/defcfn c-setsockopt "setsockopt" [:int :int :int :pointer :int] :int) (ffi/defcfn c-setsockopt "setsockopt" [:int :int :int :pointer :int] :int)
(ffi/defcfn c-accept "accept" [:int :pointer :pointer] :int :blocking) (ffi/defcfn c-accept "accept" [:int :pointer :pointer] :int :blocking)
;; recv/send and the socket-close call differ by platform. Winsock's recv/send
;; take an int length and return int (not ssize_t), and a socket is closed with
;; closesocket, not close. A symbol that exists on only one OS (closesocket on
;; Windows, close on POSIX) can only be bound there, so these live in the taken
;; platform branch — jolt interns the vars from both branches at analysis time,
;; so later references resolve either way.
(if windows?
(do
(ffi/defcfn c-recv "recv" [:int :pointer :int :int] :int :blocking)
(ffi/defcfn c-send "send" [:int :pointer :int :int] :int :blocking)
(ffi/defcfn c-close "closesocket" [:int] :int)
;; Winsock must be initialized once per process before any socket call.
(ffi/defcfn c-wsastartup "WSAStartup" [:int :pointer] :int))
(do
(ffi/defcfn c-recv "recv" [:int :pointer :size_t :int] :ssize_t :blocking) (ffi/defcfn c-recv "recv" [:int :pointer :size_t :int] :ssize_t :blocking)
(ffi/defcfn c-send "send" [:int :pointer :size_t :int] :ssize_t :blocking) (ffi/defcfn c-send "send" [:int :pointer :size_t :int] :ssize_t :blocking)
(ffi/defcfn c-close "close" [:int] :int))) (ffi/defcfn c-close "close" [:int] :int)
(def ^:private AF-INET 2) (def ^:private AF-INET 2)
(def ^:private SOCK-STREAM 1) (def ^:private SOCK-STREAM 1)
;; SOL_SOCKET / SO_REUSEADDR: 0xffff / 4 on macOS and Windows, 1 / 2 on Linux. (def ^:private macos?
(def ^:private sol-socket (if (or macos? windows?) 0xffff 1)) (str/includes? (str/lower-case (or (System/getProperty "os.name") "")) "mac"))
(def ^:private so-reuse (if (or macos? windows?) 4 2)) (def ^:private sol-socket (if macos? 0xffff 1))
(def ^:private so-reuse (if macos? 4 2))
;; Initialize Winsock (a no-op off Windows). WSAStartup is refcounted and must
;; precede any socket call; WSADATA is ~408 bytes on x64, so 512 is ample.
(defn- ensure-winsock! []
(when windows?
(let [wsadata (ffi/alloc 512)]
(try
(let [r (c-wsastartup 0x0202 wsadata)]
(when-not (zero? r)
(throw (ex-info (str "WSAStartup failed: " r) {}))))
(finally (ffi/free wsadata))))))
(defn- make-sockaddr [port] (defn- make-sockaddr [port]
(let [sa (ffi/alloc 16)] (let [sa (ffi/alloc 16)]
@ -94,7 +53,7 @@
sa)) sa))
(defn- listen-socket [port] (defn- listen-socket [port]
(ensure-winsock!) ; no-op off Windows (ffi/load-library) ; libc process symbols
(let [fd (c-socket AF-INET SOCK-STREAM 0)] (let [fd (c-socket AF-INET SOCK-STREAM 0)]
(when (neg? fd) (throw (ex-info "socket() failed" {}))) (when (neg? fd) (throw (ex-info "socket() failed" {})))
(let [opt (ffi/alloc 4)] (ffi/write opt :int 0 1) (c-setsockopt fd sol-socket so-reuse opt 4) (ffi/free opt)) (let [opt (ffi/alloc 4)] (ffi/write opt :int 0 1) (c-setsockopt fd sol-socket so-reuse opt 4) (ffi/free opt))
@ -188,10 +147,7 @@
(try (when (and ns-str (not (str/blank? ns-str)) (find-ns (symbol ns-str))) (try (when (and ns-str (not (str/blank? ns-str)) (find-ns (symbol ns-str)))
(in-ns (symbol ns-str))) (in-ns (symbol ns-str)))
(reset! result (load-string code)) (reset! result (load-string code))
(catch :default e (catch :default e (reset! err (err-msg e)))))]
(reset! err (str (err-msg e)
(when-let [bt (jolt.host/backtrace-string)]
(str "\n" bt)))))))]
{:value (when (nil? @err) (pr-str @result)) {:value (when (nil? @err) (pr-str @result))
:out out :out out
:ns (str (ns-name *ns*)) :ns (str (ns-name *ns*))
@ -280,17 +236,11 @@
no-op." no-op."
([port] (start port nil)) ([port] (start port nil))
([port middleware] ([port middleware]
;; An nREPL session is REPL-driven development: trace by default so an uncaught
;; error in code evaluated over the connection shows a tail-frame backtrace, with
;; no JOLT_TRACE needed. Covers both `--nrepl-server` and an app that starts its
;; own server under `-M:run` (reload a namespace to trace already-loaded code).
(jolt.host/enable-trace!)
(let [handler (build-handler (resolve-middleware (or middleware []))) (let [handler (build-handler (resolve-middleware (or middleware [])))
fd (listen-socket port) ; throws on bind/listen failure fd (listen-socket port) ; throws on bind/listen failure
stopped (atom false)] stopped (atom false)]
(try (spit ".nrepl-port" (str port)) (catch :default _ nil)) (try (spit ".nrepl-port" (str port)) (catch :default _ nil))
(println (str "jolt " (jolt.host/jolt-version) " nREPL server started on port " (println (str "nREPL server started on port " port " (127.0.0.1) — .nrepl-port written"))
port " (127.0.0.1) — .nrepl-port written"))
(when (seq middleware) (println (str ";; middleware: " (str/join " " middleware)))) (when (seq middleware) (println (str ";; middleware: " (str/join " " middleware))))
(println ";; connect your editor; ^C to stop") (println ";; connect your editor; ^C to stop")
(future (future

View file

@ -576,26 +576,4 @@
{:suite "deftype-method" :expr "(do (defprotocol P (m [_ o])) (defrecord R [n] P (m [_ _] n)) (m (->R 5) :x))" :expected "5"} {:suite "deftype-method" :expr "(do (defprotocol P (m [_ o])) (defrecord R [n] P (m [_ _] n)) (m (->R 5) :x))" :expected "5"}
{:suite "deftype-method" :expr "(do (defprotocol P2 (m2 [_ o])) (deftype T [n] P2 (m2 [_ _] n)) (m2 (->T 7) :x))" :expected "7"} {:suite "deftype-method" :expr "(do (defprotocol P2 (m2 [_ o])) (deftype T [n] P2 (m2 [_ _] n)) (m2 (->T 7) :x))" :expected "7"}
{:suite "protocol-host" :expr "(do (defprotocol Q (e [_])) (extend-protocol Q clojure.lang.Var (e [_] :var)) [(satisfies? Q (var map)) (e (var map))])" :expected "[true :var]"} {:suite "protocol-host" :expr "(do (defprotocol Q (e [_])) (extend-protocol Q clojure.lang.Var (e [_] :var)) [(satisfies? Q (var map)) (e (var map))])" :expected "[true :var]"}
;; Clojure 1.13 (1.13.0-alpha1) parity. Ahead of the JVM 1.12.5 the corpus
;; certifies against, so these live here rather than as certified corpus rows.
{:suite "clj-1.13 req!" :expr "(req! {:a 1} :a)" :expected "1"}
{:suite "clj-1.13 req!" :expr "(req! [10 20 30] 1)" :expected "20"}
{:suite "clj-1.13 req!" :expr "(nil? (req! {:a nil} :a))" :expected "true"}
{:suite "clj-1.13 req!" :expr "(req! {:a 1} :b)" :expected :throws}
{:suite "clj-1.13 amp-binding" :expr "(let [& 42] &)" :expected :throws}
{:suite "clj-1.13 amp-binding" :expr "(loop [& 42] &)" :expected :throws}
{:suite "clj-1.13 checked-keys" :expr "(let [{:keys! [a b]} {:a 1 :b 2}] [a b])" :expected "[1 2]"}
{:suite "clj-1.13 checked-keys" :expr "(let [{:keys! [a b]} {:a 1}] [a b])" :expected :throws}
{:suite "clj-1.13 checked-keys" :expr "(nil? (let [{:keys! [a]} {:a nil}] a))" :expected "true"}
{:suite "clj-1.13 checked-keys" :expr "(let [{:strs! [a]} {\"a\" 5}] a)" :expected "5"}
{:suite "clj-1.13 checked-keys" :expr "(let [{:keys! [a & b]} {:a 1 :b 2}] a)" :expected "1"}
{:suite "clj-1.13 checked-keys" :expr "(let [{:keys! [a & b]} {:a 1}] a)" :expected :throws}
{:suite "clj-1.13 checked-keys" :expr "(let [{:keys [a & b]} {:a 1 :b 2}] a)" :expected "1"}
{:suite "clj-1.13 checked-keys" :expr "(let [{:foo/keys! [a]} {:foo/a 7}] a)" :expected "7"}
{:suite "clj-1.13 checked-keys" :expr "(let [{:foo/keys! [a]} {}] a)" :expected :throws}
{:suite "clj-1.13 array-map-64" :expr "(keys {:a 1 :b 2 :c 3 :d 4 :e 5 :f 6 :g 7 :h 8 :i 9 :j 10})" :expected "(:a :b :c :d :e :f :g :h :i :j)"}
{:suite "clj-1.13 array-map-64" :expr "(= (map (fn [i] (keyword (str \"k\" i))) (range 20)) (keys (into {} (map (fn [i] [(keyword (str \"k\" i)) i]) (range 20)))))" :expected "true"}
{:suite "clj-1.13 array-map-64" :expr "(= (map (fn [i] (keyword (str \"k\" i))) (range 64)) (keys (reduce (fn [m i] (assoc m (keyword (str \"k\" i)) i)) {} (range 64))))" :expected "true"}
{:suite "clj-1.13 array-map-64" :expr "(= (map (fn [i] (keyword (str \"k\" i))) (range 65)) (keys (into {} (map (fn [i] [(keyword (str \"k\" i)) i]) (range 65)))))" :expected "false"}
] ]