Compare commits
98 commits
proper-chu
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0598f8e7c5 | |||
|
|
855fbc4794 | ||
|
|
6c88198115 | ||
|
|
e297a74501 | ||
|
|
c8167e1c05 | ||
|
|
bff1c288b0 | ||
|
|
94d3bcca20 | ||
|
|
79002526bb | ||
|
|
7167af4830 | ||
|
|
a3e2365217 | ||
|
|
773e647b4a | ||
|
|
dbc5afac32 | ||
|
|
403c3f302f | ||
|
|
1ed9656a0c | ||
|
|
f9fcbc37fd | ||
|
|
4398e2cf6c | ||
|
|
5467c1d98d | ||
|
|
dbc4298c0a | ||
|
|
af12f77dcd | ||
|
|
225073a11b | ||
|
|
9382c67e48 | ||
|
|
24c2280246 | ||
|
|
0afd2095e3 | ||
|
|
c63a18aae1 | ||
|
|
80f3206a6e | ||
|
|
7add315394 | ||
|
|
5ca7437826 | ||
|
|
a67dbdb93d | ||
|
|
ab10e68218 | ||
|
|
b2aa757af2 | ||
|
|
58ef0c8fa1 | ||
|
|
d6d11d5748 | ||
|
|
7b4369145d | ||
|
|
ce8e89ca86 | ||
|
|
3fb8082802 | ||
|
|
44d4875a24 | ||
|
|
95186a6782 | ||
|
|
e17bcfd0af | ||
|
|
a9542077fc | ||
|
|
d0e1a11934 | ||
|
|
2610cb3ac3 | ||
|
|
f80f9aab4b | ||
|
|
257f822825 | ||
|
|
6e333b3020 | ||
|
|
7e1df2c600 | ||
|
|
e66a91750e | ||
|
|
38abc1be84 | ||
|
|
86e36e8bee | ||
|
|
53a8aac2d0 | ||
|
|
edfd67a322 | ||
|
|
8ffc2f68c5 | ||
|
|
51dec5fd2c | ||
|
|
eb768b13c1 | ||
|
|
802fb29b07 | ||
|
|
db08ecc1bc | ||
|
|
f6bd2c6de5 | ||
|
|
b460875772 | ||
|
|
b5998f4b4a | ||
|
|
21445375fa | ||
|
|
6500f968ce | ||
|
|
a59a32a0b0 | ||
|
|
bd089f0845 | ||
|
|
b4d9eaa527 | ||
|
|
f856c16f06 | ||
|
|
01f98c2e89 | ||
|
|
0b07b376bb | ||
|
|
d4acd69a73 | ||
|
|
d7dad2b450 | ||
|
|
20d88324f4 | ||
|
|
cb03e36088 | ||
|
|
9bcac13fd2 | ||
|
|
53112d06fb | ||
|
|
7c4f9bb974 | ||
|
|
e2d842b073 | ||
|
|
77e80dab9c | ||
|
|
0bad467372 | ||
|
|
908ad63caa | ||
|
|
1008e922d8 | ||
|
|
d79ad6dc6a | ||
|
|
a2e99fff45 | ||
|
|
e4cbbb8912 | ||
|
|
4889505204 | ||
|
|
f625099ddf | ||
|
|
4a1dec277e | ||
|
|
240458d994 | ||
|
|
a58bca3bee | ||
|
|
8c7553fe55 | ||
|
|
46c9c7b4d9 | ||
|
|
8c2bd60257 | ||
|
|
7275eb54a5 | ||
|
|
649e33fe3b | ||
|
|
9e53ba4248 | ||
|
|
bbca8bc0de | ||
|
|
12058d2dcf | ||
|
|
1481a806b7 | ||
|
|
d77b4e6420 | ||
|
|
3d0cbed3c5 | ||
|
|
7f163faf2e |
96 changed files with 9202 additions and 5054 deletions
131
.github/workflows/release.yml
vendored
131
.github/workflows/release.yml
vendored
|
|
@ -12,6 +12,7 @@ on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
|
workflow_dispatch: {} # dry-run the build matrix without tagging
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write # create/update the GitHub Release and upload assets
|
contents: write # create/update the GitHub Release and upload assets
|
||||||
|
|
@ -26,10 +27,18 @@ jobs:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
target: x86_64-linux
|
target: x86_64-linux
|
||||||
- os: macos-13
|
shell: bash
|
||||||
target: x86_64-macos
|
# No x86_64-macos: GitHub is retiring the macos-13 Intel runner (jobs
|
||||||
|
# queue forever). Intel Macs build from source. macos-14 is arm64.
|
||||||
- os: macos-14
|
- os: macos-14
|
||||||
target: aarch64-macos
|
target: aarch64-macos
|
||||||
|
shell: bash
|
||||||
|
- os: windows-latest
|
||||||
|
target: x86_64-windows
|
||||||
|
shell: msys2 {0}
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: ${{ matrix.shell }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
|
|
@ -78,6 +87,65 @@ jobs:
|
||||||
if: runner.os == 'macOS'
|
if: runner.os == 'macOS'
|
||||||
run: brew install chezscheme lz4
|
run: brew install chezscheme lz4
|
||||||
|
|
||||||
|
# --- Windows: MSYS2/MinGW-w64 toolchain + Chez built from source (ta6nt).
|
||||||
|
# The whole job runs in the msys2 shell so cc/xxd/paths behave; the
|
||||||
|
# produced joltc.exe is a plain Windows binary (no MSYS runtime dep). ---
|
||||||
|
- name: Set up MSYS2 (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
uses: msys2/setup-msys2@v2
|
||||||
|
with:
|
||||||
|
msystem: MINGW64
|
||||||
|
update: false
|
||||||
|
# inherit the runner PATH so GITHUB_PATH additions (the chez wrapper
|
||||||
|
# dir) are visible inside the msys2 shell
|
||||||
|
path-type: inherit
|
||||||
|
install: >-
|
||||||
|
git make vim unzip zip
|
||||||
|
mingw-w64-x86_64-gcc
|
||||||
|
mingw-w64-x86_64-lz4
|
||||||
|
mingw-w64-x86_64-zlib
|
||||||
|
mingw-w64-x86_64-ntldd
|
||||||
|
|
||||||
|
- name: Cache Chez Scheme (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
id: cache-chez-win
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: chez-install
|
||||||
|
key: chez-${{ runner.os }}-v10.4.1-mingw64
|
||||||
|
|
||||||
|
- name: Build Chez Scheme from source (Windows)
|
||||||
|
if: runner.os == 'Windows' && steps.cache-chez-win.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
git clone --depth 1 --branch v10.4.1 https://github.com/cisco/ChezScheme.git /tmp/chez-src
|
||||||
|
cd /tmp/chez-src
|
||||||
|
./configure --threads
|
||||||
|
make -j"$(nproc)"
|
||||||
|
# `make install` drives the unix installsh through cmd and dies; the
|
||||||
|
# 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)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
run: |
|
||||||
|
bindir="$GITHUB_WORKSPACE/chez-install/bin"
|
||||||
|
{ echo '#!/bin/sh'; echo "exec \"$bindir/scheme.exe\" \"\$@\""; } > "$bindir/chez"
|
||||||
|
chmod +x "$bindir/chez"
|
||||||
|
echo "$bindir" >> "$GITHUB_PATH"
|
||||||
|
echo "JOLT_CHEZ_CSV=$GITHUB_WORKSPACE/chez-install/csv" >> "$GITHUB_ENV"
|
||||||
|
# cc is the build's compiler name; alias it to mingw gcc
|
||||||
|
{ echo '#!/bin/sh'; echo 'exec gcc "$@"'; } > "$bindir/cc"
|
||||||
|
chmod +x "$bindir/cc"
|
||||||
|
|
||||||
- name: Show Chez version
|
- name: Show Chez version
|
||||||
run: chez --version
|
run: chez --version
|
||||||
|
|
||||||
|
|
@ -86,6 +154,19 @@ 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
|
||||||
|
|
@ -93,21 +174,63 @@ jobs:
|
||||||
out="$(./target/release/joltc -e '(reduce + (range 10))')"
|
out="$(./target/release/joltc -e '(reduce + (range 10))')"
|
||||||
test "$out" = "45" || { echo "joltc -e gave '$out', want 45"; exit 1; }
|
test "$out" = "45" || { echo "joltc -e gave '$out', want 45"; exit 1; }
|
||||||
|
|
||||||
|
# The binary is a self-contained COMPILER: it must `build` an app with no
|
||||||
|
# jolt source on disk. Run from an isolated dir (nothing but the tiny app)
|
||||||
|
# so a build that reaches for host/chez/*.ss on the filesystem fails here,
|
||||||
|
# not on a user's machine.
|
||||||
|
- name: Smoke a self-contained build
|
||||||
|
run: |
|
||||||
|
joltc="$(pwd)/target/release/joltc"
|
||||||
|
work="$(mktemp -d)"
|
||||||
|
mkdir -p "$work/app/src/app"
|
||||||
|
printf '{:paths ["src"]}\n' > "$work/app/deps.edn"
|
||||||
|
printf '(ns app.core)\n(defn -main [& _] (println "built:" (reduce + (range 10))))\n' \
|
||||||
|
> "$work/app/src/app/core.clj"
|
||||||
|
( cd "$work/app" && "$joltc" build -m app.core -o app )
|
||||||
|
out="$("$work/app/app")"
|
||||||
|
test "$out" = "built: 45" || { echo "self-contained build ran '$out', want 'built: 45'"; exit 1; }
|
||||||
|
|
||||||
|
# A built binary must also run the DYNAMIC require path: a namespace not
|
||||||
|
# in the static ns graph compiles from the source roots at runtime, so the
|
||||||
|
# boot's top-level defines must be visible to the runtime compiler's eval
|
||||||
|
# (issue #290: this died with "variable var-deref is not bound").
|
||||||
|
- name: Smoke a runtime require in a built binary
|
||||||
|
run: |
|
||||||
|
joltc="$(pwd)/target/release/joltc"
|
||||||
|
work="$(mktemp -d)"
|
||||||
|
mkdir -p "$work/app/src/app"
|
||||||
|
printf '{:paths ["src"]}\n' > "$work/app/deps.edn"
|
||||||
|
printf '(ns app.extra)\n(defn greet [s] (str "Hello, " s "!"))\n' \
|
||||||
|
> "$work/app/src/app/extra.clj"
|
||||||
|
printf '(ns app.core)\n(defn -main [& _]\n (println ((requiring-resolve (quote app.extra/greet)) "runtime")))\n' \
|
||||||
|
> "$work/app/src/app/core.clj"
|
||||||
|
( cd "$work/app" && "$joltc" build -m app.core -o app )
|
||||||
|
out="$(cd "$work/app" && ./app)"
|
||||||
|
test "$out" = "Hello, runtime!" || { echo "runtime require ran '$out', want 'Hello, runtime!'"; exit 1; }
|
||||||
|
|
||||||
- name: Package
|
- name: Package
|
||||||
run: |
|
run: |
|
||||||
ver="${GITHUB_REF_NAME}"
|
ver="${GITHUB_REF_NAME}"
|
||||||
name="joltc-${ver}-${{ matrix.target }}"
|
name="joltc-${ver}-${{ matrix.target }}"
|
||||||
mkdir -p "dist/${name}"
|
mkdir -p "dist/${name}"
|
||||||
cp target/release/joltc "dist/${name}/joltc"
|
|
||||||
cp README.md LICENSE "dist/${name}/"
|
cp README.md LICENSE "dist/${name}/"
|
||||||
|
if [ "${{ runner.os }}" = "Windows" ]; then
|
||||||
|
cp target/release/joltc.exe "dist/${name}/joltc.exe"
|
||||||
|
( cd dist && zip -r "${name}.zip" "${name}" && sha256sum "${name}.zip" > "${name}.zip.sha256" )
|
||||||
|
else
|
||||||
|
cp target/release/joltc "dist/${name}/joltc"
|
||||||
tar -C dist -czf "dist/${name}.tar.gz" "${name}"
|
tar -C dist -czf "dist/${name}.tar.gz" "${name}"
|
||||||
( cd dist && shasum -a 256 "${name}.tar.gz" > "${name}.tar.gz.sha256" )
|
( cd dist && shasum -a 256 "${name}.tar.gz" > "${name}.tar.gz.sha256" )
|
||||||
|
fi
|
||||||
ls -la dist
|
ls -la dist
|
||||||
|
|
||||||
- name: Upload to the GitHub Release
|
- name: Upload to the GitHub Release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
dist/*.tar.gz
|
dist/*.tar.gz
|
||||||
dist/*.tar.gz.sha256
|
dist/*.tar.gz.sha256
|
||||||
fail_on_unmatched_files: true
|
dist/*.zip
|
||||||
|
dist/*.zip.sha256
|
||||||
|
fail_on_unmatched_files: false
|
||||||
|
|
|
||||||
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
|
|
@ -56,7 +56,11 @@ jobs:
|
||||||
- name: Install JDK + Clojure (certify oracle)
|
- name: Install JDK + Clojure (certify oracle)
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install -y default-jdk rlwrap
|
sudo apt-get install -y default-jdk rlwrap
|
||||||
curl -L -O https://github.com/clojure/brew-install/releases/latest/download/linux-install.sh
|
# --retry + --fail so a transient CDN error retries instead of handing
|
||||||
|
# bash an HTML error page (a 2min timeout page flaked a run)
|
||||||
|
curl --fail --retry 5 --retry-delay 10 --retry-all-errors -L -O \
|
||||||
|
https://github.com/clojure/brew-install/releases/latest/download/linux-install.sh
|
||||||
|
head -1 linux-install.sh | grep -q '^#!' || { echo "installer download corrupt"; cat linux-install.sh | head -5; exit 1; }
|
||||||
sudo bash linux-install.sh
|
sudo bash linux-install.sh
|
||||||
clojure --version
|
clojure --version
|
||||||
|
|
||||||
|
|
|
||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -4,3 +4,6 @@
|
||||||
[submodule "vendor/sci"]
|
[submodule "vendor/sci"]
|
||||||
path = vendor/sci
|
path = vendor/sci
|
||||||
url = https://github.com/borkdude/sci.git
|
url = https://github.com/borkdude/sci.git
|
||||||
|
[submodule "vendor/clojure-test-suite"]
|
||||||
|
path = vendor/clojure-test-suite
|
||||||
|
url = https://github.com/jank-lang/clojure-test-suite.git
|
||||||
|
|
|
||||||
22
Makefile
22
Makefile
|
|
@ -4,18 +4,23 @@
|
||||||
# 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 selfhost sci certify ffi transient infer wp devirt fieldread numwp fieldnum protoret narrow directlink numeric inline shakesmoke remint joltc joltc-release joltc-debug joltcsmoke
|
.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
|
||||||
|
|
||||||
|
# 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: selfhost ci
|
test: submodules 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: values corpus unit smoke buildsmoke sci ffi transient infer wp devirt fieldread numwp fieldnum protoret narrow directlink numeric inline certify
|
ci: submodules 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.
|
||||||
|
|
@ -42,6 +47,11 @@ smoke:
|
||||||
buildsmoke:
|
buildsmoke:
|
||||||
@sh host/chez/build-smoke.sh
|
@sh host/chez/build-smoke.sh
|
||||||
|
|
||||||
|
# `jolt build` cc-links a :jolt/native :static archive into the binary (the
|
||||||
|
# default), and --dynamic keeps the runtime load-shared-object path.
|
||||||
|
staticnativesmoke:
|
||||||
|
@sh host/chez/static-native-smoke.sh
|
||||||
|
|
||||||
# Build joltc as a self-contained native binary into target/<profile>/joltc. The
|
# Build joltc as a self-contained native binary into target/<profile>/joltc. The
|
||||||
# binary bundles the runtime, compiler, jolt-core + stdlib source, the Chez boots,
|
# binary bundles the runtime, compiler, jolt-core + stdlib source, the Chez boots,
|
||||||
# and a launcher stub, so it runs AND compiles jolt apps with no Chez or cc on the
|
# and a launcher stub, so it runs AND compiles jolt apps with no Chez or cc on the
|
||||||
|
|
@ -63,6 +73,12 @@ joltcsmoke:
|
||||||
sci:
|
sci:
|
||||||
@chez --script host/chez/run-sci.ss
|
@chez --script host/chez/run-sci.ss
|
||||||
|
|
||||||
|
# clojure-test-suite conformance: run the vendored jank-lang/clojure-test-suite
|
||||||
|
# per-namespace under joltc, gated on the per-namespace baseline
|
||||||
|
# (test/chez/cts-known-failures.txt).
|
||||||
|
cts:
|
||||||
|
@bash host/chez/cts.sh
|
||||||
|
|
||||||
# FFI: bind native functions (typed foreign-procedure), memory, and that a
|
# FFI: bind native functions (typed foreign-procedure), memory, and that a
|
||||||
# :blocking call is collect-safe (a parked thread doesn't pin the collector).
|
# :blocking call is collect-safe (a parked thread doesn't pin the collector).
|
||||||
ffi:
|
ffi:
|
||||||
|
|
|
||||||
55
README.md
55
README.md
|
|
@ -7,6 +7,31 @@ Jolt reads Clojure source, analyzes it to a host-neutral IR, emits Scheme, and
|
||||||
runs it on Chez. The compiler is self-hosted: it is written in Clojure
|
runs it on Chez. The compiler is self-hosted: it is written in Clojure
|
||||||
(`jolt-core/`) and compiles itself. It ships a Clojure-compatible standard library.
|
(`jolt-core/`) and compiles itself. It ships a Clojure-compatible standard library.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Grab the self-contained `joltc` binary (Linux/macOS/Windows) — it bundles the
|
||||||
|
runtime, 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install jolt-lang/jolt/jolt
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with the install script (installs to `/usr/local/bin` by default; `--dir <dir>`
|
||||||
|
and `--version <v>` override that):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sL https://raw.githubusercontent.com/jolt-lang/jolt/main/install | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Then `joltc -e '(+ 1 2)'`. To run from source instead (needs Chez), see
|
||||||
|
[Build](#build).
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
Only [Chez Scheme](https://cisco.github.io/ChezScheme/) (the gate invokes it as
|
Only [Chez Scheme](https://cisco.github.io/ChezScheme/) (the gate invokes it as
|
||||||
|
|
@ -24,6 +49,18 @@ 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:
|
||||||
|
|
@ -106,6 +143,24 @@ compiler. They come with a from-source Chez install; a distro `chezscheme`
|
||||||
package ships only the runtime, so `build` won't link a binary there.
|
package ships only the runtime, so `build` won't link a binary there.
|
||||||
RFC 0007 (`docs/rfc/`) covers the design and the three-mode model.
|
RFC 0007 (`docs/rfc/`) covers the design and the three-mode model.
|
||||||
|
|
||||||
|
## Standalone joltc binary
|
||||||
|
|
||||||
|
`make` builds joltc itself into a single self-contained native binary — the
|
||||||
|
runtime, compiler, `jolt-core`/`stdlib` source, and the Chez boots are baked in,
|
||||||
|
so the result runs and `build`s jolt apps on a machine with neither Chez nor a C
|
||||||
|
compiler. Build it on a host that *does* have both.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make joltc-release # => target/release/joltc (optimize-level 3, compressed)
|
||||||
|
make joltc-debug # => target/debug/joltc (optimize-level 0, inspector + debug info)
|
||||||
|
make joltc # re-mint the seed first, then both
|
||||||
|
```
|
||||||
|
|
||||||
|
`make joltc` re-mints the seed so the embedded compiler image is current before
|
||||||
|
linking; use `joltc-release`/`joltc-debug` directly to skip that when the seed is
|
||||||
|
already minted. Like `build`, both require Chez's kernel development files
|
||||||
|
(`libkernel.a`, `scheme.h`) and a C compiler.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
A small Chez runtime (`host/chez/*.ss`: value model, persistent collections, seqs,
|
A small Chez runtime (`host/chez/*.ss`: value model, persistent collections, seqs,
|
||||||
|
|
|
||||||
28
bin/joltc
28
bin/joltc
|
|
@ -13,7 +13,29 @@
|
||||||
# the user's original cwd (the project dir, where deps.edn lives) is passed in
|
# the user's original cwd (the project dir, where deps.edn lives) is passed in
|
||||||
# JOLT_PWD.
|
# JOLT_PWD.
|
||||||
root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||||
JOLT_PWD="${JOLT_PWD:-$PWD}"
|
export JOLT_PWD="${JOLT_PWD:-$PWD}"
|
||||||
export JOLT_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 "$@"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -240,6 +240,32 @@ register checks without clobbering each other. This is the mechanism jolt's
|
||||||
HTTP client library uses to emulate `java.net.URL` and `HttpURLConnection` so
|
HTTP client library uses to emulate `java.net.URL` and `HttpURLConnection` so
|
||||||
`clj-http-lite` runs unchanged.
|
`clj-http-lite` runs unchanged.
|
||||||
|
|
||||||
|
`__register-instance-check!` answers one `(instance? Foo x)` question. When a
|
||||||
|
class belongs to a *hierarchy* — a custom exception that should be caught as an
|
||||||
|
`IOException`, or a value that should match `(instance? SomeInterface x)` across
|
||||||
|
its whole supertype chain and dispatch a protocol extended to any of those
|
||||||
|
supertypes — declare its direct supers once with `jolt.host/register-class-supers!`
|
||||||
|
instead. `instance?`, `isa?`, `supers`/`ancestors`, and `extend-protocol`
|
||||||
|
dispatch all derive from the one declaration (supers are given by canonical name;
|
||||||
|
transitivity is computed):
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
;; a library's exception type that catch/instance? should treat as an IOException
|
||||||
|
(jolt.host/register-class-supers! "com.acme.RetryExhaustedException"
|
||||||
|
["java.io.IOException"])
|
||||||
|
|
||||||
|
(throw (jolt.host/throwable "com.acme.RetryExhaustedException" "gave up"))
|
||||||
|
;; (catch java.io.IOException e …) now matches it; (instance? java.lang.Exception e) is true
|
||||||
|
```
|
||||||
|
|
||||||
|
deftype/defrecord classes join the same graph automatically at definition: a
|
||||||
|
record's ancestry carries the record interfaces (`clojure.lang.IRecord`,
|
||||||
|
`IPersistentMap`, `Associative`, …), a bare deftype carries
|
||||||
|
`clojure.lang.IType`, and every protocol the type implements inline appears as
|
||||||
|
an implemented interface — so `(ancestors MyRecord)`, `(isa? MyRecord
|
||||||
|
clojure.lang.IPersistentMap)`, and hierarchy relationships `derive`d on a
|
||||||
|
class's supers all answer like the JVM.
|
||||||
|
|
||||||
Extending a *built-in* class instead (adding a method to core's `String` shim,
|
Extending a *built-in* class instead (adding a method to core's `String` shim,
|
||||||
say) means editing the relevant `host/chez/*.ss` file and running `make remint`
|
say) means editing the relevant `host/chez/*.ss` file and running `make remint`
|
||||||
— see [building-and-deps.md](building-and-deps.md).
|
— see [building-and-deps.md](building-and-deps.md).
|
||||||
|
|
|
||||||
|
|
@ -25,15 +25,13 @@ e.g. the [ring-app example](https://github.com/jolt-lang/examples/tree/main/ring
|
||||||
[dependency](https://github.com/weavejester/dependency) and
|
[dependency](https://github.com/weavejester/dependency) and
|
||||||
[meta-merge](https://github.com/weavejester/meta-merge) deps
|
[meta-merge](https://github.com/weavejester/meta-merge) deps
|
||||||
* [honeysql](https://github.com/seancorfield/honeysql) — SQL formatter and helpers
|
* [honeysql](https://github.com/seancorfield/honeysql) — SQL formatter and helpers
|
||||||
* [clojure.jdbc](https://github.com/yogthos/clojure.jdbc) — as
|
* [clojure.jdbc](https://github.com/yogthos/clojure.jdbc) — via
|
||||||
[jolt-lang/db](https://github.com/jolt-lang/db)'s `jdbc.core`, over the built-in
|
[jolt-lang/db](https://github.com/jolt-lang/db)'s `jdbc.core`, over the built-in
|
||||||
SQLite access (libsqlite3 via Chez's FFI)
|
SQLite access (libsqlite3 via Chez's FFI)
|
||||||
* [next.jdbc](https://github.com/seancorfield/next-jdbc) — a compatibility layer in
|
|
||||||
[jolt-lang/db](https://github.com/jolt-lang/db) over `jdbc.core`
|
|
||||||
* [tools.logging](https://github.com/clojure/tools.logging) — runs verbatim over a
|
* [tools.logging](https://github.com/clojure/tools.logging) — runs verbatim over a
|
||||||
native `clojure.tools.logging.impl` stderr backend
|
native `clojure.tools.logging.impl` stderr backend
|
||||||
* [migratus](https://github.com/yogthos/migratus) — database migrations over the
|
* [migratus](https://github.com/yogthos/migratus) — database migrations over
|
||||||
next.jdbc layer
|
[jolt-lang/db](https://github.com/jolt-lang/db)
|
||||||
* [malli](https://github.com/metosin/malli) — data schema validation, on the
|
* [malli](https://github.com/metosin/malli) — data schema validation, on the
|
||||||
malli-app example.
|
malli-app example.
|
||||||
* [markdown-clj](https://github.com/yogthos/markdown-clj) — Markdown → HTML, on the
|
* [markdown-clj](https://github.com/yogthos/markdown-clj) — Markdown → HTML, on the
|
||||||
|
|
@ -72,6 +70,11 @@ e.g. the [ring-app example](https://github.com/jolt-lang/examples/tree/main/ring
|
||||||
[tools.macro](https://github.com/clojure/tools.macro).
|
[tools.macro](https://github.com/clojure/tools.macro).
|
||||||
* [test.check](https://github.com/clojure/test.check) — property-based testing
|
* [test.check](https://github.com/clojure/test.check) — property-based testing
|
||||||
(generators, `quick-check`, shrinking).
|
(generators, `quick-check`, shrinking).
|
||||||
|
* [tools.reader](https://github.com/clojure/tools.reader) — a Clojure reader in
|
||||||
|
Clojure (edn + full reader, indexing/pushback reader types).
|
||||||
|
* [rewrite-clj](https://github.com/clj-commons/rewrite-clj) — parse/rewrite Clojure
|
||||||
|
source while preserving whitespace and comments (nodes + zipper), over
|
||||||
|
[tools.reader](https://github.com/clojure/tools.reader).
|
||||||
* [tick](https://github.com/juxt/tick) — date/time over Jolt's `java.time`;
|
* [tick](https://github.com/juxt/tick) — date/time over Jolt's `java.time`;
|
||||||
`#time/…` literals via `time-literals`.
|
`#time/…` literals via `time-literals`.
|
||||||
* [transit-jolt](https://github.com/jolt-lang/transit-jolt) — Transit (JSON) read/write
|
* [transit-jolt](https://github.com/jolt-lang/transit-jolt) — Transit (JSON) read/write
|
||||||
|
|
|
||||||
|
|
@ -225,3 +225,31 @@ reader functions are the deliberate exception, S20). Forms read identically
|
||||||
whether or not they will be evaluated; `read-string` of any printable value
|
whether or not they will be evaluated; `read-string` of any printable value
|
||||||
`v` followed by evaluation yields a value equal to `v` for the
|
`v` followed by evaluation yields a value equal to `v` for the
|
||||||
self-evaluating types (§4 print/read round-trip contract).
|
self-evaluating types (§4 print/read round-trip contract).
|
||||||
|
|
||||||
|
## Strict tokens and edn mode
|
||||||
|
|
||||||
|
The reader rejects what the reference rejects (corpus `edn / strictness`,
|
||||||
|
`reader / strict tokens`):
|
||||||
|
|
||||||
|
- A token that starts like a number but doesn't parse as one is
|
||||||
|
NumberFormatException, never a symbol: `1a`, `08` (a leading zero demands
|
||||||
|
octal digits; `042` is 34), `0x2g`, `2r2`. A ratio's parts are plain digit
|
||||||
|
runs (`1/-1` is invalid); a zero denominator is ArithmeticException.
|
||||||
|
- Empty ns/name parts are invalid tokens: `:`, `::`, `foo/`, `/foo`, `:/foo`.
|
||||||
|
`/` (division), `ns//` and `:/` (a name of exactly `/`) are valid.
|
||||||
|
- Map literals with duplicate keys and set literals with duplicate elements
|
||||||
|
throw IllegalArgumentException at read.
|
||||||
|
- An unsupported string escape (`"\q"`) and an octal escape past `\377`
|
||||||
|
(string or `\o` char) throw. A stray close delimiter at top level is
|
||||||
|
"Unmatched delimiter". `\r` terminates a line comment like `\n`.
|
||||||
|
- `#inst` validates its calendar fields progressively (month 1–12, day valid
|
||||||
|
for the month including leap years, hour < 24, minute < 60); `#uuid`
|
||||||
|
demands canonical 8-4-4-4-12 hex.
|
||||||
|
|
||||||
|
clojure.edn adds on top of that (`__read-form-edn` seam): auto-resolved
|
||||||
|
keywords (`::k`) are invalid (no resolution context), each `#_` discarded
|
||||||
|
form is validated through the same `:readers`/`:default` pipeline (an
|
||||||
|
unreadable tagged element throws even when discarded), `M` literals
|
||||||
|
construct BigDecimals, lists satisfy `list?`, and end-of-input honors the
|
||||||
|
`:eof` option — an opts map without `:eof` makes EOF an error, while the
|
||||||
|
no-opts arity returns nil.
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,164 @@ cases; clojure-test-suite `core_test/parse_uuid.cljc`,
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### clojure.template/apply-template, clojure.test/are — since 1.1
|
||||||
|
|
||||||
|
```
|
||||||
|
(apply-template argv expr values)
|
||||||
|
(are argv expr & args)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Semantics**
|
||||||
|
|
||||||
|
- S1. `apply-template` MUST replace every occurrence of each `argv` symbol
|
||||||
|
in `expr` with its corresponding value by structural walk (postwalk symbol
|
||||||
|
substitution), not by lexical binding. Occurrences inside `quote` and at
|
||||||
|
any nesting depth substitute: `(apply-template '[x] '(f 'x) '[if])` ⇒
|
||||||
|
`(f 'if)`.
|
||||||
|
- S2. `do-template` MUST partition `args` by `(count argv)` and expand to a
|
||||||
|
`do` of one substituted `expr` per group.
|
||||||
|
- S3. `clojure.test/are` MUST expand through `do-template` with `expr`
|
||||||
|
wrapped in `is`. Consequently `(are [x] (special-symbol? 'x) if def)`
|
||||||
|
asserts `(special-symbol? 'if)` and `(special-symbol? 'def)` — a
|
||||||
|
let-binding implementation is non-conforming (the quoted symbol would not
|
||||||
|
substitute).
|
||||||
|
|
||||||
|
**Errors**
|
||||||
|
|
||||||
|
- X1. `are` MUST throw at macroexpansion when `(count args)` is not a
|
||||||
|
positive multiple of a non-empty `(count argv)` (empty/empty is allowed).
|
||||||
|
- X2. `apply-template` MUST throw when `argv` is not a vector of symbols.
|
||||||
|
|
||||||
|
**Conformance**
|
||||||
|
|
||||||
|
S1–S3 → `test/chez/clojure-test.clj` (are with quoted template var);
|
||||||
|
clojure-test-suite `core_test/special_symbol_qmark.cljc` and every
|
||||||
|
`are`-based suite namespace.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### make-hierarchy, derive, underive, isa?, parents, ancestors, descendants — since 1.0
|
||||||
|
|
||||||
|
```
|
||||||
|
(make-hierarchy)
|
||||||
|
(derive tag parent) (derive h tag parent)
|
||||||
|
(underive tag parent) (underive h tag parent)
|
||||||
|
(isa? child parent) (isa? h child parent)
|
||||||
|
(parents tag) (ancestors tag) (descendants tag) ; + (f h tag) forms
|
||||||
|
```
|
||||||
|
|
||||||
|
**Semantics**
|
||||||
|
|
||||||
|
- S1. A hierarchy is a pure value `{:parents {tag #{...}} :ancestors {...}
|
||||||
|
:descendants {...}}`; the 3-arity forms are pure, the shorter arities read and
|
||||||
|
mutate the global hierarchy.
|
||||||
|
- S2. `isa?` is true when `(= child parent)`, when the host type system says
|
||||||
|
parent is assignable from child (both classes), when the relationship was
|
||||||
|
`derive`d — including a relationship derived on one of a class child's
|
||||||
|
supers — or component-wise for equal-length vectors.
|
||||||
|
- S3. Class tags answer through the host type hierarchy: `(parents c)` includes
|
||||||
|
the class's direct supers (`bases` — a concrete class's chain roots at
|
||||||
|
`java.lang.Object`, an interface's does not); `(ancestors c)` is the
|
||||||
|
transitive set plus anything `derive`d on the class or its supers. A
|
||||||
|
deftype/defrecord class's ancestry includes its implemented protocol
|
||||||
|
interfaces and, for records, the record interfaces
|
||||||
|
(`clojure.lang.IRecord`/`IPersistentMap`/`Associative`/…; `clojure.lang.IType`
|
||||||
|
for a bare deftype).
|
||||||
|
- S4. `derive` returns the updated hierarchy (3-arity) or nil (2-arity);
|
||||||
|
deriving a relationship that already holds transitively, or one that would
|
||||||
|
create a cycle, throws.
|
||||||
|
|
||||||
|
**Errors**
|
||||||
|
|
||||||
|
- X1. `derive` asserts its argument shapes: parent must be a namespaced Named
|
||||||
|
value; tag must be a class or a Named value (namespaced in the 2-arity
|
||||||
|
global form); `(derive h tag tag)` fails the `not=` assert. AssertionError.
|
||||||
|
- X2. `underive`/`derive` with a non-hierarchy `h` throw at the parents
|
||||||
|
lookup (the map is called as a function, like the reference).
|
||||||
|
- X3. `(descendants h SomeClass)` throws UnsupportedOperationException
|
||||||
|
("Can't get descendants of classes") — Java type inheritance is not
|
||||||
|
enumerable downward.
|
||||||
|
|
||||||
|
**Conformance**
|
||||||
|
|
||||||
|
S1–S4, X1–X3 → corpus `hierarchy / *` rows; clojure-test-suite
|
||||||
|
`core_test/{derive,underive,isa_…,parents,ancestors,descendants}.cljc`
|
||||||
|
(all fully passing).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### atom, add-watch, remove-watch, set-validator!, get-validator — since 1.0
|
||||||
|
|
||||||
|
```
|
||||||
|
(atom x & {:keys [meta validator]})
|
||||||
|
(add-watch iref key f) (remove-watch iref key)
|
||||||
|
(set-validator! iref f) (get-validator iref)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Semantics**
|
||||||
|
|
||||||
|
- S1. Watches, validators, and reference metadata are one contract (the JVM's
|
||||||
|
ARef/IRef) shared by atoms, vars, and agents. `add-watch`/`remove-watch`
|
||||||
|
return the reference; re-adding a key replaces that watch in place.
|
||||||
|
- S2. A watch is called `(f key ref old new)` after a state change: atom
|
||||||
|
swap!/reset!/compare-and-set!, var ROOT changes (`def` on a watched var,
|
||||||
|
`var-set` outside a thread binding, `alter-var-root` — a thread-binding set
|
||||||
|
does not notify), and each agent action's state change.
|
||||||
|
- S3. A validator gates every state change and, via the `:validator` ctor
|
||||||
|
option, the initial value — an invalid initial value never constructs the
|
||||||
|
reference.
|
||||||
|
- S4. The `:meta` ctor option attaches reference metadata (`meta` reads it,
|
||||||
|
`alter-meta!`/`reset-meta!` update it); nil is allowed.
|
||||||
|
|
||||||
|
**Errors**
|
||||||
|
|
||||||
|
- X1. A rejected value (validator returns logical false or the ctor option
|
||||||
|
fails on the initial value) throws IllegalStateException "Invalid reference
|
||||||
|
state".
|
||||||
|
- X2. A non-map `:meta` ctor option throws ClassCastException.
|
||||||
|
|
||||||
|
**Conformance**
|
||||||
|
|
||||||
|
S1–S4, X1–X2 → corpus `iref / *` rows; clojure-test-suite
|
||||||
|
`core_test/{atom,add-watch,remove-watch}.cljc` (the remaining baselined error
|
||||||
|
in the watch namespaces is their STM `ref` section — refs are out of scope,
|
||||||
|
`stm-refs` in `coverage.md`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### clojure.string coercion, some-fn, ifn? — since 1.2/1.3
|
||||||
|
|
||||||
|
```
|
||||||
|
(clojure.string/upper-case s) … (some-fn p & ps) (ifn? x)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Semantics**
|
||||||
|
|
||||||
|
- S1. The clojure.string case fns and searches (`upper-case`, `lower-case`,
|
||||||
|
`capitalize`, `starts-with?`, `ends-with?`, `includes?`, `index-of`,
|
||||||
|
`replace`) take any Object `s` through its `toString`, like the reference's
|
||||||
|
`^CharSequence`+`.toString` signatures: `(upper-case :kw)` is `":KW"`,
|
||||||
|
`(capitalize 1)` is `"1"`. nil throws (method call on null); a nil `substr`
|
||||||
|
throws.
|
||||||
|
- S2. `some-fn` follows the reference arities: at least one predicate
|
||||||
|
(`(some-fn)` is an arity error) and the returned fn chains with `or`, so a
|
||||||
|
no-match result is the last predicate's own falsy value (`false` stays
|
||||||
|
`false`).
|
||||||
|
- S3. `ifn?` covers fns, keywords, symbols, maps, sets, vectors, vars,
|
||||||
|
multimethods, promises (invoking a promise delivers it), and a
|
||||||
|
deftype/defrecord implementing `clojure.lang.IFn`'s `invoke`.
|
||||||
|
- S4. A `defmulti`/`defmethod` deferred inside a fn body interns/resolves in
|
||||||
|
the namespace it was WRITTEN in (the macros bake their expansion ns), not
|
||||||
|
whatever namespace is current when it runs.
|
||||||
|
|
||||||
|
**Conformance**
|
||||||
|
|
||||||
|
S1–S4 → corpus `string / toString coercion`, `core / some-fn`, `core / ifn?`,
|
||||||
|
`multimethods / deferred definition`; clojure-test-suite string/some-fn/
|
||||||
|
ifn-qmark/boolean-qmark/reduce namespaces (all fully passing).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Authoring notes
|
## Authoring notes
|
||||||
|
|
||||||
- Source examples from the ClojureDocs export (`clojuredocs-export.edn`,
|
- Source examples from the ClojureDocs export (`clojuredocs-export.edn`,
|
||||||
|
|
|
||||||
|
|
@ -72,11 +72,41 @@ bindings resolve. Each entry is a map — `{:name "sqlite3" :darwin
|
||||||
the running process's own symbols, e.g. libc sockets, no external file). A
|
the running process's own symbols, e.g. libc sockets, no external file). A
|
||||||
project inherits its dependencies' `:jolt/native`.
|
project inherits its dependencies' `:jolt/native`.
|
||||||
|
|
||||||
|
### Static vs dynamic linking
|
||||||
|
|
||||||
|
When you `joltc build`, a native lib is **statically linked** into the binary by
|
||||||
|
default if the spec carries a `:static` archive — so the executable calls the C
|
||||||
|
code with no shared object present at runtime. Add `:static` alongside the runtime
|
||||||
|
candidates:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
{:name "sqlite3"
|
||||||
|
:static {:archive "/opt/homebrew/lib/libsqlite3.a"} ; or {:lib "sqlite3" :libdir "/usr/lib"}
|
||||||
|
:darwin ["libsqlite3.0.dylib"] ; still used by `run`/`repl` and by --dynamic
|
||||||
|
:linux ["libsqlite3.so.0"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
`:static {:archive PATH}` force-loads the whole `.a` and is the reliable
|
||||||
|
cross-platform form. `:static {:lib NAME :libdir DIR}` links `-lNAME` (with a
|
||||||
|
`-Bstatic` preference on Linux); on macOS, which has no `-Bstatic`, prefer the
|
||||||
|
archive form. A spec with no `:static` (or a build passed `--dynamic`, or
|
||||||
|
`:jolt/build {:dynamic-natives true}`) keeps the old behavior — the shared object
|
||||||
|
is loaded at startup via `load-shared-object`.
|
||||||
|
|
||||||
|
Static linking needs a C compiler (`cc`) on `PATH` at build time (plus the C libs
|
||||||
|
the Chez kernel links — lz4, zlib, ncurses). The distributed `joltc` bundles the
|
||||||
|
Chez kernel, so it re-links the launcher stub with the archive baked in — no
|
||||||
|
external Chez, just `cc`. Without a `cc`, a `:static` lib fails with a message
|
||||||
|
pointing you to install one or pass `--dynamic`. Keep a `:darwin`/`:linux`
|
||||||
|
candidate on any `:static` spec so `run`/`repl` (which have no static binary) can
|
||||||
|
still load it.
|
||||||
|
|
||||||
## Standalone binaries
|
## Standalone binaries
|
||||||
|
|
||||||
`joltc build -m NS` compiles the app and every library into one executable (the
|
`joltc build -m NS` compiles the app and every library into one executable (the
|
||||||
runtime + compiler are baked in). It loads the resolved `:jolt/native` libs at
|
runtime + compiler are baked in). Resolved `:jolt/native` libs are statically
|
||||||
startup, so an FFI app — sockets, SQLite — runs with no jolt or Chez on the path.
|
linked in (or loaded at startup — see [Native libraries](#native-libraries)), so
|
||||||
|
an FFI app — sockets, SQLite — runs with no jolt or Chez on the path.
|
||||||
|
|
||||||
Output goes under the project's `target/`, cargo-style: `target/release/<project>`
|
Output goes under the project's `target/`, cargo-style: `target/release/<project>`
|
||||||
by default and with `--opt`, `target/debug/<project>` with `--dev` (the
|
by default and with `--opt`, `target/debug/<project>` with `--dev` (the
|
||||||
|
|
@ -152,6 +182,30 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -23,21 +23,38 @@
|
||||||
(fields (mutable val) (mutable watches) (mutable validator) lock)
|
(fields (mutable val) (mutable watches) (mutable validator) lock)
|
||||||
(nongenerative jolt-atom-v3))
|
(nongenerative jolt-atom-v3))
|
||||||
|
|
||||||
;; (atom init) / (atom init :validator f :meta m): scan the trailing keyword opts
|
;; a rejected reference value is IllegalStateException, like ARef.validate.
|
||||||
;; for :validator (the only one with runtime behaviour; :meta is accepted/ignored).
|
(define (jolt-iref-state-throw)
|
||||||
|
(jolt-throw (jolt-host-throwable "java.lang.IllegalStateException" "Invalid reference state")))
|
||||||
|
|
||||||
|
;; (atom init :meta m :validator f) — the ARef ctor contract: the validator runs
|
||||||
|
;; against the initial value (an invalid init never constructs), :meta must be a
|
||||||
|
;; map (anything else is the JVM's IPersistentMap cast failure).
|
||||||
(define (jolt-atom-new v . opts)
|
(define (jolt-atom-new v . opts)
|
||||||
(let loop ((o opts) (validator jolt-nil))
|
(let loop ((o opts) (validator jolt-nil) (m #f))
|
||||||
(cond
|
(cond
|
||||||
((or (null? o) (null? (cdr o))) (make-jolt-atom v '() validator (make-mutex)))
|
((or (null? o) (null? (cdr o)))
|
||||||
|
(let ((a (make-jolt-atom v '() validator (make-mutex))))
|
||||||
|
(jolt-atom-validate a v)
|
||||||
|
(when (and m (not (jolt-nil? m)))
|
||||||
|
(unless (jolt-map? m)
|
||||||
|
(jolt-throw (jolt-host-throwable
|
||||||
|
"java.lang.ClassCastException"
|
||||||
|
(string-append "class " (jolt-class-name m)
|
||||||
|
" cannot be cast to class clojure.lang.IPersistentMap"))))
|
||||||
|
(hashtable-set! meta-table a m))
|
||||||
|
a))
|
||||||
((and (keyword-t? (car o)) (string=? (keyword-t-name (car o)) "validator"))
|
((and (keyword-t? (car o)) (string=? (keyword-t-name (car o)) "validator"))
|
||||||
(loop (cddr o) (cadr o)))
|
(loop (cddr o) (cadr o) m))
|
||||||
(else (loop (cddr o) validator)))))
|
((and (keyword-t? (car o)) (string=? (keyword-t-name (car o)) "meta"))
|
||||||
|
(loop (cddr o) validator (cadr o)))
|
||||||
|
(else (loop (cddr o) validator m)))))
|
||||||
|
|
||||||
;; validate a candidate value: a non-nil validator that returns falsey rejects.
|
;; validate a candidate value: a non-nil validator that returns falsey rejects.
|
||||||
(define (jolt-atom-validate a v)
|
(define (jolt-atom-validate a v)
|
||||||
(let ((vf (jolt-atom-validator a)))
|
(let ((vf (jolt-atom-validator a)))
|
||||||
(when (and (not (jolt-nil? vf)) (jolt-not (jolt-invoke vf v)))
|
(when (and (not (jolt-nil? vf)) (jolt-not (jolt-invoke vf v)))
|
||||||
(error #f "Invalid reference state"))))
|
(jolt-iref-state-throw))))
|
||||||
|
|
||||||
;; notify each watch (k ref old new), in insertion order (alist is reverse-built,
|
;; notify each watch (k ref old new), in insertion order (alist is reverse-built,
|
||||||
;; so walk it reversed to match add order).
|
;; so walk it reversed to match add order).
|
||||||
|
|
@ -106,27 +123,87 @@
|
||||||
(jolt-atom-notify a old v)
|
(jolt-atom-notify a old v)
|
||||||
(jolt-vector old v)))
|
(jolt-vector old v)))
|
||||||
|
|
||||||
;; --- watches / validators ---------------------------------------------------
|
;; --- watches / validators: the IRef seam --------------------------------------
|
||||||
|
;; On the JVM these are the ARef contract shared by atom/var/agent/ref. The atom
|
||||||
|
;; keeps its record slots (the hot swap!/reset! path); every OTHER watchable
|
||||||
|
;; reference type registers a predicate here and stores its watches/validator in
|
||||||
|
;; identity-keyed side tables. A ref type makes itself notify by calling
|
||||||
|
;; iref-notify at its mutation points (vars do at root set).
|
||||||
|
(define iref-arms '())
|
||||||
|
(define (register-iref-arm! pred) (set! iref-arms (cons pred iref-arms)))
|
||||||
|
(define (iref? r)
|
||||||
|
(let loop ((as iref-arms))
|
||||||
|
(cond ((null? as) #f) (((car as) r) #t) (else (loop (cdr as))))))
|
||||||
|
(define iref-watch-tbl (make-weak-eq-hashtable))
|
||||||
|
(define iref-validator-tbl (make-weak-eq-hashtable))
|
||||||
|
(define (iref-notify r old new)
|
||||||
|
(for-each (lambda (kv) (jolt-invoke (cdr kv) (car kv) r old new))
|
||||||
|
(reverse (hashtable-ref iref-watch-tbl r '()))))
|
||||||
|
(define (iref-validate r v)
|
||||||
|
(let ((vf (hashtable-ref iref-validator-tbl r jolt-nil)))
|
||||||
|
(when (and (not (jolt-nil? vf)) (jolt-not (jolt-invoke vf v)))
|
||||||
|
(jolt-iref-state-throw))))
|
||||||
|
|
||||||
;; add-watch interns (key . fn) (replacing any existing key, keeping order);
|
;; add-watch interns (key . fn) (replacing any existing key, keeping order);
|
||||||
;; remove-watch drops it; both return the atom. set-validator! installs a
|
;; remove-watch drops it; both return the reference. set-validator! installs a
|
||||||
;; validator and validates the CURRENT value immediately (Clojure throws if it's
|
;; validator and validates the CURRENT value immediately (Clojure throws if it's
|
||||||
;; already invalid); get-validator reads the slot.
|
;; already invalid); get-validator reads the slot.
|
||||||
|
(define (jolt-watch-add alist key f)
|
||||||
|
(cons (cons key f) (remp (lambda (kv) (jolt=2 (car kv) key)) alist)))
|
||||||
(define (jolt-add-watch a key f)
|
(define (jolt-add-watch a key f)
|
||||||
(jolt-atom-watches-set! a
|
(cond
|
||||||
(cons (cons key f)
|
((jolt-atom? a)
|
||||||
(remp (lambda (kv) (jolt=2 (car kv) key)) (jolt-atom-watches a))))
|
(jolt-atom-watches-set! a (jolt-watch-add (jolt-atom-watches a) key f))
|
||||||
a)
|
a)
|
||||||
|
((iref? a)
|
||||||
|
(hashtable-set! iref-watch-tbl a (jolt-watch-add (hashtable-ref iref-watch-tbl a '()) key f))
|
||||||
|
a)
|
||||||
|
(else (error #f "add-watch: not a watchable reference" a))))
|
||||||
(define (jolt-remove-watch a key)
|
(define (jolt-remove-watch a key)
|
||||||
|
(cond
|
||||||
|
((jolt-atom? a)
|
||||||
(jolt-atom-watches-set! a
|
(jolt-atom-watches-set! a
|
||||||
(remp (lambda (kv) (jolt=2 (car kv) key)) (jolt-atom-watches a)))
|
(remp (lambda (kv) (jolt=2 (car kv) key)) (jolt-atom-watches a)))
|
||||||
a)
|
a)
|
||||||
|
((iref? a)
|
||||||
|
(hashtable-set! iref-watch-tbl a
|
||||||
|
(remp (lambda (kv) (jolt=2 (car kv) key)) (hashtable-ref iref-watch-tbl a '())))
|
||||||
|
a)
|
||||||
|
(else (error #f "remove-watch: not a watchable reference" a))))
|
||||||
(define (jolt-set-validator! a f)
|
(define (jolt-set-validator! a f)
|
||||||
(let ((vf (if (jolt-nil? f) jolt-nil f)))
|
(let ((vf (if (jolt-nil? f) jolt-nil f)))
|
||||||
|
(cond
|
||||||
|
((jolt-atom? a)
|
||||||
(when (and (not (jolt-nil? vf)) (jolt-not (jolt-invoke vf (jolt-atom-val a))))
|
(when (and (not (jolt-nil? vf)) (jolt-not (jolt-invoke vf (jolt-atom-val a))))
|
||||||
(error #f "Invalid reference state"))
|
(jolt-iref-state-throw))
|
||||||
(jolt-atom-validator-set! a vf)
|
(jolt-atom-validator-set! a vf))
|
||||||
|
((iref? a)
|
||||||
|
(when (and (not (jolt-nil? vf)) (jolt-not (jolt-invoke vf (jolt-deref a))))
|
||||||
|
(jolt-iref-state-throw))
|
||||||
|
(hashtable-set! iref-validator-tbl a vf))
|
||||||
|
(else (error #f "set-validator!: not a reference" a)))
|
||||||
jolt-nil))
|
jolt-nil))
|
||||||
(define (jolt-get-validator a) (jolt-atom-validator a))
|
(define (jolt-get-validator a)
|
||||||
|
(cond ((jolt-atom? a) (jolt-atom-validator a))
|
||||||
|
((iref? a) (hashtable-ref iref-validator-tbl a jolt-nil))
|
||||||
|
(else jolt-nil)))
|
||||||
|
|
||||||
|
;; vars are watchable IRefs: a root change (def / var-set on the root /
|
||||||
|
;; alter-var-root) validates and notifies like Var.bindRoot. The def-var! wrap
|
||||||
|
;; pays two weak-table probes per def and only does IRef work on a watched var.
|
||||||
|
(register-iref-arm! var-cell?)
|
||||||
|
(define def-var!-pre-iref def-var!)
|
||||||
|
(set! def-var!
|
||||||
|
(lambda (ns name v)
|
||||||
|
(let ((c (jolt-var ns name)))
|
||||||
|
(if (or (pair? (hashtable-ref iref-watch-tbl c '()))
|
||||||
|
(not (jolt-nil? (hashtable-ref iref-validator-tbl c jolt-nil))))
|
||||||
|
(let ((old (var-cell-root c)))
|
||||||
|
(iref-validate c v)
|
||||||
|
(let ((r (def-var!-pre-iref ns name v)))
|
||||||
|
(iref-notify c old v)
|
||||||
|
r))
|
||||||
|
(def-var!-pre-iref ns name v)))))
|
||||||
|
|
||||||
(def-var! "clojure.core" "atom" jolt-atom-new)
|
(def-var! "clojure.core" "atom" jolt-atom-new)
|
||||||
(def-var! "clojure.core" "deref" jolt-deref)
|
(def-var! "clojure.core" "deref" jolt-deref)
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,15 @@
|
||||||
(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 "'"))
|
||||||
|
|
@ -80,6 +89,32 @@
|
||||||
(bld-walk-files root "" '())))
|
(bld-walk-files root "" '())))
|
||||||
(list "jolt-core" "stdlib")))
|
(list "jolt-core" "stdlib")))
|
||||||
|
|
||||||
|
;; Embed every runtime .ss the build inlines into an app (the transitive closure of
|
||||||
|
;; the manifest's loads: rt.ss + all it loads, the seed, compile-eval, loader, ffi,
|
||||||
|
;; png, vendored irregex). Keyed by the exact path the (load "…") forms use, so
|
||||||
|
;; build.ss's bld-source-string reads them from the binary with no jolt source on
|
||||||
|
;; disk. Traversal mirrors bld-emit-runtime/bld-inline-line via the same
|
||||||
|
;; bld-file-lines + bld-load-path, so the embedded set is exactly what build reads.
|
||||||
|
(define (jb-collect-load-paths)
|
||||||
|
(let ((seen (make-hashtable string-hash string=?)) (order '()))
|
||||||
|
(define (walk path)
|
||||||
|
(when (and path (not (hashtable-ref seen path #f)))
|
||||||
|
(hashtable-set! seen path #t)
|
||||||
|
(set! order (cons path order))
|
||||||
|
(for-each (lambda (l) (walk (bld-load-path l))) (bld-file-lines path))))
|
||||||
|
(for-each (lambda (entry) (when (string? entry) (walk (bld-load-path entry))))
|
||||||
|
bld-runtime-manifest)
|
||||||
|
(for-each (lambda (kv) (walk (bld-load-path (cdr kv)))) bld-tagged-loads)
|
||||||
|
(reverse order)))
|
||||||
|
|
||||||
|
(define (jb-emit-runtime-embeds out)
|
||||||
|
(for-each
|
||||||
|
(lambda (path)
|
||||||
|
(put-string out (string-append
|
||||||
|
"(register-embedded-resource! " (ei-str-lit path) " "
|
||||||
|
(ei-str-lit (read-file-string path)) ")\n")))
|
||||||
|
(jb-collect-load-paths)))
|
||||||
|
|
||||||
;; The launcher (Chez scheme-start): replicates host/chez/cli.ss but reads argv
|
;; The launcher (Chez scheme-start): replicates host/chez/cli.ss but reads argv
|
||||||
;; from the scheme-start lambda and has no repo root to cd into (all source is
|
;; from the scheme-start lambda and has no repo root to cd into (all source is
|
||||||
;; embedded; JOLT_PWD defaults to cwd via io/jolt.main). build.ss is already
|
;; embedded; JOLT_PWD defaults to cwd via io/jolt.main). build.ss is already
|
||||||
|
|
@ -101,17 +136,19 @@
|
||||||
(register-embedded-bytes! (car spec) bv)))
|
(register-embedded-bytes! (car spec) bv)))
|
||||||
'((\"csv/petite.boot\" \"jolt_petite_boot\" \"jolt_petite_boot_len\")
|
'((\"csv/petite.boot\" \"jolt_petite_boot\" \"jolt_petite_boot_len\")
|
||||||
(\"csv/scheme.boot\" \"jolt_scheme_boot\" \"jolt_scheme_boot_len\")
|
(\"csv/scheme.boot\" \"jolt_scheme_boot\" \"jolt_scheme_boot_len\")
|
||||||
(\"stub/launcher\" \"jolt_stub\" \"jolt_stub_len\")))))
|
(\"stub/launcher\" \"jolt_stub\" \"jolt_stub_len\")
|
||||||
|
(\"csv/scheme.h\" \"jolt_scheme_h\" \"jolt_scheme_h_len\")
|
||||||
|
(\"csv/libkernel.a\" \"jolt_libkernel_a\" \"jolt_libkernel_a_len\")
|
||||||
|
(\"stub/launcher.c\" \"jolt_launcher_c\" \"jolt_launcher_c_len\")))))
|
||||||
|
|
||||||
(suppress-greeting #t)
|
(suppress-greeting #t)
|
||||||
(scheme-start
|
(scheme-start
|
||||||
(lambda args
|
(lambda args
|
||||||
(set-source-roots! (list \"jolt-core\" \"stdlib\"))
|
(set-source-roots! (list \"jolt-core\" \"stdlib\"))
|
||||||
(guard (v (#t (jolt-report-throwable v (current-error-port))
|
;; JOLT_TRACE at RUNTIME (the env is unset at heap-build), before any app ns
|
||||||
(let ((bt (jolt-backtrace-string v)))
|
;; compiles, so a `-M:run` traces the app's own code.
|
||||||
(when bt (display \" trace:\\n\" (current-error-port))
|
(jolt-trace-init-from-env!)
|
||||||
(display bt (current-error-port))))
|
(guard (v (#t (jolt-report-throwable v (current-error-port)) (exit 1)))
|
||||||
(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
|
||||||
|
|
@ -131,8 +168,14 @@
|
||||||
(bld-emit-runtime out #f #f)
|
(bld-emit-runtime out #f #f)
|
||||||
(put-string out "\n;; === build driver (inlined for self-contained `jolt build`) ===\n")
|
(put-string out "\n;; === build driver (inlined for self-contained `jolt build`) ===\n")
|
||||||
(bld-inline-line "(load \"host/chez/build.ss\")" out 0)
|
(bld-inline-line "(load \"host/chez/build.ss\")" out 0)
|
||||||
|
(put-string out "\n;; === embedded runtime source (self-contained `build` reads these) ===\n")
|
||||||
|
(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))
|
||||||
|
|
@ -182,6 +225,13 @@
|
||||||
(jb-c-array (string-append bld-csv-dir "/petite.boot") (string-append jb-build "/petite_data.h") "jolt_petite_boot")
|
(jb-c-array (string-append bld-csv-dir "/petite.boot") (string-append jb-build "/petite_data.h") "jolt_petite_boot")
|
||||||
(jb-c-array (string-append bld-csv-dir "/scheme.boot") (string-append jb-build "/scheme_data.h") "jolt_scheme_boot")
|
(jb-c-array (string-append bld-csv-dir "/scheme.boot") (string-append jb-build "/scheme_data.h") "jolt_scheme_boot")
|
||||||
(jb-c-array jb-stub (string-append jb-build "/stub_data.h") "jolt_stub")
|
(jb-c-array jb-stub (string-append jb-build "/stub_data.h") "jolt_stub")
|
||||||
|
;; Also bundle the Chez kernel (libkernel.a + scheme.h) and the launcher source,
|
||||||
|
;; so a `build` with :static native libs can re-link a custom stub with those
|
||||||
|
;; archives baked in — the appended-stub path can't add object code to a prebuilt
|
||||||
|
;; stub, so it relinks (build.ss bld-relink-stub). Needs the system cc at build.
|
||||||
|
(jb-c-array (string-append bld-csv-dir "/scheme.h") (string-append jb-build "/schemeh_data.h") "jolt_scheme_h")
|
||||||
|
(jb-c-array (string-append bld-csv-dir "/libkernel.a") (string-append jb-build "/libkernel_data.h") "jolt_libkernel_a")
|
||||||
|
(jb-c-array "host/chez/stub/launcher.c" (string-append jb-build "/launcherc_data.h") "jolt_launcher_c")
|
||||||
|
|
||||||
(define jb-main-c (string-append jb-build "/main.c"))
|
(define jb-main-c (string-append jb-build "/main.c"))
|
||||||
(let ((mc (open-output-file jb-main-c 'replace)))
|
(let ((mc (open-output-file jb-main-c 'replace)))
|
||||||
|
|
@ -192,6 +242,9 @@
|
||||||
"#include \"petite_data.h\"\n"
|
"#include \"petite_data.h\"\n"
|
||||||
"#include \"scheme_data.h\"\n"
|
"#include \"scheme_data.h\"\n"
|
||||||
"#include \"stub_data.h\"\n"
|
"#include \"stub_data.h\"\n"
|
||||||
|
"#include \"schemeh_data.h\"\n"
|
||||||
|
"#include \"libkernel_data.h\"\n"
|
||||||
|
"#include \"launcherc_data.h\"\n"
|
||||||
"int main(int argc, char *argv[]) {\n"
|
"int main(int argc, char *argv[]) {\n"
|
||||||
" Sscheme_init(0);\n"
|
" Sscheme_init(0);\n"
|
||||||
" Sregister_boot_file_bytes(\"jolt\", jolt_boot, jolt_boot_len);\n"
|
" Sregister_boot_file_bytes(\"jolt\", jolt_boot, jolt_boot_len);\n"
|
||||||
|
|
@ -200,7 +253,12 @@
|
||||||
" Sscheme_deinit();\n return status;\n}\n"))
|
" Sscheme_deinit();\n return status;\n}\n"))
|
||||||
(close-port mc))
|
(close-port mc))
|
||||||
|
|
||||||
|
;; -rdynamic puts the embedded jolt_* boot/stub symbols in the dynamic symbol
|
||||||
|
;; 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).
|
||||||
(bld-system (string-append
|
(bld-system (string-append
|
||||||
"cc -O2 -I'" bld-csv-dir "' -I'" jb-build "' '" jb-main-c "' '"
|
;; the embedded jolt_* arrays must be foreign-entry-visible at runtime:
|
||||||
|
;; -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"))
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,28 @@ 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
|
||||||
|
|
@ -116,4 +138,33 @@ fi
|
||||||
if grep -q 'def-var! "clojure.core" "group-by"' "$out.build/flat.ss"; then
|
if grep -q 'def-var! "clojure.core" "group-by"' "$out.build/flat.ss"; then
|
||||||
echo " FAIL: --tree-shake kept an unreachable clojure.core fn (group-by)"; exit 1
|
echo " FAIL: --tree-shake kept an unreachable clojure.core fn (group-by)"; exit 1
|
||||||
fi
|
fi
|
||||||
echo "build smoke: passed (release + optimized + direct-link + tree-shake + compiler+core shake)"
|
# A registered data reader that returns a CODE form must be compiled into the
|
||||||
|
# binary (the emit path applies it too, not just the interpreted loader): the
|
||||||
|
# datareader-app's #code literal builds to 42, not the literal list.
|
||||||
|
drapp="$root/test/chez/datareader-app"
|
||||||
|
drout="$(dirname "$out")/dr-bin"
|
||||||
|
if ! JOLT_PWD="$drapp" bin/joltc build -m drtest.main -o "$drout" >/dev/null 2>&1; then
|
||||||
|
echo " FAIL: jolt build of a data-reader app exited non-zero"; exit 1
|
||||||
|
fi
|
||||||
|
got_dr="$(cd / && "$drout" 2>&1 | tail -1)"
|
||||||
|
if [ "$got_dr" != "42" ]; then
|
||||||
|
echo " FAIL: built #code data reader — want 42, got \`$got_dr\`"; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A script namespace with no -main (just top-level side effects) must build and
|
||||||
|
# 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)"
|
||||||
|
|
|
||||||
|
|
@ -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 cmd)) (in (car p)))
|
(let* ((p (process (bld-sh-wrap 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 cmd)))
|
(let ((rc (system (bld-sh-wrap 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)))))
|
||||||
|
|
||||||
|
|
@ -57,6 +57,24 @@
|
||||||
;; --- toolchain discovery ----------------------------------------------------
|
;; --- toolchain discovery ----------------------------------------------------
|
||||||
(define bld-machine (symbol->string (machine-type)))
|
(define bld-machine (symbol->string (machine-type)))
|
||||||
(define bld-osx? (bld-contains? bld-machine "osx"))
|
(define bld-osx? (bld-contains? bld-machine "osx"))
|
||||||
|
(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
|
||||||
|
|
@ -80,6 +98,9 @@
|
||||||
(cand (string-append bindir "/../lib/csv" bld-version "/" bld-machine)))
|
(cand (string-append bindir "/../lib/csv" bld-version "/" bld-machine)))
|
||||||
cand))))
|
cand))))
|
||||||
|
|
||||||
|
(define (bld-have-cc?)
|
||||||
|
(> (string-length (bld-sh-capture "command -v cc")) 0))
|
||||||
|
|
||||||
(define (bld-check-toolchain)
|
(define (bld-check-toolchain)
|
||||||
(for-each
|
(for-each
|
||||||
(lambda (f)
|
(lambda (f)
|
||||||
|
|
@ -91,14 +112,21 @@
|
||||||
|
|
||||||
;; Link flags. macOS Homebrew layout for the kernel's lz4/zlib/ncurses deps.
|
;; Link flags. macOS Homebrew layout for the kernel's lz4/zlib/ncurses deps.
|
||||||
(define (bld-link-libs)
|
(define (bld-link-libs)
|
||||||
(if bld-osx?
|
(cond
|
||||||
|
(bld-osx?
|
||||||
(let ((lz4 (bld-sh-capture "brew --prefix lz4 2>/dev/null")))
|
(let ((lz4 (bld-sh-capture "brew --prefix lz4 2>/dev/null")))
|
||||||
(string-append
|
(string-append
|
||||||
(if (> (string-length lz4) 0) (string-append "-L" lz4 "/lib ") "")
|
(if (> (string-length lz4) 0) (string-append "-L" lz4 "/lib ") "")
|
||||||
"-llz4 -lz -lncurses -framework Foundation -liconv -lm"))
|
"-llz4 -lz -lncurses -framework Foundation -liconv -lm")))
|
||||||
|
;; Windows (ta6nt, MinGW-w64 under MSYS2): the Chez kernel pulls in
|
||||||
|
;; compression, winsock, COM/UUID, and the registry.
|
||||||
|
(bld-nt?
|
||||||
|
;; -static: a single-file exe (no libwinpthread/libgcc/lz4 DLL deps) —
|
||||||
|
;; 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.
|
||||||
"-llz4 -lz -lncurses -ltinfo -ldl -lm -lpthread -luuid -lrt"))
|
(else "-llz4 -lz -lncurses -ltinfo -ldl -lm -lpthread -luuid -lrt")))
|
||||||
|
|
||||||
;; --- runtime manifest (mirrors host/chez/cli.ss's load order) ---------------
|
;; --- runtime manifest (mirrors host/chez/cli.ss's load order) ---------------
|
||||||
;; A line is either literal Scheme text to inline, or a tag whose emission the build
|
;; A line is either literal Scheme text to inline, or a tag whose emission the build
|
||||||
|
|
@ -138,12 +166,23 @@
|
||||||
(q2 (let scan ((i (+ q1 1))) (if (char=? (string-ref s i) #\") i (scan (+ i 1))))))
|
(q2 (let scan ((i (+ q1 1))) (if (char=? (string-ref s i) #\") i (scan (+ i 1))))))
|
||||||
(substring s (+ q1 1) q2)))))
|
(substring s (+ q1 1) q2)))))
|
||||||
|
|
||||||
(define (bld-file-lines path)
|
;; runtime source for PATH: from the binary's embedded store if present (a
|
||||||
(call-with-input-file path
|
;; self-contained joltc building an app, with no jolt checkout on disk), else read
|
||||||
(lambda (p)
|
;; from disk (running from a source checkout). build-joltc embeds every runtime
|
||||||
(let loop ((acc '()))
|
;; .ss the manifest inlines, so `build` never touches the filesystem for them.
|
||||||
(let ((l (get-line p)))
|
(define (bld-source-string path)
|
||||||
(if (eof-object? l) (reverse acc) (loop (cons l acc))))))))
|
(let ((emb (hashtable-ref embedded-resources path #f)))
|
||||||
|
(if (string? emb) emb (read-file-string path))))
|
||||||
|
|
||||||
|
(define (bld-string-lines s)
|
||||||
|
(let ((n (string-length s)))
|
||||||
|
(let loop ((i 0) (start 0) (acc '()))
|
||||||
|
(cond ((>= i n) (reverse (if (> i start) (cons (substring s start i) acc) acc)))
|
||||||
|
((char=? (string-ref s i) #\newline)
|
||||||
|
(loop (+ i 1) (+ i 1) (cons (substring s start i) acc)))
|
||||||
|
(else (loop (+ i 1) start acc))))))
|
||||||
|
|
||||||
|
(define (bld-file-lines path) (bld-string-lines (bld-source-string path)))
|
||||||
|
|
||||||
;; Emit one line to OUT, recursively inlining a `(load ...)` of a repo file.
|
;; Emit one line to OUT, recursively inlining a `(load ...)` of a repo file.
|
||||||
(define (bld-inline-line line out depth)
|
(define (bld-inline-line line out depth)
|
||||||
|
|
@ -271,21 +310,24 @@
|
||||||
(define (bld-strs x) (map jolt-str-render-one (seq->list x)))
|
(define (bld-strs x) (map jolt-str-render-one (seq->list x)))
|
||||||
|
|
||||||
;; Emit native-library loads. `natives` is the encoded jolt seq jolt.main/
|
;; Emit native-library loads. `natives` is the encoded jolt seq jolt.main/
|
||||||
;; encode-natives produced: each entry is ["process"] | ["req" cand…] | ["opt" cand…].
|
;; encode-natives produced: each entry is ["process"] | ["static" form…] |
|
||||||
;; `which` selects 'required (process + req) or 'optional. Required + process loads
|
;; ["req" cand…] | ["opt" cand…]. `which` selects 'required (process + static +
|
||||||
;; are emitted before the app forms (the app's defcfn foreign-procedures resolve
|
;; req) or 'optional. Required loads are emitted before the app forms (the app's
|
||||||
;; their symbols at top-level eval during startup, so the libs must be loaded
|
;; defcfn foreign-procedures resolve their symbols at top-level eval during
|
||||||
;; first); a load-shared-object failure there is fatal — correct for a required
|
;; startup, so the libs must be loaded first); a load-shared-object failure there
|
||||||
;; lib. Optional loads run in the scheme-start launcher, where guard catches a
|
;; is fatal — correct for a required lib. A "static" lib is cc-linked into the
|
||||||
;; missing lib (an optional lib's namespace is only present when the app requires
|
;; binary (see bld-native-link-flags), so its symbols are already in the process:
|
||||||
;; it, so its foreign-procedures aren't among the baked top-level forms).
|
;; it loads them the same way a "process" lib does. Optional loads run in the
|
||||||
|
;; scheme-start launcher, where guard catches a missing lib (an optional lib's
|
||||||
|
;; namespace is only present when the app requires it, so its foreign-procedures
|
||||||
|
;; aren't among the baked top-level forms).
|
||||||
(define (bld-emit-natives out natives which)
|
(define (bld-emit-natives out natives which)
|
||||||
(for-each
|
(for-each
|
||||||
(lambda (entry)
|
(lambda (entry)
|
||||||
(let* ((parts (bld-strs entry)) (kind (car parts)) (cands (cdr parts))
|
(let* ((parts (bld-strs entry)) (kind (car parts)) (cands (cdr parts))
|
||||||
(cand-lits (fold-left (lambda (s c) (string-append s (ei-str-lit c) " ")) "" cands)))
|
(cand-lits (fold-left (lambda (s c) (string-append s (ei-str-lit c) " ")) "" cands)))
|
||||||
(cond
|
(cond
|
||||||
((and (eq? which 'required) (string=? kind "process"))
|
((and (eq? which 'required) (or (string=? kind "process") (string=? kind "static")))
|
||||||
(put-string out "(jolt-build-load-native '() #f #t)\n"))
|
(put-string out "(jolt-build-load-native '() #f #t)\n"))
|
||||||
((and (eq? which 'required) (string=? kind "req"))
|
((and (eq? which 'required) (string=? kind "req"))
|
||||||
(put-string out (string-append "(jolt-build-load-native (list " cand-lits ") #f #f)\n")))
|
(put-string out (string-append "(jolt-build-load-native (list " cand-lits ") #f #f)\n")))
|
||||||
|
|
@ -293,6 +335,66 @@
|
||||||
(put-string out (string-append "(jolt-build-load-native (list " cand-lits ") #t #f)\n"))))))
|
(put-string out (string-append "(jolt-build-load-native (list " cand-lits ") #t #f)\n"))))))
|
||||||
(seq->list natives)))
|
(seq->list natives)))
|
||||||
|
|
||||||
|
;; The cc link fragment for the "static" natives: each archive must be FORCE-loaded
|
||||||
|
;; (the linker would otherwise drop an archive member main.c never references) and,
|
||||||
|
;; on Linux, the executable's symbols exported into the dynamic table so the
|
||||||
|
;; startup (load-shared-object #f) + foreign-procedure can resolve them (-rdynamic,
|
||||||
|
;; added by build-with-cc when this fragment is non-empty). Returns "" when no lib
|
||||||
|
;; is statically linked. Entry forms: ["static" "archive" path] | ["static" "lib"
|
||||||
|
;; name libdir].
|
||||||
|
(define (bld-native-link-flags natives)
|
||||||
|
(fold-left
|
||||||
|
(lambda (acc entry)
|
||||||
|
(let ((parts (bld-strs entry)))
|
||||||
|
(if (string=? (car parts) "static")
|
||||||
|
(string-append acc " " (bld-one-static-link (cdr parts)))
|
||||||
|
acc)))
|
||||||
|
"" (seq->list natives)))
|
||||||
|
|
||||||
|
;; A statically-linked native is only in the OUTPUT binary, but build step 1
|
||||||
|
;; evaluates the app's `foreign-procedure` forms in THIS process (to register its
|
||||||
|
;; macros/vars), and Chez resolves a foreign entry eagerly. So make the archive's
|
||||||
|
;; symbols resolvable here: build a throwaway shared object from it (force-loading
|
||||||
|
;; every member) and load it. The output binary still cc-links the static archive;
|
||||||
|
;; this temp .so is build-time only. Only the "archive" form is preloaded — the
|
||||||
|
;; "lib" form names a system library the OS loader already finds by soname.
|
||||||
|
(define (bld-preload-static-natives! natives builddir)
|
||||||
|
(let ((n 0))
|
||||||
|
(for-each
|
||||||
|
(lambda (entry)
|
||||||
|
(let ((parts (bld-strs entry)))
|
||||||
|
(when (and (string=? (car parts) "static") (string=? (cadr parts) "archive"))
|
||||||
|
(let* ((archive (caddr parts))
|
||||||
|
(so (string-append builddir "/native-" (number->string n)
|
||||||
|
(if bld-osx? ".dylib" ".so"))))
|
||||||
|
(set! n (+ n 1))
|
||||||
|
(bld-system
|
||||||
|
(if bld-osx?
|
||||||
|
(string-append "cc -dynamiclib -undefined dynamic_lookup -Wl,-all_load '"
|
||||||
|
archive "' -o '" so "'")
|
||||||
|
(string-append "cc -shared -Wl,--whole-archive '" archive
|
||||||
|
"' -Wl,--no-whole-archive -Wl,--unresolved-symbols=ignore-all -o '" so "'")))
|
||||||
|
(load-shared-object so)))))
|
||||||
|
(seq->list natives))))
|
||||||
|
|
||||||
|
(define (bld-one-static-link form)
|
||||||
|
(let ((kind (car form)))
|
||||||
|
(cond
|
||||||
|
((string=? kind "archive")
|
||||||
|
(let ((path (cadr form)))
|
||||||
|
(if bld-osx?
|
||||||
|
(string-append "-Wl,-force_load," path)
|
||||||
|
(string-append "-Wl,--whole-archive " path " -Wl,--no-whole-archive"))))
|
||||||
|
((string=? kind "lib")
|
||||||
|
(let* ((lib (cadr form)) (dir (caddr form))
|
||||||
|
(L (if (> (string-length dir) 0) (string-append "-L" dir " ") "")))
|
||||||
|
;; -Bstatic forces the .a over a .so of the same -l name (GNU ld). macOS's
|
||||||
|
;; ld64 has no -Bstatic; there an :archive path is the reliable form.
|
||||||
|
(if bld-osx?
|
||||||
|
(string-append L "-l" lib)
|
||||||
|
(string-append L "-Wl,-Bstatic -l" lib " -Wl,-Bdynamic"))))
|
||||||
|
(else ""))))
|
||||||
|
|
||||||
;; Walk an embed root recursively; return (resource-name . abspath) pairs, where
|
;; Walk an embed root recursively; return (resource-name . abspath) pairs, where
|
||||||
;; resource-name is the "/"-joined path under the root (what io/resource is asked for).
|
;; resource-name is the "/"-joined path under the root (what io/resource is asked for).
|
||||||
(define (bld-walk-files root rel acc)
|
(define (bld-walk-files root rel acc)
|
||||||
|
|
@ -334,10 +436,31 @@
|
||||||
;; direct-link?: opt-in closed-world direct-linking (app->app calls bind directly,
|
;; direct-link?: opt-in closed-world direct-linking (app->app calls bind directly,
|
||||||
;; no runtime redefinition). Off by default in every mode — release stays
|
;; no runtime redefinition). Off by default in every mode — release stays
|
||||||
;; dynamically linked.
|
;; dynamically linked.
|
||||||
|
(define (bld-suffix? s suf)
|
||||||
|
(let ((n (string-length s)) (m (string-length suf)))
|
||||||
|
(and (>= n m) (string=? (substring s (- n m) n) suf))))
|
||||||
(define (build-binary entry-ns out-path mode natives embed-dirs ext-roots direct-link? tree-shake?)
|
(define (build-binary entry-ns out-path mode natives embed-dirs ext-roots direct-link? tree-shake?)
|
||||||
|
;; Windows executables carry .exe; normalize here so the append-payload and
|
||||||
|
;; cc paths agree and the shell can run the result.
|
||||||
|
(let ((out-path (if (and bld-nt? (not (bld-suffix? out-path ".exe")))
|
||||||
|
(string-append out-path ".exe")
|
||||||
|
out-path)))
|
||||||
;; The self-contained path (jolt-embedded-bytes "stub/launcher") needs no csv
|
;; The self-contained path (jolt-embedded-bytes "stub/launcher") needs no csv
|
||||||
;; kernel files, no Chez, no cc — only the legacy cc path does.
|
;; kernel files, no Chez, no cc — only the legacy cc path does.
|
||||||
(unless (jolt-embedded-bytes "stub/launcher") (bld-check-toolchain))
|
(unless (jolt-embedded-bytes "stub/launcher") (bld-check-toolchain))
|
||||||
|
(when (> (string-length (bld-native-link-flags natives)) 0)
|
||||||
|
;; :static natives are cc-linked into the binary, so a C compiler must be on
|
||||||
|
;; PATH — the self-contained joltc bundles the Chez kernel (libkernel.a +
|
||||||
|
;; scheme.h) and relinks a custom stub (see build-self-contained), but still
|
||||||
|
;; needs the system cc for that link. Fail early (before the app's foreign-
|
||||||
|
;; procedure forms eval below) with an actionable message.
|
||||||
|
(unless (bld-have-cc?)
|
||||||
|
(error 'jolt-build
|
||||||
|
"static native linking needs a C compiler (cc) on PATH; install one, or pass --dynamic to load the library at runtime."))
|
||||||
|
;; Preload static archives' symbols into this process so step 1's foreign-
|
||||||
|
;; procedure evals resolve; the .build dir must exist first.
|
||||||
|
(bld-mkdir-p (string-append out-path ".build"))
|
||||||
|
(bld-preload-static-natives! natives (string-append out-path ".build")))
|
||||||
;; 1. record app namespaces in dependency order as they finish loading.
|
;; 1. record app namespaces in dependency order as they finish loading.
|
||||||
(let ((app-order '()))
|
(let ((app-order '()))
|
||||||
(set-ns-loaded-hook!
|
(set-ns-loaded-hook!
|
||||||
|
|
@ -370,6 +493,11 @@
|
||||||
;; whole-program param-type fixpoint before per-form emit
|
;; whole-program param-type fixpoint before per-form emit
|
||||||
(when (string=? mode "optimized") (bld-wp-infer! ordered)))
|
(when (string=? mode "optimized") (bld-wp-infer! ordered)))
|
||||||
(lambda ()
|
(lambda ()
|
||||||
|
;; A #tag data-reader literal must compile in the binary the same as
|
||||||
|
;; it loads interpreted — apply the reader rewrite to each emitted
|
||||||
|
;; form too (no-op unless the app registered data readers).
|
||||||
|
(parameterize ((ei-emit-form-hook
|
||||||
|
(lambda (form) (if data-readers-active (ldr-apply-readers form) form))))
|
||||||
(if tree-shake?
|
(if tree-shake?
|
||||||
(dce-shake
|
(dce-shake
|
||||||
(dce-blob-records "host/chez/seed/prelude.ss")
|
(dce-blob-records "host/chez/seed/prelude.ss")
|
||||||
|
|
@ -394,7 +522,7 @@
|
||||||
(append (bld-ns-prelude (car nf) src)
|
(append (bld-ns-prelude (car nf) src)
|
||||||
(bld-emit-ns (car nf) src)))))
|
(bld-emit-ns (car nf) src)))))
|
||||||
ordered))
|
ordered))
|
||||||
#f)))
|
#f))))
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(set-optimize! #f)
|
(set-optimize! #f)
|
||||||
((var-deref "jolt.backend-scheme" "set-direct-link!") #f)))))
|
((var-deref "jolt.backend-scheme" "set-direct-link!") #f)))))
|
||||||
|
|
@ -443,11 +571,23 @@
|
||||||
"))\n"
|
"))\n"
|
||||||
" (list \"jolt-core\" \"stdlib\"))))\n"))
|
" (list \"jolt-core\" \"stdlib\"))))\n"))
|
||||||
(put-string out (string-append
|
(put-string out (string-append
|
||||||
" (let ((mainv (var-deref " (ei-str-lit entry-ns) " \"-main\")))\n"
|
;; Call -main only if the entry namespace defines one;
|
||||||
|
;; 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"
|
||||||
" (apply jolt-invoke mainv args)))\n"
|
;; Loading the app left the current ns at the entry ns; reset
|
||||||
|
;; 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
|
||||||
|
|
@ -460,27 +600,81 @@
|
||||||
;; make-boot-file, then xxd the boot into a C array and cc-link against
|
;; make-boot-file, then xxd the boot into a C array and cc-link against
|
||||||
;; libkernel.a. Kept so `make buildsmoke` still exercises the cc path.
|
;; libkernel.a. Kept so `make buildsmoke` still exercises the cc path.
|
||||||
(if (jolt-embedded-bytes "stub/launcher")
|
(if (jolt-embedded-bytes "stub/launcher")
|
||||||
(build-self-contained entry-ns out-path mode builddir flat-ss flat-so boot)
|
(build-self-contained entry-ns out-path mode builddir flat-ss flat-so boot
|
||||||
(build-with-cc entry-ns out-path mode builddir flat-ss flat-so boot boot-h main-c)))))))
|
(bld-native-link-flags natives))
|
||||||
|
(build-with-cc entry-ns out-path mode builddir flat-ss flat-so boot boot-h main-c
|
||||||
|
(bld-native-link-flags natives)))))))))
|
||||||
|
|
||||||
;; --- self-contained link (in-process compile + append the boot to the stub) ---
|
;; --- self-contained link (in-process compile + append the boot to the stub) ---
|
||||||
;; compile-file runs with a FRESH scheme-environment as the interaction
|
;; compile-file runs against the DEFAULT interaction environment, so the boot's
|
||||||
;; environment: the loaded jolt runtime redefines `error` (regex.ss), and flat.ss
|
;; top-level defines land in the real symbol cells — the runtime compiler's
|
||||||
;; inlines that same runtime, so an early reference to `error` (before its define
|
;; eval'd code must resolve them (var-deref, jolt-invoke, the jolt-n* macros)
|
||||||
;; runs) must bind to the kernel primitive — a clean env gives exactly that, the
|
;; when the built binary dynamically requires a namespace. Compiling in a clean
|
||||||
;; same as the legacy path compiling in a fresh Chez process. Without it the boot
|
;; copy-environment instead orphans every define in locations eval can't see,
|
||||||
;; dies at startup with "variable error is not bound".
|
;; and the binary dies with "variable var-deref is not bound" the moment a
|
||||||
(define (build-self-contained entry-ns out-path mode builddir flat-ss flat-so boot)
|
;; runtime require compiles source.
|
||||||
|
;;
|
||||||
|
;; The default env has a wrinkle the legacy fresh-Chez path doesn't: THIS
|
||||||
|
;; process's cells hold jolt's redefinitions of some kernel names (`error`,
|
||||||
|
;; regex.ss), so references to them compile as cell reads — and a read that
|
||||||
|
;; runs before the redefining form would find the fresh binary's cell unbound.
|
||||||
|
;; The prologue closes that: it first binds each redefined kernel name's cell
|
||||||
|
;; to its kernel value, making the boot's earliest reads identical to the
|
||||||
|
;; legacy path's primitive references.
|
||||||
|
|
||||||
|
;; every top-level (define nm …)/(define (nm …) …) name in the flat file that
|
||||||
|
;; shadows a scheme-environment VARIABLE (syntax names don't eval; skip them).
|
||||||
|
(define (bld-kernel-prologue flat-ss)
|
||||||
|
(let ((seen (make-eq-hashtable))
|
||||||
|
(kenv (scheme-environment))
|
||||||
|
(names '()))
|
||||||
|
(let ((ip (open-input-file flat-ss)))
|
||||||
|
(let loop ()
|
||||||
|
(let ((f (read ip)))
|
||||||
|
(unless (eof-object? f)
|
||||||
|
(when (and (pair? f) (eq? (car f) 'define) (pair? (cdr f)))
|
||||||
|
(let* ((h (cadr f))
|
||||||
|
(nm (if (pair? h) (car h) h)))
|
||||||
|
(when (and (symbol? nm)
|
||||||
|
(not (hashtable-ref seen nm #f))
|
||||||
|
(guard (e (#t #f)) (begin (eval nm kenv) #t)))
|
||||||
|
(hashtable-set! seen nm #t)
|
||||||
|
(set! names (cons nm names)))))
|
||||||
|
(loop))))
|
||||||
|
(close-port ip))
|
||||||
|
(apply string-append
|
||||||
|
(map (lambda (nm)
|
||||||
|
(let ((s (symbol->string nm)))
|
||||||
|
(string-append "(define " s " (eval '" s " (scheme-environment)))\n")))
|
||||||
|
(reverse names)))))
|
||||||
|
|
||||||
|
;; prepend the prologue to the flat file in place.
|
||||||
|
(define (bld-prepend-prologue! flat-ss)
|
||||||
|
(let ((prologue (bld-kernel-prologue flat-ss))
|
||||||
|
(body (read-file-string flat-ss)))
|
||||||
|
(let ((out (open-output-file flat-ss 'replace)))
|
||||||
|
(put-string out ";; kernel-name cells pre-bound so early reads match the kernel primitives\n")
|
||||||
|
(put-string out prologue)
|
||||||
|
(put-string out body)
|
||||||
|
(close-port out))))
|
||||||
|
|
||||||
|
(define (build-self-contained entry-ns out-path mode builddir flat-ss flat-so boot native-link)
|
||||||
(let ((petite (string-append builddir "/petite.boot"))
|
(let ((petite (string-append builddir "/petite.boot"))
|
||||||
(scheme (string-append builddir "/scheme.boot")))
|
(scheme (string-append builddir "/scheme.boot")))
|
||||||
(jolt-spill-embedded! "csv/petite.boot" petite)
|
(jolt-spill-embedded! "csv/petite.boot" petite)
|
||||||
(jolt-spill-embedded! "csv/scheme.boot" scheme)
|
(jolt-spill-embedded! "csv/scheme.boot" scheme)
|
||||||
(display (string-append "jolt build: compiling " entry-ns " (" mode " mode, self-contained)\n"))
|
(display (string-append "jolt build: compiling " entry-ns " (" mode " mode, self-contained)\n"))
|
||||||
(parameterize ((interaction-environment (copy-environment (scheme-environment))))
|
(bld-prepend-prologue! flat-ss)
|
||||||
(compile-file flat-ss flat-so)
|
(compile-file flat-ss flat-so)
|
||||||
(make-boot-file boot '() petite scheme flat-so))
|
(make-boot-file boot '() petite scheme flat-so)
|
||||||
|
;; The stub is the native launcher the boot is appended to. With no :static
|
||||||
|
;; natives it's the prebuilt one bundled in joltc (no cc needed); with :static
|
||||||
|
;; natives it's re-linked here from the bundled kernel + launcher source so the
|
||||||
|
;; archives are baked in and their symbols resolve in the running binary.
|
||||||
|
(if (> (string-length native-link) 0)
|
||||||
|
(bld-relink-stub builddir native-link out-path)
|
||||||
|
(jolt-spill-embedded! "stub/launcher" out-path))
|
||||||
;; link: stub bytes ++ boot ++ frame, then make it executable.
|
;; link: stub bytes ++ boot ++ frame, then make it executable.
|
||||||
(jolt-spill-embedded! "stub/launcher" out-path)
|
|
||||||
(jolt-append-payload! out-path (read-file-bytes boot))
|
(jolt-append-payload! out-path (read-file-bytes boot))
|
||||||
(jolt-chmod-755 out-path)
|
(jolt-chmod-755 out-path)
|
||||||
(display (string-append "jolt build: wrote " out-path "\n"))
|
(display (string-append "jolt build: wrote " out-path "\n"))
|
||||||
|
|
@ -489,8 +683,27 @@
|
||||||
"jolt build: note — on macOS this binary is unsigned; to share it,\n"
|
"jolt build: note — on macOS this binary is unsigned; to share it,\n"
|
||||||
" `xattr -d com.apple.quarantine " out-path "` on the target, or sign it.\n")))))
|
" `xattr -d com.apple.quarantine " out-path "` on the target, or sign it.\n")))))
|
||||||
|
|
||||||
|
;; Re-link the launcher stub with the app's static native archives baked in, to
|
||||||
|
;; OUT-PATH. The self-contained joltc bundles the Chez kernel (libkernel.a),
|
||||||
|
;; header, and launcher source; spill them and drive the system cc — the same link
|
||||||
|
;; build-joltc.ss ran once at joltc-build time, plus the force-load archive flags
|
||||||
|
;; (native-link) and, on Linux, -rdynamic so the baked-in symbols stay dlsym-
|
||||||
|
;; visible for (load-shared-object #f) + foreign-procedure at startup.
|
||||||
|
(define (bld-relink-stub builddir native-link out-path)
|
||||||
|
(let ((h (string-append builddir "/scheme.h"))
|
||||||
|
(lk (string-append builddir "/libkernel.a"))
|
||||||
|
(lc (string-append builddir "/launcher.c")))
|
||||||
|
(jolt-spill-embedded! "csv/scheme.h" h)
|
||||||
|
(jolt-spill-embedded! "csv/libkernel.a" lk)
|
||||||
|
(jolt-spill-embedded! "stub/launcher.c" lc)
|
||||||
|
(display "jolt build: relinking launcher stub with static native libraries\n")
|
||||||
|
(bld-system (string-append
|
||||||
|
"cc -O2 " (if bld-osx? "" "-rdynamic ")
|
||||||
|
"-I'" builddir "' '" lc "' '" lk "' -o '" out-path "' "
|
||||||
|
(bld-link-libs) native-link))))
|
||||||
|
|
||||||
;; --- legacy cc link (dev bin/joltc): fresh Chez compile + xxd + cc ------------
|
;; --- legacy cc link (dev bin/joltc): fresh Chez compile + xxd + cc ------------
|
||||||
(define (build-with-cc entry-ns out-path mode builddir flat-ss flat-so boot boot-h main-c)
|
(define (build-with-cc entry-ns out-path mode builddir flat-ss flat-so boot boot-h main-c native-link)
|
||||||
(display (string-append "jolt build: compiling " entry-ns " (" mode " mode)\n"))
|
(display (string-append "jolt build: compiling " entry-ns " (" mode " mode)\n"))
|
||||||
(let ((cs (string-append builddir "/compile.ss")))
|
(let ((cs (string-append builddir "/compile.ss")))
|
||||||
(let ((p (open-output-file cs 'replace)))
|
(let ((p (open-output-file cs 'replace)))
|
||||||
|
|
@ -520,9 +733,13 @@
|
||||||
" int status = Sscheme_start(argc, (const char **)argv);\n"
|
" int status = Sscheme_start(argc, (const char **)argv);\n"
|
||||||
" Sscheme_deinit();\n return status;\n}\n"))
|
" Sscheme_deinit();\n return status;\n}\n"))
|
||||||
(close-port mc))
|
(close-port mc))
|
||||||
|
;; -rdynamic (Linux) exports the executable's symbols into the dynamic table so
|
||||||
|
;; a statically-linked native lib's symbols resolve via (load-shared-object #f)
|
||||||
|
;; at startup. macOS keeps unstripped executable symbols dlsym-visible already.
|
||||||
(bld-system (string-append
|
(bld-system (string-append
|
||||||
"cc -O2 -I'" bld-csv-dir "' '" main-c "' '" bld-csv-dir "/libkernel.a' "
|
"cc -O2 " (if (and (not bld-osx?) (> (string-length native-link) 0)) "-rdynamic " "")
|
||||||
"-o '" out-path "' " (bld-link-libs)))
|
"-I'" bld-csv-dir "' '" main-c "' '" bld-csv-dir "/libkernel.a' "
|
||||||
|
"-o '" out-path "' " (bld-link-libs) native-link))
|
||||||
(display (string-append "jolt build: wrote " out-path "\n")))
|
(display (string-append "jolt build: wrote " out-path "\n")))
|
||||||
|
|
||||||
(def-var! "jolt.host" "build-binary"
|
(def-var! "jolt.host" "build-binary"
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,26 @@
|
||||||
|
|
||||||
(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")
|
||||||
|
|
@ -35,8 +55,9 @@
|
||||||
;; and exit non-zero, instead of Chez's opaque "non-condition value" dump. The
|
;; and exit non-zero, instead of Chez's opaque "non-condition value" dump. The
|
||||||
;; message/ex-data/cause + a mapped Clojure backtrace come from the shared
|
;; message/ex-data/cause + a mapped Clojure backtrace come from the shared
|
||||||
;; renderer (source-registry.ss); the cli adds the top-level source location.
|
;; renderer (source-registry.ss); the cli adds the top-level source location.
|
||||||
(define (jolt-report-uncaught v)
|
(define (jolt-report-uncaught raw)
|
||||||
(let ((port (current-error-port)))
|
(let ((v (jolt-unwrap-throw raw))
|
||||||
|
(port (current-error-port)))
|
||||||
(jolt-render-throwable v port)
|
(jolt-render-throwable v port)
|
||||||
;; The top-level form that was evaluating when this propagated (file:line:col).
|
;; The top-level form that was evaluating when this propagated (file:line:col).
|
||||||
(let ((loc (jolt-current-source-string)))
|
(let ((loc (jolt-current-source-string)))
|
||||||
|
|
@ -45,6 +66,9 @@
|
||||||
(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
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@
|
||||||
(vec-set (pvec-tail p) (fxand i pv-mask) x) #f)
|
(vec-set (pvec-tail p) (fxand i pv-mask) x) #f)
|
||||||
(mk-pvec cnt (pvec-shift p)
|
(mk-pvec cnt (pvec-shift p)
|
||||||
(pv-assoc-trie (pvec-shift p) (pvec-root p) i x) (pvec-tail p) #f)))
|
(pv-assoc-trie (pvec-shift p) (pvec-root p) i x) (pvec-tail p) #f)))
|
||||||
(else (error 'assoc "vector index out of bounds")))))
|
(else (jolt-throw (jolt-host-throwable "java.lang.IndexOutOfBoundsException" "vector index out of bounds"))))))
|
||||||
(define (pvec-peek p)
|
(define (pvec-peek p)
|
||||||
(let ((n (pvec-cnt p))) (if (fx=? n 0) jolt-nil (pvec-nth-d p (fx- n 1) jolt-nil))))
|
(let ((n (pvec-cnt p))) (if (fx=? n 0) jolt-nil (pvec-nth-d p (fx- n 1) jolt-nil))))
|
||||||
;; pop the last trie chunk back into the tail; #f means the subtree emptied.
|
;; pop the last trie chunk back into the tail; #f means the subtree emptied.
|
||||||
|
|
@ -298,8 +298,21 @@
|
||||||
(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).
|
;; map already holds 8 entries (array.length >= 16 in the reference). Clojure 1.13
|
||||||
|
;; 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)))))))
|
||||||
|
|
||||||
|
|
@ -310,7 +323,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 (fx<? cnt array-map-limit))
|
(if (and ord (pmap-array-keep? cnt ord k))
|
||||||
(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))))
|
||||||
|
|
@ -352,10 +365,14 @@
|
||||||
(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, hash map beyond (RT.map).
|
;; map LITERAL ({...}): array map up to 8 entries (64 if keyword-only, per 1.13),
|
||||||
|
;; 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) (if (fx>? (pmap-cnt m) array-map-limit) (pmap->hash m) m))
|
(cond ((null? kvs)
|
||||||
|
(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).
|
||||||
|
|
@ -404,9 +421,11 @@
|
||||||
(if (null? args)
|
(if (null? args)
|
||||||
(jolt-vector)
|
(jolt-vector)
|
||||||
(let ((coll (car args)) (xs (cdr args)))
|
(let ((coll (car args)) (xs (cdr args)))
|
||||||
(if (jolt-nil? coll)
|
(cond
|
||||||
(fold-left jolt-conj1 jolt-empty-list xs)
|
;; 1-arity returns the coll untouched — (conj nil) is nil
|
||||||
(meta-carry coll (fold-left jolt-conj1 coll xs))))))
|
((null? xs) coll)
|
||||||
|
((jolt-nil? coll) (fold-left jolt-conj1 jolt-empty-list xs))
|
||||||
|
(else (meta-carry coll (fold-left jolt-conj1 coll xs)))))))
|
||||||
|
|
||||||
;; A host shim registers a type's get via register-get-arm! (handler: (coll k d) ->
|
;; A host shim registers a type's get via register-get-arm! (handler: (coll k d) ->
|
||||||
;; value) instead of set!-wrapping jolt-get — disjoint coll types, checked before the
|
;; value) instead of set!-wrapping jolt-get — disjoint coll types, checked before the
|
||||||
|
|
@ -443,21 +462,28 @@
|
||||||
(define (rec-coll-method coll name)
|
(define (rec-coll-method coll name)
|
||||||
(and (jrec? coll) (find-method-any-protocol (jrec-tag coll) name)))
|
(and (jrec? coll) (find-method-any-protocol (jrec-tag coll) name)))
|
||||||
|
|
||||||
|
(define (jolt-nth-nil-idx! i)
|
||||||
|
(when (jolt-nil? i)
|
||||||
|
(jolt-throw (jolt-host-throwable "java.lang.NullPointerException" "nth index"))))
|
||||||
(define jolt-nth
|
(define jolt-nth
|
||||||
(case-lambda
|
(case-lambda
|
||||||
((coll i)
|
((coll i)
|
||||||
|
(jolt-nth-nil-idx! i)
|
||||||
(let ((i (->idx i)))
|
(let ((i (->idx i)))
|
||||||
(cond ((pvec? coll) (let ((v (pvec-v coll)))
|
(cond ((jolt-nil? coll) jolt-nil) ; RT.nth(nil, i) is nil at any index
|
||||||
|
((pvec? coll) (let ((v (pvec-v coll)))
|
||||||
(if (and (fx>=? i 0) (fx<? i (vector-length v))) (vector-ref v i)
|
(if (and (fx>=? i 0) (fx<? i (vector-length v))) (vector-ref v i)
|
||||||
(error 'nth "index out of bounds"))))
|
(jolt-throw (jolt-host-throwable "java.lang.IndexOutOfBoundsException" "index out of bounds")))))
|
||||||
((string? coll) (if (and (fx>=? i 0) (fx<? i (string-length coll))) (string-ref coll i)
|
((string? coll) (if (and (fx>=? i 0) (fx<? i (string-length coll))) (string-ref coll i)
|
||||||
(error 'nth "index out of bounds")))
|
(jolt-throw (jolt-host-throwable "java.lang.IndexOutOfBoundsException" "index out of bounds"))))
|
||||||
((or (cseq? coll) (empty-list-t? coll)) (seq-nth coll i #f jolt-nil))
|
((or (cseq? coll) (empty-list-t? coll)) (seq-nth coll i #f jolt-nil))
|
||||||
((rec-coll-method coll "nth") => (lambda (m) (jolt-invoke m coll i)))
|
((rec-coll-method coll "nth") => (lambda (m) (jolt-invoke m coll i)))
|
||||||
(else (error 'nth "unsupported collection")))))
|
(else (error 'nth "unsupported collection")))))
|
||||||
((coll i d)
|
((coll i d)
|
||||||
|
(jolt-nth-nil-idx! i)
|
||||||
(let ((i (->idx i)))
|
(let ((i (->idx i)))
|
||||||
(cond ((pvec? coll) (pvec-nth-d coll i d))
|
(cond ((jolt-nil? coll) d) ; RT.nth(nil, i, notFound) is notFound
|
||||||
|
((pvec? coll) (pvec-nth-d coll i d))
|
||||||
((string? coll) (if (and (fx>=? i 0) (fx<? i (string-length coll))) (string-ref coll i) d))
|
((string? coll) (if (and (fx>=? i 0) (fx<? i (string-length coll))) (string-ref coll i) d))
|
||||||
((or (cseq? coll) (empty-list-t? coll)) (seq-nth coll i #t d))
|
((or (cseq? coll) (empty-list-t? coll)) (seq-nth coll i #t d))
|
||||||
((rec-coll-method coll "nth") => (lambda (m) (jolt-invoke m coll i d)))
|
((rec-coll-method coll "nth") => (lambda (m) (jolt-invoke m coll i d)))
|
||||||
|
|
@ -502,6 +528,21 @@
|
||||||
((pset? coll) (pset-contains? coll k))
|
((pset? coll) (pset-contains? coll k))
|
||||||
((pvec? coll) (let ((k (->idx k))) (and (fixnum? k) (fx>=? k 0) (fx<? k (pvec-count coll)))))
|
((pvec? coll) (let ((k (->idx k))) (and (fixnum? k) (fx>=? k 0) (fx<? k (pvec-count coll)))))
|
||||||
((jolt-nil? coll) #f)
|
((jolt-nil? coll) #f)
|
||||||
|
;; a string supports contains? by INDEX only (RT.contains: CharSequence +
|
||||||
|
;; Number key); any other key — or any unsupported type — is the JVM's
|
||||||
|
;; IllegalArgumentException.
|
||||||
|
((string? coll)
|
||||||
|
(if (and (number? k) (exact? k) (integer? k))
|
||||||
|
(and (>= k 0) (< k (string-length coll)))
|
||||||
|
(jolt-throw (jolt-host-throwable
|
||||||
|
"java.lang.IllegalArgumentException"
|
||||||
|
"contains? not supported on type: java.lang.String"))))
|
||||||
|
((or (cseq? coll) (empty-list-t? coll) (number? coll) (boolean? coll)
|
||||||
|
(keyword? coll) (jolt-symbol? coll) (char? coll))
|
||||||
|
(jolt-throw (jolt-host-throwable
|
||||||
|
"java.lang.IllegalArgumentException"
|
||||||
|
(string-append "contains? not supported on type: "
|
||||||
|
(guard (e (#t "?")) (jolt-class-name coll))))))
|
||||||
(else #f)))
|
(else #f)))
|
||||||
|
|
||||||
(define (jolt-empty? coll)
|
(define (jolt-empty? coll)
|
||||||
|
|
@ -514,15 +555,25 @@
|
||||||
((cseq? coll) #f) ; a cseq is non-empty by construction
|
((cseq? coll) #f) ; a cseq is non-empty by construction
|
||||||
(else (error 'empty? "unsupported collection"))))
|
(else (error 'empty? "unsupported collection"))))
|
||||||
|
|
||||||
|
(define (jolt-stack-throw coll)
|
||||||
|
(jolt-throw (jolt-host-throwable
|
||||||
|
"java.lang.ClassCastException"
|
||||||
|
(string-append "class " (guard (e (#t "?")) (jolt-class-name coll))
|
||||||
|
" cannot be cast to class clojure.lang.IPersistentStack"))))
|
||||||
(define (jolt-peek coll)
|
(define (jolt-peek coll)
|
||||||
(cond ((pvec? coll) (pvec-peek coll))
|
(cond ((pvec? coll) (pvec-peek coll))
|
||||||
((or (cseq? coll) (empty-list-t? coll)) (jolt-first coll)) ; list peek = first
|
;; list peek = first; a non-list seq (range, a rest chain) is not an
|
||||||
((jolt-nil? coll) jolt-nil) (else (error 'peek "unsupported collection"))))
|
;; IPersistentStack on the JVM
|
||||||
|
((and (cseq? coll) (cseq-list? coll)) (jolt-first coll))
|
||||||
|
((empty-list-t? coll) (jolt-first coll))
|
||||||
|
((jolt-nil? coll) jolt-nil)
|
||||||
|
(else (jolt-stack-throw coll))))
|
||||||
(define (jolt-pop coll)
|
(define (jolt-pop coll)
|
||||||
(cond ((pvec? coll) (meta-carry coll (pvec-pop coll)))
|
(cond ((jolt-nil? coll) jolt-nil) ; RT.pop(nil) is nil
|
||||||
((cseq? coll) (meta-carry coll (jolt-rest coll))) ; list pop = rest
|
((pvec? coll) (meta-carry coll (pvec-pop coll)))
|
||||||
|
((and (cseq? coll) (cseq-list? coll)) (meta-carry coll (jolt-rest coll)))
|
||||||
((empty-list-t? coll) (error 'pop "can't pop empty list"))
|
((empty-list-t? coll) (error 'pop "can't pop empty list"))
|
||||||
(else (error 'pop "unsupported collection"))))
|
(else (jolt-stack-throw coll))))
|
||||||
|
|
||||||
;; ============================================================================
|
;; ============================================================================
|
||||||
;; equality / hash hooks called from values.ss (jolt=2 / jolt-hash)
|
;; equality / hash hooks called from values.ss (jolt=2 / jolt-hash)
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,48 @@
|
||||||
;; 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
|
||||||
|
;; reader metadata as a with-meta form; strip it to read the bare ns symbol.
|
||||||
|
(define (ce-unwrap-meta x)
|
||||||
|
(if (and (cseq? x) (cseq-list? x))
|
||||||
|
(let ((items (seq->list x)))
|
||||||
|
(if (and (pair? items) (symbol-t? (car items))
|
||||||
|
(string=? (symbol-t-name (car items)) "with-meta") (pair? (cdr items)))
|
||||||
|
(cadr items) x))
|
||||||
|
x))
|
||||||
|
|
||||||
;; (quote X) -> X, else x — unwraps a quoted require spec.
|
;; (quote X) -> X, else x — unwraps a quoted require spec.
|
||||||
(define (ce-unquote x)
|
(define (ce-unquote x)
|
||||||
|
|
@ -135,14 +177,22 @@
|
||||||
;; (require spec...) / (use spec...) — specs are quoted
|
;; (require spec...) / (use spec...) — specs are quoted
|
||||||
((and hn (or (string=? hn "require") (string=? hn "use")))
|
((and hn (or (string=? hn "require") (string=? hn "use")))
|
||||||
(for-each (lambda (a) (chez-register-spec! ns (ce-unquote a))) (cdr items)))
|
(for-each (lambda (a) (chez-register-spec! ns (ce-unquote a))) (cdr items)))
|
||||||
;; (ns name (:require [a :as x]) ...) — clause specs are literal
|
;; (ns name (:require [a :as x]) ...) — clause specs are literal. Register
|
||||||
|
;; the aliases under NAME (the ns being defined), not the passed `ns`:
|
||||||
|
;; when a file is loaded its ns form compiles while (chez-current-ns) is
|
||||||
|
;; still the requiring ns, so using `ns` would leak the loaded ns's
|
||||||
|
;; aliases into its requirer and clobber a same-named alias there
|
||||||
|
;; (rewrite-clj.zip.base's [node.protocols :as node] over the caller's node).
|
||||||
((and hn (string=? hn "ns"))
|
((and hn (string=? hn "ns"))
|
||||||
|
(let ((ns-name (if (and (pair? (cdr items)) (symbol-t? (ce-unwrap-meta (cadr items))))
|
||||||
|
(symbol-t-name (ce-unwrap-meta (cadr items)))
|
||||||
|
ns)))
|
||||||
(for-each (lambda (clause)
|
(for-each (lambda (clause)
|
||||||
(when (and (cseq? clause) (cseq-list? clause))
|
(when (and (cseq? clause) (cseq-list? clause))
|
||||||
(let ((cl (seq->list clause)))
|
(let ((cl (seq->list clause)))
|
||||||
(when (ce-clause-require? cl)
|
(when (ce-clause-require? cl)
|
||||||
(for-each (lambda (spec) (chez-register-spec! ns spec)) (cdr cl))))))
|
(for-each (lambda (spec) (chez-register-spec! ns-name spec)) (cdr cl))))))
|
||||||
(if (pair? (cdr items)) (cddr items) '())))
|
(if (pair? (cdr items)) (cddr items) '()))))
|
||||||
(else (for-each (lambda (x) (ce-scan-requires! x ns)) items))))))))
|
(else (for-each (lambda (x) (ce-scan-requires! x ns)) items))))))))
|
||||||
|
|
||||||
;; Already-read FORM -> Scheme source string (analyze -> emit on Chez).
|
;; Already-read FORM -> Scheme source string (analyze -> emit on Chez).
|
||||||
|
|
@ -193,7 +243,13 @@
|
||||||
;; A top-level (do ...) is UNROLLED — each subform compiled+eval'd in turn, like
|
;; A top-level (do ...) is UNROLLED — each subform compiled+eval'd in turn, like
|
||||||
;; Clojure's top-level do — so a runtime defmacro/def in an earlier subform is
|
;; Clojure's top-level do — so a runtime defmacro/def in an earlier subform is
|
||||||
;; visible (macro flag set, var interned) before a later subform is analyzed.
|
;; visible (macro flag set, var interned) before a later subform is analyzed.
|
||||||
|
;; a non-form VALUE (a function object, a BigDecimal, a reference type)
|
||||||
|
;; self-evaluates, like eval on the JVM.
|
||||||
(define (jolt-compile-eval-form form ns)
|
(define (jolt-compile-eval-form form ns)
|
||||||
|
(if (or (procedure? form) (jbigdec? form) (jolt-atom? form) (jolt-multifn? form))
|
||||||
|
form
|
||||||
|
(jolt-compile-eval-form* form ns)))
|
||||||
|
(define (jolt-compile-eval-form* form ns)
|
||||||
(cond
|
(cond
|
||||||
;; thread the current ns: an earlier subform may switch it (ns/in-ns call
|
;; thread the current ns: an earlier subform may switch it (ns/in-ns call
|
||||||
;; set-chez-ns!), and the next subform must be ANALYZED in that ns so its defs
|
;; set-chez-ns!), and the next subform must be ANALYZED in that ns so its defs
|
||||||
|
|
@ -211,6 +267,9 @@
|
||||||
;; 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)))))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -117,20 +117,21 @@
|
||||||
(let ((a (car args)))
|
(let ((a (car args)))
|
||||||
(cond
|
(cond
|
||||||
((jolt-symbol? a) a)
|
((jolt-symbol? a) a)
|
||||||
;; (symbol "ns/name") splits the namespace at the LAST "/" (JVM
|
;; (symbol "ns/name") splits the namespace at the FIRST "/" (JVM
|
||||||
;; Symbol.intern), so (namespace (symbol "foo/bar")) => "foo". A lone "/"
|
;; Symbol.intern), so (namespace (symbol "foo/bar/baz")) => "foo" with
|
||||||
;; or a leading slash has no namespace. The no-ns sentinel is #f — matches
|
;; name "bar/baz". A lone "/" or a leading slash has no namespace. The
|
||||||
;; emit's quoted-symbol lowering (jolt-symbol #f "x"), so (= 'x (symbol
|
;; no-ns sentinel is #f — matches emit's quoted-symbol lowering
|
||||||
;; "x")) holds (jolt= compares ns with strict equal?).
|
;; (jolt-symbol #f "x"), so (= 'x (symbol "x")) holds (jolt= compares
|
||||||
|
;; ns with strict equal?).
|
||||||
((string? a)
|
((string? a)
|
||||||
(let ((slen (string-length a)))
|
(let ((slen (string-length a)))
|
||||||
(if (string=? a "/")
|
(if (string=? a "/")
|
||||||
(jolt-symbol #f "/")
|
(jolt-symbol #f "/")
|
||||||
(let loop ((i (- slen 1)))
|
(let loop ((i 1))
|
||||||
(cond ((<= i 0) (jolt-symbol #f a))
|
(cond ((>= i slen) (jolt-symbol #f a))
|
||||||
((char=? (string-ref a i) #\/)
|
((char=? (string-ref a i) #\/)
|
||||||
(jolt-symbol (substring a 0 i) (substring a (+ i 1) slen)))
|
(jolt-symbol (substring a 0 i) (substring a (+ i 1) slen)))
|
||||||
(else (loop (- i 1))))))))
|
(else (loop (+ i 1))))))))
|
||||||
((keyword? a) (jolt-symbol (keyword-t-ns a) (keyword-t-name a)))
|
((keyword? a) (jolt-symbol (keyword-t-ns a) (keyword-t-name a)))
|
||||||
;; (symbol a-var) -> the var's qualified symbol (clojure.spec.alpha/->sym).
|
;; (symbol a-var) -> the var's qualified symbol (clojure.spec.alpha/->sym).
|
||||||
((var-cell? a) (jolt-symbol (var-cell-ns a) (var-cell-name a)))
|
((var-cell? a) (jolt-symbol (var-cell-ns a) (var-cell-name a)))
|
||||||
|
|
@ -155,7 +156,12 @@
|
||||||
;; int/long: truncate toward zero to an EXACT integer (= JVM long). char -> code
|
;; int/long: truncate toward zero to an EXACT integer (= JVM long). char -> code
|
||||||
;; point (exact). double: always a flonum (= JVM double).
|
;; point (exact). double: always a flonum (= JVM double).
|
||||||
(define (jolt-int x) (if (char? x) (char->integer x) (exact (truncate x))))
|
(define (jolt-int x) (if (char? x) (char->integer x) (exact (truncate x))))
|
||||||
(define (jolt-double x) (if (char? x) (exact->inexact (char->integer x)) (exact->inexact x)))
|
;; a numeric type outside Chez's tower converts through this hook (bigdec).
|
||||||
|
(define (jolt-double-slow x) (jolt-num-cast-throw x))
|
||||||
|
(define (jolt-double x)
|
||||||
|
(cond ((char? x) (exact->inexact (char->integer x)))
|
||||||
|
((number? x) (exact->inexact x))
|
||||||
|
(else (jolt-double-slow x))))
|
||||||
|
|
||||||
;; compare: 3-way, returns an EXACT integer (= JVM compare -> int).
|
;; compare: 3-way, returns an EXACT integer (= JVM compare -> int).
|
||||||
(define (jolt-cmp3 x y) (cond ((< x y) -1) ((> x y) 1) (else 0)))
|
(define (jolt-cmp3 x y) (cond ((< x y) -1) ((> x y) 1) (else 0)))
|
||||||
|
|
@ -172,7 +178,11 @@
|
||||||
((jolt-nil? b) 1)
|
((jolt-nil? b) 1)
|
||||||
((and (number? a) (number? b)) (jolt-cmp3 a b))
|
((and (number? a) (number? b)) (jolt-cmp3 a b))
|
||||||
((and (string? a) (string? b)) (jolt-strcmp a b))
|
((and (string? a) (string? b)) (jolt-strcmp a b))
|
||||||
((and (keyword? a) (keyword? b)) (jolt-strcmp (jolt-kw->string a) (jolt-kw->string b)))
|
;; keywords order like symbols: a nil namespace sorts before any namespace,
|
||||||
|
;; then by namespace, then by name (Keyword.compareTo -> Symbol.compareTo)
|
||||||
|
((and (keyword? a) (keyword? b))
|
||||||
|
(let ((r (jolt-strcmp (or (keyword-t-ns a) "") (or (keyword-t-ns b) ""))))
|
||||||
|
(if (= r 0) (jolt-strcmp (keyword-t-name a) (keyword-t-name b)) r)))
|
||||||
((and (jolt-symbol? a) (jolt-symbol? b))
|
((and (jolt-symbol? a) (jolt-symbol? b))
|
||||||
(let ((r (jolt-strcmp (jolt-sym-ns-string a) (jolt-sym-ns-string b))))
|
(let ((r (jolt-strcmp (jolt-sym-ns-string a) (jolt-sym-ns-string b))))
|
||||||
(if (= r 0) (jolt-strcmp (symbol-t-name a) (symbol-t-name b)) r)))
|
(if (= r 0) (jolt-strcmp (symbol-t-name a) (symbol-t-name b)) r)))
|
||||||
|
|
@ -195,16 +205,84 @@
|
||||||
(def-var! "clojure.core" "keyword" jolt-keyword)
|
(def-var! "clojure.core" "keyword" jolt-keyword)
|
||||||
(def-var! "clojure.core" "symbol" jolt-symbol-new)
|
(def-var! "clojure.core" "symbol" jolt-symbol-new)
|
||||||
(def-var! "clojure.core" "gensym" jolt-gensym)
|
(def-var! "clojure.core" "gensym" jolt-gensym)
|
||||||
(def-var! "clojure.core" "int" jolt-int)
|
;; --- checked narrow casts (RT.byteCast/shortCast/intCast/longCast/charCast) --
|
||||||
;; char: coerce a code point (jolt's all-flonum number) to a Chez char; pass a
|
;; One helper carries the JVM ranges: truncate toward zero, then range-check.
|
||||||
;; char through. Inverse of int on chars. The cross-compiled emitter's
|
;; NaN casts to 0 (Java (long)NaN); an out-of-range value (including a float
|
||||||
;; chez-str-lit needs it for printable-ASCII escaping.
|
;; infinity) is IllegalArgumentException "Value out of range for <type>: x".
|
||||||
(define (jolt-char x) (if (char? x) x (integer->char (exact (round x)))))
|
;; A non-numeric operand is the usual ClassCastException. Numeric types outside
|
||||||
|
;; Chez's tower truncate through a hook the shim extends (BigDecimal).
|
||||||
|
(define (jolt-cast-range-throw name x)
|
||||||
|
(jolt-throw (jolt-host-throwable
|
||||||
|
"java.lang.IllegalArgumentException"
|
||||||
|
(string-append "Value out of range for " name ": " (jolt-str x)))))
|
||||||
|
(define (jolt-cast-truncate-slow x) (jolt-num-cast-throw x))
|
||||||
|
(define (jolt-checked-cast name lo hi x)
|
||||||
|
(let ((n (cond ((char? x) (char->integer x))
|
||||||
|
((and (number? x) (exact? x)) (truncate x))
|
||||||
|
;; a double range-checks ITSELF (before truncation): (byte
|
||||||
|
;; 127.000001) throws, (byte 1.1) is 1; NaN casts to 0; an
|
||||||
|
;; infinity always fails the compare.
|
||||||
|
((flonum? x) (cond ((nan? x) 0)
|
||||||
|
((or (< x lo) (> x hi)) (+ hi 1))
|
||||||
|
(else (exact (truncate x)))))
|
||||||
|
(else (jolt-cast-truncate-slow x)))))
|
||||||
|
(if (and (>= n lo) (<= n hi)) n (jolt-cast-range-throw name x))))
|
||||||
|
(define (jolt-byte-cast x) (jolt-checked-cast "byte" -128 127 x))
|
||||||
|
(define (jolt-short-cast x) (jolt-checked-cast "short" -32768 32767 x))
|
||||||
|
(define (jolt-int-cast x) (jolt-checked-cast "int" -2147483648 2147483647 x))
|
||||||
|
(define (jolt-long-cast x) (jolt-checked-cast "long" -9223372036854775808 9223372036854775807 x))
|
||||||
|
(def-var! "clojure.core" "int" jolt-int-cast)
|
||||||
|
(def-var! "clojure.core" "long" jolt-long-cast)
|
||||||
|
(def-var! "clojure.core" "byte" jolt-byte-cast)
|
||||||
|
(def-var! "clojure.core" "short" jolt-short-cast)
|
||||||
|
;; char: pass a char through; a code point must be in [0, 0xFFFF] (charCast).
|
||||||
|
(define (jolt-char x)
|
||||||
|
(if (char? x) x (integer->char (jolt-checked-cast "char" 0 65535 x))))
|
||||||
(def-var! "clojure.core" "char" jolt-char)
|
(def-var! "clojure.core" "char" jolt-char)
|
||||||
;; long: same truncation as int in jolt's all-flonum model (seed core-long =
|
;; unchecked-long: truncate + wrap to 64 bits (RT.uncheckedLongCast — a float
|
||||||
;; math/trunc; char -> code point). Distinct cell so (long ...) resolves.
|
;; infinity saturates, NaN is 0). unchecked-int wraps and sign-folds to 32.
|
||||||
(def-var! "clojure.core" "long" jolt-int)
|
(define (jolt-cast-saturate n lo hi) (cond ((< n lo) lo) ((> n hi) hi) (else n)))
|
||||||
|
(define (jolt-unchecked-long x)
|
||||||
|
(cond ((char? x) (char->integer x))
|
||||||
|
;; an exact integer wraps (long narrowing); a double SATURATES (Java's
|
||||||
|
;; double->long conversion clamps at the bounds, NaN is 0).
|
||||||
|
((and (number? x) (exact? x)) (jolt-wrap64 (truncate x)))
|
||||||
|
((flonum? x) (if (nan? x) 0
|
||||||
|
(jolt-cast-saturate (if (infinite? x) (if (> x 0.0) unc-2^63 (- unc-2^63)) (exact (truncate x)))
|
||||||
|
-9223372036854775808 9223372036854775807)))
|
||||||
|
(else (jolt-wrap64 (jolt-cast-truncate-slow x)))))
|
||||||
|
(define (jolt-unchecked-int x)
|
||||||
|
(if (flonum? x)
|
||||||
|
;; double->int clamps like Java
|
||||||
|
(if (nan? x) 0
|
||||||
|
(jolt-cast-saturate (if (infinite? x) (if (> x 0.0) #x80000000 (- #x80000000)) (exact (truncate x)))
|
||||||
|
-2147483648 2147483647))
|
||||||
|
(let ((i (bitwise-and (jolt-unchecked-long x) #xffffffff)))
|
||||||
|
(if (>= i #x80000000) (- i #x100000000) i))))
|
||||||
|
(def-var! "clojure.core" "unchecked-long" jolt-unchecked-long)
|
||||||
|
(def-var! "clojure.core" "unchecked-int" jolt-unchecked-int)
|
||||||
(def-var! "clojure.core" "double" jolt-double)
|
(def-var! "clojure.core" "double" jolt-double)
|
||||||
;; float: Chez has no single-float type, so float coerces to a flonum like double.
|
;; float: Chez has no single-float type, so the value stays a flonum — but the
|
||||||
(def-var! "clojure.core" "float" jolt-double)
|
;; cast range-checks against Float/MAX_VALUE like RT.floatCast (an infinity is
|
||||||
|
;; out of range; NaN passes).
|
||||||
|
(define fl-float-max 3.4028234663852886e38)
|
||||||
|
(define (jolt-float x)
|
||||||
|
(let ((d (jolt-double x)))
|
||||||
|
(if (and (flonum? d) (not (nan? d))
|
||||||
|
(or (< d (- fl-float-max)) (> d fl-float-max)))
|
||||||
|
(jolt-cast-range-throw "float" x)
|
||||||
|
d)))
|
||||||
|
(def-var! "clojure.core" "float" jolt-float)
|
||||||
|
;; numerator/denominator: jolt ratios are Chez exact rationals; a non-ratio is
|
||||||
|
;; the JVM's Ratio cast failure.
|
||||||
|
(define (jolt-ratio-part name f)
|
||||||
|
(lambda (x)
|
||||||
|
(if (and (number? x) (exact? x) (rational? x) (not (integer? x)))
|
||||||
|
(f x)
|
||||||
|
(jolt-throw (jolt-host-throwable
|
||||||
|
"java.lang.ClassCastException"
|
||||||
|
(string-append "class " (guard (e (#t "?")) (jolt-class-name x))
|
||||||
|
" cannot be cast to class clojure.lang.Ratio"))))))
|
||||||
|
(def-var! "clojure.core" "numerator" (jolt-ratio-part "numerator" numerator))
|
||||||
|
(def-var! "clojure.core" "denominator" (jolt-ratio-part "denominator" denominator))
|
||||||
(def-var! "clojure.core" "compare" jolt-compare)
|
(def-var! "clojure.core" "compare" jolt-compare)
|
||||||
|
|
|
||||||
120
host/chez/cts.sh
Executable file
120
host/chez/cts.sh
Executable file
|
|
@ -0,0 +1,120 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# clojure-test-suite gate: run the vendored jank-lang/clojure-test-suite
|
||||||
|
# (vendor/clojure-test-suite) against joltc, one process per test namespace (a
|
||||||
|
# hang or crash is contained), and compare per-namespace fail/error counts
|
||||||
|
# against the checked-in baseline test/chez/cts-known-failures.txt.
|
||||||
|
#
|
||||||
|
# The comparison is exact, like certify's allowlist: a namespace doing WORSE
|
||||||
|
# than the baseline fails the gate (regression), and one doing BETTER also
|
||||||
|
# fails (stale baseline — update the file in the same change that improved it).
|
||||||
|
#
|
||||||
|
# JOLT_CTS_JOBS=N parallel workers (default 4)
|
||||||
|
# JOLT_CTS_TIMEOUT=SECS per-namespace timeout (default 120)
|
||||||
|
# JOLT_CTS_WRITE_BASELINE=1 regenerate the baseline file instead of gating
|
||||||
|
# JOLT_CTS_NS=ns1,ns2 run only these namespaces, verbose, no gating
|
||||||
|
set -u
|
||||||
|
root="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
||||||
|
cd "$root"
|
||||||
|
|
||||||
|
suite="vendor/clojure-test-suite/test"
|
||||||
|
baseline="test/chez/cts-known-failures.txt"
|
||||||
|
app="$root/test/chez/cts-app"
|
||||||
|
jobs="${JOLT_CTS_JOBS:-4}"
|
||||||
|
tmo="${JOLT_CTS_TIMEOUT:-120}"
|
||||||
|
|
||||||
|
if [ ! -d "$suite/clojure" ]; then
|
||||||
|
echo "cts: skipped (git submodule update --init vendor/clojure-test-suite)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
work="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$work"' EXIT
|
||||||
|
|
||||||
|
# test namespaces from the .cljc files (portability is a helper, not a test ns)
|
||||||
|
find "$suite" -name '*.cljc' | sed "s|^$suite/||;s|\.cljc$||;s|/|.|g;s|_|-|g" \
|
||||||
|
| grep -v '\.portability$' | sort > "$work/nses"
|
||||||
|
if [ -n "${JOLT_CTS_NS:-}" ]; then
|
||||||
|
echo "${JOLT_CTS_NS}" | tr ',' '\n' > "$work/nses"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# round-robin the namespaces over N sequential workers; each worker appends
|
||||||
|
# "ns pass fail error" lines (HUNG/CRASH in the pass column) to its own file.
|
||||||
|
awk -v j="$jobs" '{print > ("'"$work"'/chunk." (NR % j))}' "$work/nses"
|
||||||
|
run_chunk() {
|
||||||
|
chunk="$1"; out="$2"
|
||||||
|
while IFS= read -r ns; do
|
||||||
|
res=$(JOLT_PWD="$app" perl -e "alarm $tmo; exec @ARGV" -- "$root/bin/joltc" -M:cts "$ns" 2>&1 </dev/null)
|
||||||
|
rc=$?
|
||||||
|
line=$(echo "$res" | grep '^CTS-RESULT' | head -1)
|
||||||
|
if [ -n "$line" ]; then
|
||||||
|
echo "$line" | awk '{print $2, $3, $4, $5}' >> "$out"
|
||||||
|
if [ -n "${JOLT_CTS_NS:-}" ]; then
|
||||||
|
echo "$res" | grep -E 'FAIL:|ERROR:|LOAD:' | sed 's/^/ /' >> "$out"
|
||||||
|
fi
|
||||||
|
elif [ $rc -ge 128 ]; then
|
||||||
|
echo "$ns HUNG 0 0" >> "$out"
|
||||||
|
else
|
||||||
|
echo "$ns CRASH 0 0" >> "$out"
|
||||||
|
fi
|
||||||
|
done < "$chunk"
|
||||||
|
}
|
||||||
|
for c in "$work"/chunk.*; do
|
||||||
|
run_chunk "$c" "$c.res" &
|
||||||
|
done
|
||||||
|
wait
|
||||||
|
cat "$work"/chunk.*.res 2>/dev/null | sort > "$work/results"
|
||||||
|
|
||||||
|
if [ -n "${JOLT_CTS_NS:-}" ]; then
|
||||||
|
cat "$work/results"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
summary=$(awk '$2!="HUNG" && $2!="CRASH" {p+=$2; f+=$3; e+=$4; c++}
|
||||||
|
$2=="HUNG" {h++} $2=="CRASH" {x++}
|
||||||
|
END {printf "%d namespaces: pass %d, fail %d, error %d, hung %d, crash %d",
|
||||||
|
c+h+x, p, f, e, h, x}' "$work/results")
|
||||||
|
|
||||||
|
if [ "${JOLT_CTS_WRITE_BASELINE:-0}" = "1" ]; then
|
||||||
|
{
|
||||||
|
echo "# clojure-test-suite known failures: <namespace> <fail> <error>"
|
||||||
|
echo "# The gate fails on any per-namespace change, worse OR better; regenerate"
|
||||||
|
echo "# with: JOLT_CTS_WRITE_BASELINE=1 host/chez/cts.sh"
|
||||||
|
awk '$2=="HUNG" || $2=="CRASH" {print $1, $2, $2; next}
|
||||||
|
$3 != 0 || $4 != 0 {print $1, $3, $4}' "$work/results"
|
||||||
|
} > "$baseline"
|
||||||
|
echo "cts: $summary"
|
||||||
|
echo "cts: baseline written to $baseline ($(grep -cv '^#' "$baseline") namespaces)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$baseline" ]; then
|
||||||
|
echo "cts: FAIL — no baseline; run JOLT_CTS_WRITE_BASELINE=1 host/chez/cts.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
status=0
|
||||||
|
while read -r ns p f e; do
|
||||||
|
case "$p" in HUNG|CRASH) f="$p"; e="$p" ;; esac
|
||||||
|
bl=$(grep -v '^#' "$baseline" | awk -v n="$ns" '$1==n {print $2, $3; exit}')
|
||||||
|
if [ -n "$bl" ]; then bf="${bl%% *}"; be="${bl##* }"; else bf=0; be=0; fi
|
||||||
|
if [ "$f" = "$bf" ] && [ "$e" = "$be" ]; then
|
||||||
|
continue
|
||||||
|
elif [ "$f" = "HUNG" ] || [ "$f" = "CRASH" ] \
|
||||||
|
|| { [ "$bf" != "HUNG" ] && [ "$bf" != "CRASH" ] \
|
||||||
|
&& { [ "$f" -gt "$bf" ] || [ "$e" -gt "$be" ]; }; }; then
|
||||||
|
echo "cts: NEW regression in $ns — fail $f error $e (baseline $bf $be)"
|
||||||
|
status=1
|
||||||
|
else
|
||||||
|
echo "cts: STALE baseline for $ns — now fail $f error $e (baseline $bf $be); update $baseline"
|
||||||
|
status=1
|
||||||
|
fi
|
||||||
|
done < "$work/results"
|
||||||
|
|
||||||
|
# a baseline entry whose namespace no longer reports is stale too
|
||||||
|
while read -r ns bf be; do
|
||||||
|
grep -q "^$ns " "$work/results" || { echo "cts: STALE baseline entry $ns (namespace gone)"; status=1; }
|
||||||
|
done < <(grep -v '^#' "$baseline")
|
||||||
|
|
||||||
|
echo "cts: $summary"
|
||||||
|
if [ $status -eq 0 ]; then echo "cts: passed (matches baseline)"; else echo "cts: FAILED"; fi
|
||||||
|
exit $status
|
||||||
|
|
@ -90,7 +90,10 @@
|
||||||
;; str re-serializes the read form (compiled identically; comments/whitespace are
|
;; str re-serializes the read form (compiled identically; comments/whitespace are
|
||||||
;; irrelevant).
|
;; irrelevant).
|
||||||
(define (dce-blob-records path)
|
(define (dce-blob-records path)
|
||||||
(call-with-input-file path
|
;; bld-source-string (build.ss) reads the embedded copy when running from a
|
||||||
|
;; self-contained joltc, else the file on disk — so tree-shake works with no
|
||||||
|
;; jolt checkout present. Forward ref: build.ss loads after this file.
|
||||||
|
(call-with-port (open-input-string (bld-source-string path))
|
||||||
(lambda (p)
|
(lambda (p)
|
||||||
(let loop ((acc '()))
|
(let loop ((acc '()))
|
||||||
(let ((form (read p)))
|
(let ((form (read p)))
|
||||||
|
|
|
||||||
|
|
@ -77,14 +77,23 @@
|
||||||
(let ((p (dyn-find-binding v)))
|
(let ((p (dyn-find-binding v)))
|
||||||
(if p
|
(if p
|
||||||
(begin (set-cdr! p val) val)
|
(begin (set-cdr! p val) val)
|
||||||
(begin (var-cell-root-set! v val) (var-cell-defined?-set! v #t) val)))
|
;; a ROOT change is Var.bindRoot: validate, set, notify watches
|
||||||
|
;; (a thread-binding set does not notify, like the JVM).
|
||||||
|
(let ((old (var-cell-root v)))
|
||||||
|
(iref-validate v val)
|
||||||
|
(var-cell-root-set! v val) (var-cell-defined?-set! v #t)
|
||||||
|
(iref-notify v old val)
|
||||||
|
val)))
|
||||||
(error #f "var-set: not a var" v)))
|
(error #f "var-set: not a var" v)))
|
||||||
|
|
||||||
;; alter-var-root: atomically apply f to the current root plus args.
|
;; alter-var-root: atomically apply f to the current root plus args.
|
||||||
(define (jolt-alter-var-root v f . args)
|
(define (jolt-alter-var-root v f . args)
|
||||||
(let ((new (apply jolt-invoke f (var-cell-root v) args)))
|
(let* ((old (var-cell-root v))
|
||||||
|
(new (apply jolt-invoke f old args)))
|
||||||
|
(iref-validate v new)
|
||||||
(var-cell-root-set! v new)
|
(var-cell-root-set! v new)
|
||||||
(var-cell-defined?-set! v #t)
|
(var-cell-defined?-set! v #t)
|
||||||
|
(iref-notify v old new)
|
||||||
new))
|
new))
|
||||||
|
|
||||||
;; __local-var: a fresh free-standing var cell (not interned). with-local-vars
|
;; __local-var: a fresh free-standing var cell (not interned). with-local-vars
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,10 @@
|
||||||
;; 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))))
|
||||||
|
|
@ -63,15 +67,23 @@
|
||||||
;; the seed minter (ei-emit-ns: optimize? #f, guard? #t — tolerant, skips a form
|
;; the seed minter (ei-emit-ns: optimize? #f, guard? #t — tolerant, skips a form
|
||||||
;; that fails to emit) and `jolt build` (bld-emit-ns: optimize? #t, guard? #f —
|
;; that fails to emit) and `jolt build` (bld-emit-ns: optimize? #t, guard? #f —
|
||||||
;; strict, a failing form errors the build).
|
;; strict, a failing form errors the build).
|
||||||
|
;; A per-form transform applied to each read form before emit — the build sets it
|
||||||
|
;; to the data-reader rewrite (loader.ss ldr-apply-readers) so a registered #tag
|
||||||
|
;; literal compiles in a `jolt build` the same as it does in an interpreted load.
|
||||||
|
;; #f (the default, and during the seed mint where loader.ss isn't loaded) is no
|
||||||
|
;; transform, so emit-image.ss carries no loader dependency.
|
||||||
|
(define ei-emit-form-hook (make-parameter #f))
|
||||||
|
|
||||||
(define (ei-emit-ns* ns-name src optimize? guard?)
|
(define (ei-emit-ns* ns-name src optimize? guard?)
|
||||||
;; set the ns before reading so ::kw auto-resolves against this ns (the runtime
|
;; set the ns before reading so ::kw auto-resolves against this ns (the runtime
|
||||||
;; loader reads form-by-form after the ns form sets it; the cross-compile reads
|
;; loader reads form-by-form after the ns form sets it; the cross-compile reads
|
||||||
;; all forms up front, so set it here).
|
;; all forms up front, so set it here).
|
||||||
(set-chez-ns! ns-name)
|
(set-chez-ns! ns-name)
|
||||||
|
(let ((hook (ei-emit-form-hook)))
|
||||||
(let loop ((forms (ei-read-all src)) (acc '()))
|
(let loop ((forms (ei-read-all src)) (acc '()))
|
||||||
(if (null? forms)
|
(if (null? forms)
|
||||||
(reverse acc)
|
(reverse acc)
|
||||||
(let ((f (car forms)))
|
(let ((f (let ((f0 (car forms))) (if hook (hook f0) f0))))
|
||||||
(ce-scan-requires! f ns-name)
|
(ce-scan-requires! f ns-name)
|
||||||
(cond
|
(cond
|
||||||
((ei-ns-form? f) (loop (cdr forms) acc))
|
((ei-ns-form? f) (loop (cdr forms) acc))
|
||||||
|
|
@ -89,7 +101,7 @@
|
||||||
(ei-compile-form (make-analyze-ctx ns-name) f optimize?))))
|
(ei-compile-form (make-analyze-ctx ns-name) f optimize?))))
|
||||||
(loop (cdr forms)
|
(loop (cdr forms)
|
||||||
(if (and guard? (not scm)) acc
|
(if (and guard? (not scm)) acc
|
||||||
(cons (if guard? (string-append "(guard (e (#t #f))\n " scm ")") scm) acc))))))))))
|
(cons (if guard? (string-append "(guard (e (#t #f))\n " scm ")") scm) acc)))))))))))
|
||||||
|
|
||||||
(define (ei-emit-ns ns-name src) (ei-emit-ns* ns-name src #f #t))
|
(define (ei-emit-ns ns-name src) (ei-emit-ns* ns-name src #f #t))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,12 @@
|
||||||
;; a qualified ns may be a require :as alias (s/split -> clojure.string/split)
|
;; a qualified ns may be a require :as alias (s/split -> clojure.string/split)
|
||||||
(let ((target (or (chez-resolve-alias (chez-actx-cns ctx) qualified) qualified)))
|
(let ((target (or (chez-resolve-alias (chez-actx-cns ctx) qualified) qualified)))
|
||||||
(var-cell-lookup target nm))
|
(var-cell-lookup target nm))
|
||||||
(or (var-cell-lookup (chez-actx-cns ctx) nm)
|
(or (let ((c (var-cell-lookup (chez-actx-cns ctx) nm)))
|
||||||
|
;; an undefined forward-intern must not shadow a real referred
|
||||||
|
;; or clojure.core var — e.g. the compiler ns referencing `set`,
|
||||||
|
;; which late-binds (interns `jolt.backend-scheme/set` undefined)
|
||||||
|
;; and would otherwise hide clojure.core/set on the mint fixpoint.
|
||||||
|
(and c (var-cell-defined? c) c))
|
||||||
;; a :refer'd name resolves to its source ns
|
;; a :refer'd name resolves to its source ns
|
||||||
(let ((ref (chez-resolve-refer (chez-actx-cns ctx) nm)))
|
(let ((ref (chez-resolve-refer (chez-actx-cns ctx) nm)))
|
||||||
(and ref (var-cell-lookup ref nm)))
|
(and ref (var-cell-lookup ref nm)))
|
||||||
|
|
@ -425,6 +430,19 @@
|
||||||
(define (hc-record-ctor-key ctx name)
|
(define (hc-record-ctor-key ctx name)
|
||||||
(let ((nm (hc-record-tag-name name)))
|
(let ((nm (hc-record-tag-name name)))
|
||||||
(or (and nm (chez-find-ctor-key nm (chez-current-ns))) jolt-nil)))
|
(or (and nm (chez-find-ctor-key nm (chez-current-ns))) jolt-nil)))
|
||||||
|
;; The fully-qualified deftype tag ("ns.Name") IFF `class` names a deftype DEFINED
|
||||||
|
;; in the ctx's compile ns — the analyzer qualifies a bare (Name. …) to it, so a
|
||||||
|
;; deftype doesn't shadow a same-named built-in host class in an unrelated ns
|
||||||
|
;; (rewrite-clj imports java.io.PushbackReader; tools.reader defines its own). Strict:
|
||||||
|
;; only this ns's own def (the preferred shape key) counts, not the global
|
||||||
|
;; simple-name fallback, so a ns that merely uses the built-in resolves nil.
|
||||||
|
(define (hc-deftype-ctor-class ctx class)
|
||||||
|
(let* ((nm (jolt-str-render-one class))
|
||||||
|
(cns (hc-current-ns ctx))
|
||||||
|
(key (string-append cns "/->" nm)))
|
||||||
|
(if (hashtable-ref chez-record-shapes-tbl key #f)
|
||||||
|
(string-append cns "." nm)
|
||||||
|
jolt-nil)))
|
||||||
;; record + protocol-method shapes for the inference, from the runtime registries
|
;; record + protocol-method shapes for the inference, from the runtime registries
|
||||||
;; (records.ss) populated as deftype/defprotocol forms load.
|
;; (records.ss) populated as deftype/defprotocol forms load.
|
||||||
(define (hc-record-shapes ctx) (chez-record-shapes-map))
|
(define (hc-record-shapes ctx) (chez-record-shapes-map))
|
||||||
|
|
@ -500,6 +518,7 @@
|
||||||
(def-var! "jolt.host" "form-syntax-quote-lower" hc-syntax-quote-lower)
|
(def-var! "jolt.host" "form-syntax-quote-lower" hc-syntax-quote-lower)
|
||||||
(def-var! "jolt.host" "record-type?" hc-record-type?)
|
(def-var! "jolt.host" "record-type?" hc-record-type?)
|
||||||
(def-var! "jolt.host" "record-ctor-key" hc-record-ctor-key)
|
(def-var! "jolt.host" "record-ctor-key" hc-record-ctor-key)
|
||||||
|
(def-var! "jolt.host" "deftype-ctor-class" hc-deftype-ctor-class)
|
||||||
(def-var! "jolt.host" "record-shapes" hc-record-shapes)
|
(def-var! "jolt.host" "record-shapes" hc-record-shapes)
|
||||||
(def-var! "jolt.host" "protocol-methods" hc-protocol-methods)
|
(def-var! "jolt.host" "protocol-methods" hc-protocol-methods)
|
||||||
(def-var! "jolt.host" "inline-enabled?" hc-inline-enabled?)
|
(def-var! "jolt.host" "inline-enabled?" hc-inline-enabled?)
|
||||||
|
|
|
||||||
|
|
@ -7,21 +7,19 @@
|
||||||
;;
|
;;
|
||||||
;; Arithmetic follows java.math.BigDecimal's scale rules: add/sub align to the
|
;; Arithmetic follows java.math.BigDecimal's scale rules: add/sub align to the
|
||||||
;; larger scale; multiply adds scales; divide gives the exact quotient at minimal
|
;; larger scale; multiply adds scales; divide gives the exact quotient at minimal
|
||||||
;; scale or throws ArithmeticException on a non-terminating expansion. Clojure
|
;; scale or throws ArithmeticException on a non-terminating expansion (a bound
|
||||||
;; contagion: a bigdec mixed with an integer stays a bigdec; a flonum operand wins
|
;; *math-context* rounds instead). Clojure contagion: a bigdec mixed with an
|
||||||
;; (the result is a double). jbd-add/-sub/-mul/-div, jbd-min/-max, the jbd-lt?/…
|
;; integer or ratio stays a bigdec; a flonum operand wins (the result is a
|
||||||
;; /zero? helpers, and jbd-quot/-rem are the shared engine. Two paths reach it, both
|
;; double). jbd-add/-sub/-mul/-div, jbd-min/-max, the jbd-lt?/…/zero? helpers,
|
||||||
;; leaving the inlined native hot path untouched:
|
;; and jbd-quot/-rem are the shared engine. Two paths reach it, both leaving the
|
||||||
;; - value position ((reduce + bigs)/(apply * bigs)): the jolt-add/-sub/-mul/-div
|
;; inlined fast path untouched:
|
||||||
;; and compare shims dispatch here when a bigdec operand is present.
|
;; - the seq.ss binary dispatch: every generic op (any position — (+ (bigdec x)
|
||||||
;; - call position ((+ 1.5M 2.5M), (< a b), (zero? b)): jolt.passes.numeric tags
|
;; 1), (reduce + bigs), (quot 10.0 3M)) whose operand is outside Chez's tower
|
||||||
;; the invoke :num-kind :bigdec when every operand is statically a bigdec (M
|
;; falls to the jolt-*-slow hooks extended below.
|
||||||
;; literal or a let-bound copy, integer literals allowed), and the back end
|
;; - static call position ((+ 1.5M 2.5M), (< a b), (zero? b)): jolt.passes.numeric
|
||||||
;; lowers it to the jbd op. Non-bigdec code is unaffected.
|
;; tags the invoke :num-kind :bigdec when every operand is statically a bigdec
|
||||||
;; Gaps (a runtime bigdec the analyzer can't see statically): a bigdec mixed with a
|
;; (M literal or a let-bound copy, integer literals allowed), and the back end
|
||||||
;; flonum in call position ((+ 1.5M 2.0)) and arithmetic over a bigdec the analyzer
|
;; lowers it directly to the jbd op.
|
||||||
;; types as :any ((+ (bigdec x) 1)) fall through to the raw op and throw; use value
|
|
||||||
;; position or a literal-typed let.
|
|
||||||
|
|
||||||
(define-record-type jbigdec (fields unscaled scale) (nongenerative chez-jbigdec-v1))
|
(define-record-type jbigdec (fields unscaled scale) (nongenerative chez-jbigdec-v1))
|
||||||
|
|
||||||
|
|
@ -79,11 +77,13 @@
|
||||||
(define (jbigdec->flonum b)
|
(define (jbigdec->flonum b)
|
||||||
(exact->inexact (/ (jbigdec-unscaled b) (expt 10 (jbigdec-scale b)))))
|
(exact->inexact (/ (jbigdec-unscaled b) (expt 10 (jbigdec-scale b)))))
|
||||||
|
|
||||||
;; coerce an exact integer to a scale-0 bigdec; pass a bigdec through. Used on the
|
;; coerce an exact operand to a bigdec; pass a bigdec through. Used on the
|
||||||
;; non-flonum mixed path (bigdec + long -> bigdec).
|
;; non-flonum mixed path (bigdec + long -> bigdec). A Ratio converts like
|
||||||
|
;; Numbers.toBigDecimal — exact decimal expansion or throw on non-terminating.
|
||||||
(define (jbd-coerce x)
|
(define (jbd-coerce x)
|
||||||
(cond ((jbigdec? x) x)
|
(cond ((jbigdec? x) x)
|
||||||
((and (number? x) (exact? x) (integer? x)) (make-jbigdec x 0))
|
((and (number? x) (exact? x) (integer? x)) (make-jbigdec x 0))
|
||||||
|
((and (number? x) (exact? x) (rational? x)) (jbd-rational->bigdec x))
|
||||||
(else (error #f "bigdec arithmetic: cannot coerce operand" x))))
|
(else (error #f "bigdec arithmetic: cannot coerce operand" x))))
|
||||||
|
|
||||||
;; --- core arithmetic on the {unscaled, scale} pair --------------------------
|
;; --- core arithmetic on the {unscaled, scale} pair --------------------------
|
||||||
|
|
@ -117,12 +117,39 @@
|
||||||
"java.lang.ArithmeticException"
|
"java.lang.ArithmeticException"
|
||||||
"Non-terminating decimal expansion; no exact representable decimal result.")))))))
|
"Non-terminating decimal expansion; no exact representable decimal result.")))))))
|
||||||
|
|
||||||
|
;; floor(log10 |r|) for a nonzero exact rational.
|
||||||
|
(define (jbd-exp10 r)
|
||||||
|
(let ((n (abs (numerator r))) (d (denominator r)))
|
||||||
|
(if (>= n d)
|
||||||
|
(- (jbd-digits (quotient n d)) 1)
|
||||||
|
(let loop ((x (* n 10)) (e -1))
|
||||||
|
(if (>= x d) e (loop (* x 10) (- e 1)))))))
|
||||||
|
;; round an exact rational to `prec` significant digits (the MathContext divide).
|
||||||
|
(define (jbd-rational-prec r prec mode)
|
||||||
|
(if (= r 0)
|
||||||
|
(make-jbigdec 0 0)
|
||||||
|
(let* ((neg (< r 0)) (ar (abs r))
|
||||||
|
(s (- prec 1 (jbd-exp10 ar)))
|
||||||
|
(scaled (* ar (expt 10 s)))
|
||||||
|
(q (floor scaled)) (frac (- scaled q))
|
||||||
|
(q2 (if (jbd-round-inc? q frac 1 mode neg) (+ q 1) q))
|
||||||
|
(res (make-jbigdec (if neg (- q2) q2) s)))
|
||||||
|
;; a carry can add a digit (9.99 -> 10.0); re-normalizing drops an exact
|
||||||
|
;; trailing zero, never re-rounds.
|
||||||
|
(if (> (jbd-digits q2) prec) (jbd-round-prec res prec mode) res))))
|
||||||
|
|
||||||
(define (jbd2-div a b)
|
(define (jbd2-div a b)
|
||||||
(when (= 0 (jbigdec-unscaled b))
|
(when (= 0 (jbigdec-unscaled b))
|
||||||
(jolt-throw (jolt-host-throwable "java.lang.ArithmeticException" "Divide by zero")))
|
(jolt-throw (jolt-host-throwable "java.lang.ArithmeticException" "Divide by zero")))
|
||||||
;; a/b = (ua * 10^sb) / (ub * 10^sa) as an exact rational.
|
;; a/b = (ua * 10^sb) / (ub * 10^sa) as an exact rational. Unlimited context:
|
||||||
(jbd-rational->bigdec (/ (* (jbigdec-unscaled a) (expt 10 (jbigdec-scale b)))
|
;; exact result at minimal scale or throw on a non-terminating expansion. A
|
||||||
(* (jbigdec-unscaled b) (expt 10 (jbigdec-scale a))))))
|
;; bound *math-context* instead rounds to its precision.
|
||||||
|
(let ((r (/ (* (jbigdec-unscaled a) (expt 10 (jbigdec-scale b)))
|
||||||
|
(* (jbigdec-unscaled b) (expt 10 (jbigdec-scale a)))))
|
||||||
|
(mc (jbd-math-context)))
|
||||||
|
(if mc
|
||||||
|
(jbd-rational-prec r (jbd-mc-precision mc) (jbd-mc-mode mc))
|
||||||
|
(jbd-rational->bigdec r))))
|
||||||
|
|
||||||
;; integer-division semantics (quot/rem): truncate toward zero, scale 0.
|
;; integer-division semantics (quot/rem): truncate toward zero, scale 0.
|
||||||
(define (jbd-int-quot a b)
|
(define (jbd-int-quot a b)
|
||||||
|
|
@ -139,13 +166,65 @@
|
||||||
(define (jbd-compare2 a b)
|
(define (jbd-compare2 a b)
|
||||||
(let-values (((ua ub s) (jbd-align a b))) (cond ((< ua ub) -1) ((> ua ub) 1) (else 0))))
|
(let-values (((ua ub s) (jbd-align a b))) (cond ((< ua ub) -1) ((> ua ub) 1) (else 0))))
|
||||||
|
|
||||||
|
;; --- *math-context* (with-precision) -----------------------------------------
|
||||||
|
;; with-precision binds clojure.core/*math-context* to {:precision N :rounding
|
||||||
|
;; MODE}; every exact bigdec result rounds through it (java.math.MathContext).
|
||||||
|
(define jbd-kw-precision (keyword #f "precision"))
|
||||||
|
(define jbd-kw-rounding (keyword #f "rounding"))
|
||||||
|
(define (jbd-math-context)
|
||||||
|
(let ((mc (var-deref "clojure.core" "*math-context*")))
|
||||||
|
(if (jolt-nil? mc) #f mc)))
|
||||||
|
(define (jbd-mc-precision mc) (jolt-get mc jbd-kw-precision))
|
||||||
|
(define (jbd-mc-mode mc)
|
||||||
|
(let ((r (jolt-get mc jbd-kw-rounding)))
|
||||||
|
(cond ((symbol-t? r) (symbol-t-name r))
|
||||||
|
((string? r) r)
|
||||||
|
(else "HALF_UP"))))
|
||||||
|
|
||||||
|
;; should |value| = q + r/div (0 <= r < div) round up in magnitude? neg is the
|
||||||
|
;; value's sign; r/div may be exact rationals (the division path).
|
||||||
|
(define (jbd-round-inc? q r div mode neg)
|
||||||
|
(cond ((= r 0) #f)
|
||||||
|
((string=? mode "UP") #t)
|
||||||
|
((string=? mode "DOWN") #f)
|
||||||
|
((string=? mode "CEILING") (not neg))
|
||||||
|
((string=? mode "FLOOR") neg)
|
||||||
|
((string=? mode "HALF_DOWN") (> (* 2 r) div))
|
||||||
|
((string=? mode "HALF_EVEN")
|
||||||
|
(let ((c (- (* 2 r) div)))
|
||||||
|
(cond ((> c 0) #t) ((< c 0) #f) (else (odd? q)))))
|
||||||
|
((string=? mode "UNNECESSARY")
|
||||||
|
(jolt-throw (jolt-host-throwable "java.lang.ArithmeticException" "Rounding necessary")))
|
||||||
|
(else (>= (* 2 r) div)))) ; HALF_UP, the MathContext default
|
||||||
|
|
||||||
|
(define (jbd-digits n) (string-length (number->string (abs n))))
|
||||||
|
;; round a bigdec to `prec` significant digits with `mode` (a RoundingMode name).
|
||||||
|
(define (jbd-round-prec bd prec mode)
|
||||||
|
(let ((u (jbigdec-unscaled bd)) (s (jbigdec-scale bd)))
|
||||||
|
(if (= u 0)
|
||||||
|
bd
|
||||||
|
(let ((digs (jbd-digits u)))
|
||||||
|
(if (<= digs prec)
|
||||||
|
bd
|
||||||
|
(let* ((drop (- digs prec)) (div (expt 10 drop))
|
||||||
|
(neg (< u 0)) (au (abs u))
|
||||||
|
(q (quotient au div)) (r (remainder au div))
|
||||||
|
(q2 (if (jbd-round-inc? q r div mode neg) (+ q 1) q))
|
||||||
|
(res (make-jbigdec (if neg (- q2) q2) (- s drop))))
|
||||||
|
;; a carry can add a digit back (99 -> 100 at precision 2)
|
||||||
|
(if (> (jbd-digits q2) prec) (jbd-round-prec res prec mode) res)))))))
|
||||||
|
(define (jbd-mc-round x)
|
||||||
|
(let ((mc (and (jbigdec? x) (jbd-math-context))))
|
||||||
|
(if mc (jbd-round-prec x (jbd-mc-precision mc) (jbd-mc-mode mc)) x)))
|
||||||
|
|
||||||
;; A binary op over operands that may mix bigdec / integer / flonum. flonum-op is
|
;; A binary op over operands that may mix bigdec / integer / flonum. flonum-op is
|
||||||
;; the native fallback for the double-contagion path; bd-op is the exact bigdec op.
|
;; the native fallback for the double-contagion path; bd-op is the exact bigdec op
|
||||||
|
;; (its result rounds through a bound *math-context*).
|
||||||
(define (jbd-binop flonum-op bd-op a b)
|
(define (jbd-binop flonum-op bd-op a b)
|
||||||
(if (or (flonum? a) (flonum? b))
|
(if (or (flonum? a) (flonum? b))
|
||||||
(flonum-op (if (jbigdec? a) (jbigdec->flonum a) a)
|
(flonum-op (if (jbigdec? a) (jbigdec->flonum a) a)
|
||||||
(if (jbigdec? b) (jbigdec->flonum b) b))
|
(if (jbigdec? b) (jbigdec->flonum b) b))
|
||||||
(bd-op (jbd-coerce a) (jbd-coerce b))))
|
(jbd-mc-round (bd-op (jbd-coerce a) (jbd-coerce b)))))
|
||||||
|
|
||||||
;; --- variadic engine ops (Phase-2 emit targets + value-position folds) -------
|
;; --- variadic engine ops (Phase-2 emit targets + value-position folds) -------
|
||||||
(define (jbd-fold flonum-op bd-op init xs)
|
(define (jbd-fold flonum-op bd-op init xs)
|
||||||
|
|
@ -203,23 +282,96 @@
|
||||||
;; --- wire into the value model ----------------------------------------------
|
;; --- wire into the value model ----------------------------------------------
|
||||||
(def-var! "clojure.core" "bigdec" jolt-bigdec)
|
(def-var! "clojure.core" "bigdec" jolt-bigdec)
|
||||||
|
|
||||||
;; Value-position arithmetic: (reduce + bigs) / (apply * bigs) pass +/*/- // AS A
|
;; The seq.ss binary numeric dispatch (jolt-add2/… and the jolt-n* macros) routes
|
||||||
;; VALUE, which lowers to these shims (NOT the inlined hot-path native op). Extend
|
;; any op whose operand is outside Chez's tower to the *-slow hooks; extend each
|
||||||
;; them to dispatch to the bigdec engine when a bigdec operand is present; ordinary
|
;; with a bigdec arm. Every arithmetic position (call, value, higher-order)
|
||||||
;; numeric folds hit the captured native path unchanged.
|
;; funnels through these, so contagion and *math-context* rounding apply
|
||||||
(define jbd-prev-add jolt-add)
|
;; uniformly. min/max need no arm: the generic jolt-min2 compares through
|
||||||
(define jbd-prev-sub jolt-sub)
|
;; jolt-num-cmp-slow and returns the original operand.
|
||||||
(define jbd-prev-mul jolt-mul)
|
(set! jolt-num-slow?
|
||||||
(define jbd-prev-div jolt-div)
|
(let ((prev jolt-num-slow?)) (lambda (x) (or (jbigdec? x) (prev x)))))
|
||||||
(define jbd-prev-min jolt-min)
|
(define (jbd-extend-hook prev bd-op)
|
||||||
(define jbd-prev-max jolt-max)
|
(lambda (a b)
|
||||||
(define (jbd-any? xs) (and (pair? xs) (or (jbigdec? (car xs)) (jbd-any? (cdr xs)))))
|
(if (or (jbigdec? a) (jbigdec? b)) (bd-op a b) (prev a b))))
|
||||||
(set! jolt-add (lambda xs (if (jbd-any? xs) (apply jbd-add xs) (apply jbd-prev-add xs))))
|
(set! jolt-add-slow (jbd-extend-hook jolt-add-slow (lambda (a b) (jbd-binop + jbd2+ a b))))
|
||||||
(set! jolt-sub (lambda xs (if (jbd-any? xs) (apply jbd-sub xs) (apply jbd-prev-sub xs))))
|
(set! jolt-sub-slow (jbd-extend-hook jolt-sub-slow (lambda (a b) (jbd-binop - jbd2- a b))))
|
||||||
(set! jolt-mul (lambda xs (if (jbd-any? xs) (apply jbd-mul xs) (apply jbd-prev-mul xs))))
|
(set! jolt-mul-slow (jbd-extend-hook jolt-mul-slow (lambda (a b) (jbd-binop * jbd2* a b))))
|
||||||
(set! jolt-div (lambda xs (if (jbd-any? xs) (apply jbd-div xs) (apply jbd-prev-div xs))))
|
(set! jolt-div-slow (jbd-extend-hook jolt-div-slow (lambda (a b) (jbd-binop / jbd2-div a b))))
|
||||||
(set! jolt-min (lambda xs (if (jbd-any? xs) (apply jbd-min xs) (apply jbd-prev-min xs))))
|
(set! jolt-num-cmp-slow
|
||||||
(set! jolt-max (lambda xs (if (jbd-any? xs) (apply jbd-max xs) (apply jbd-prev-max xs))))
|
(let ((prev jolt-num-cmp-slow))
|
||||||
|
(lambda (a b)
|
||||||
|
(if (and (or (jbigdec? a) (jbigdec? b)) (jbd-numberish? a) (jbd-numberish? b))
|
||||||
|
(jbd-value-compare a b)
|
||||||
|
(prev a b)))))
|
||||||
|
;; quot/rem/mod: a double operand demotes to the double path; exact operands use
|
||||||
|
;; the integer-division bigdec ops (mod = rem, floor-adjusted to the divisor's sign).
|
||||||
|
(define (jbd->num x) (if (jbigdec? x) (jbigdec->flonum x) x))
|
||||||
|
(set! jolt-quot-slow
|
||||||
|
(jbd-extend-hook jolt-quot-slow
|
||||||
|
(lambda (a b) (if (or (flonum? a) (flonum? b))
|
||||||
|
(jolt-quot (jbd->num a) (jbd->num b))
|
||||||
|
(jbd-int-quot (jbd-coerce a) (jbd-coerce b))))))
|
||||||
|
(set! jolt-rem-slow
|
||||||
|
(jbd-extend-hook jolt-rem-slow
|
||||||
|
(lambda (a b) (if (or (flonum? a) (flonum? b))
|
||||||
|
(jolt-rem (jbd->num a) (jbd->num b))
|
||||||
|
(jbd-int-rem (jbd-coerce a) (jbd-coerce b))))))
|
||||||
|
(set! jolt-mod-slow
|
||||||
|
(jbd-extend-hook jolt-mod-slow
|
||||||
|
(lambda (a b)
|
||||||
|
(if (or (flonum? a) (flonum? b))
|
||||||
|
(jolt-mod (jbd->num a) (jbd->num b))
|
||||||
|
(let* ((bb (jbd-coerce b))
|
||||||
|
(m (jbd-int-rem (jbd-coerce a) bb)))
|
||||||
|
(if (or (jbd-zero? m) (eq? (jbd-neg? m) (jbd-neg? bb))) m (jbd2+ m bb)))))))
|
||||||
|
;; unary shims: inc/dec and the sign predicates take a bigdec arm. set! updates
|
||||||
|
;; call-position references; the re-def-var! updates the var cell AND claims the
|
||||||
|
;; wrapped proc's class name before the prelude's inc'/dec' aliases are defined
|
||||||
|
;; ((type inc) stays clojure.core$inc — first def wins in the class registry).
|
||||||
|
(define jbd-one (make-jbigdec 1 0))
|
||||||
|
(set! jolt-inc (let ((prev jolt-inc)) (lambda (x) (if (jbigdec? x) (jbd-mc-round (jbd2+ x jbd-one)) (prev x)))))
|
||||||
|
(set! jolt-dec (let ((prev jolt-dec)) (lambda (x) (if (jbigdec? x) (jbd-mc-round (jbd2- x jbd-one)) (prev x)))))
|
||||||
|
(set! jolt-zero? (let ((prev jolt-zero?)) (lambda (x) (if (jbigdec? x) (jbd-zero? x) (prev x)))))
|
||||||
|
(set! jolt-pos? (let ((prev jolt-pos?)) (lambda (x) (if (jbigdec? x) (jbd-pos? x) (prev x)))))
|
||||||
|
(set! jolt-neg? (let ((prev jolt-neg?)) (lambda (x) (if (jbigdec? x) (jbd-neg? x) (prev x)))))
|
||||||
|
;; a BigDecimal IS a number (java.lang.Number): extend the number? native so the
|
||||||
|
;; predicate — and everything defined over it (num, =='s guard) — accepts it.
|
||||||
|
;; The compiled fast paths test Chez number? directly and are unaffected.
|
||||||
|
(set! jolt-number? (let ((prev jolt-number?)) (lambda (x) (if (jbigdec? x) #t (prev x)))))
|
||||||
|
(def-var! "clojure.core" "number?" jolt-number?)
|
||||||
|
(def-var! "clojure.core" "inc" jolt-inc)
|
||||||
|
(def-var! "clojure.core" "dec" jolt-dec)
|
||||||
|
(def-var! "clojure.core" "zero?" jolt-zero?)
|
||||||
|
(def-var! "clojure.core" "pos?" jolt-pos?)
|
||||||
|
(def-var! "clojure.core" "neg?" jolt-neg?)
|
||||||
|
|
||||||
|
;; rationalize: reference Clojure goes through BigDecimal.valueOf(double) — the
|
||||||
|
;; SHORTEST decimal print of the double, not its exact binary value — so
|
||||||
|
;; (rationalize 1.1) is 11/10. A bigdec is exact already; other exacts pass through.
|
||||||
|
(define (jolt-rationalize x)
|
||||||
|
(cond ((jbigdec? x) (/ (jbigdec-unscaled x) (expt 10 (jbigdec-scale x))))
|
||||||
|
((flonum? x)
|
||||||
|
(if (or (nan? x) (infinite? x))
|
||||||
|
(jolt-throw (jolt-host-throwable "java.lang.NumberFormatException"
|
||||||
|
(string-append "Invalid input: " (number->string x))))
|
||||||
|
(let ((bd (jolt-bigdec-from-string (jolt-num->string x))))
|
||||||
|
(/ (jbigdec-unscaled bd) (expt 10 (jbigdec-scale bd))))))
|
||||||
|
((number? x) x)
|
||||||
|
(else (jolt-num-cast-throw x))))
|
||||||
|
(def-var! "clojure.core" "rationalize" jolt-rationalize)
|
||||||
|
|
||||||
|
;; double/float of a bigdec is its flonum value.
|
||||||
|
(set! jolt-double-slow
|
||||||
|
(let ((prev jolt-double-slow))
|
||||||
|
(lambda (x) (if (jbigdec? x) (jbigdec->flonum x) (prev x)))))
|
||||||
|
|
||||||
|
;; narrow casts truncate a bigdec like Number.longValue.
|
||||||
|
(set! jolt-cast-truncate-slow
|
||||||
|
(let ((prev jolt-cast-truncate-slow))
|
||||||
|
(lambda (x)
|
||||||
|
(if (jbigdec? x)
|
||||||
|
(truncate (/ (jbigdec-unscaled x) (expt 10 (jbigdec-scale x))))
|
||||||
|
(prev x)))))
|
||||||
|
|
||||||
;; compare: add a bigdec arm (enables compare / sort / sorted collections). A
|
;; compare: add a bigdec arm (enables compare / sort / sorted collections). A
|
||||||
;; bigdec vs a plain number compares by value; bigdec vs bigdec is scale-independent.
|
;; bigdec vs a plain number compares by value; bigdec vs bigdec is scale-independent.
|
||||||
|
|
|
||||||
261
host/chez/java/class-hierarchy.ss
Normal file
261
host/chez/java/class-hierarchy.ss
Normal file
|
|
@ -0,0 +1,261 @@
|
||||||
|
;; class-hierarchy.ss — one JVM class/interface graph, the single source of truth
|
||||||
|
;; for every "what classes does this satisfy" question. value-host-tags (protocol
|
||||||
|
;; dispatch), instance?, isa?/supers/ancestors, and the exception hierarchy all
|
||||||
|
;; derive from the ONE table here instead of maintaining parallel hand-kept lists
|
||||||
|
;; that drift apart.
|
||||||
|
;;
|
||||||
|
;; The graph is keyed by canonical (FQN) class name -> its DIRECT super
|
||||||
|
;; interfaces/classes (also FQN). Transitivity is computed (jch-closure), so a row
|
||||||
|
;; lists only what a class directly extends/implements, matching the JVM source.
|
||||||
|
;;
|
||||||
|
;; It is OPEN: a library registers a class and its supers with
|
||||||
|
;; jolt.host/register-class-supers! (plus a class-arm in host-class.ss to map its
|
||||||
|
;; values to that class name), and every derived view picks the class up with no
|
||||||
|
;; core change. Loaded before records.ss so value-host-tags can derive from it.
|
||||||
|
|
||||||
|
;; canonical-name -> list of direct super canonical-names. Mutable + extensible.
|
||||||
|
(define jvm-class-parents (make-hashtable string-hash string=?))
|
||||||
|
;; closure cache, invalidated whenever the graph is extended.
|
||||||
|
(define jch-closure-cache (make-hashtable string-hash string=?))
|
||||||
|
(define jch-tags-cache (make-hashtable string-hash string=?))
|
||||||
|
|
||||||
|
;; Merge direct supers for a class (union with any already registered). Public so
|
||||||
|
;; libraries can graft their own classes onto the modeled hierarchy.
|
||||||
|
(define (jch-register-supers! name supers)
|
||||||
|
(let ((cur (hashtable-ref jvm-class-parents name '())))
|
||||||
|
(hashtable-set! jvm-class-parents name
|
||||||
|
(let add ((ss supers) (acc cur))
|
||||||
|
(cond ((null? ss) acc)
|
||||||
|
((member (car ss) acc) (add (cdr ss) acc))
|
||||||
|
(else (add (cdr ss) (append acc (list (car ss)))))))))
|
||||||
|
(hashtable-clear! jch-closure-cache)
|
||||||
|
(hashtable-clear! jch-tags-cache))
|
||||||
|
|
||||||
|
(define (jch-direct-supers name) (hashtable-ref jvm-class-parents name '()))
|
||||||
|
|
||||||
|
;; Replace a class's direct supers outright (defrecord re-declares the row its
|
||||||
|
;; deftype half registered). Same cache invalidation as a register.
|
||||||
|
(define (jch-set-supers! name supers)
|
||||||
|
(hashtable-set! jvm-class-parents name supers)
|
||||||
|
(hashtable-clear! jch-closure-cache)
|
||||||
|
(hashtable-clear! jch-tags-cache)
|
||||||
|
(set! jch-known-cache #f)
|
||||||
|
(set! jch-simple->fqn-cache #f))
|
||||||
|
|
||||||
|
;; transitive supers of NAME (canonical), excluding NAME and Object; Object is the
|
||||||
|
;; universal root supplied by callers. Breadth-first, deduped, stable order.
|
||||||
|
(define (jch-closure name)
|
||||||
|
(or (hashtable-ref jch-closure-cache name #f)
|
||||||
|
(let ((result
|
||||||
|
(let loop ((pending (jch-direct-supers name)) (seen '()))
|
||||||
|
(cond ((null? pending) (reverse seen))
|
||||||
|
((member (car pending) seen) (loop (cdr pending) seen))
|
||||||
|
(else (loop (append (jch-direct-supers (car pending)) (cdr pending))
|
||||||
|
(cons (car pending) seen)))))))
|
||||||
|
(hashtable-set! jch-closure-cache name result)
|
||||||
|
result)))
|
||||||
|
|
||||||
|
;; ns segment munging for a JVM-spelled class name: dashes become underscores
|
||||||
|
;; (clojure.core-test.x -> clojure.core_test.x).
|
||||||
|
(define (jch-munge-segments s)
|
||||||
|
(list->string (map (lambda (c) (if (char=? c #\-) #\_ c)) (string->list s))))
|
||||||
|
|
||||||
|
(define (jch-last-segment s)
|
||||||
|
(let loop ((i (- (string-length s) 1)))
|
||||||
|
(cond ((< i 0) s)
|
||||||
|
((char=? (string-ref s i) #\.) (substring s (+ i 1) (string-length s)))
|
||||||
|
((char=? (string-ref s i) #\$) (substring s (+ i 1) (string-length s)))
|
||||||
|
(else (loop (- i 1))))))
|
||||||
|
|
||||||
|
;; The protocol-dispatch / instance? tag list for a value of class NAME: the class
|
||||||
|
;; and its whole ancestry, each in BOTH canonical and simple spelling (extend-protocol
|
||||||
|
;; and instance? accept either "Associative" or "clojure.lang.Associative"), plus
|
||||||
|
;; "Object". Memoized — this is on the hot protocol-dispatch path.
|
||||||
|
(define (jch-tags name)
|
||||||
|
(or (hashtable-ref jch-tags-cache name #f)
|
||||||
|
(let* ((chain (cons name (jch-closure name)))
|
||||||
|
(result
|
||||||
|
(let build ((cs chain) (acc '()))
|
||||||
|
(if (null? cs)
|
||||||
|
(reverse (cons "Object" acc))
|
||||||
|
(let* ((fqn (car cs))
|
||||||
|
(simple (jch-last-segment fqn))
|
||||||
|
(acc1 (if (member fqn acc) acc (cons fqn acc)))
|
||||||
|
(acc2 (if (or (string=? simple fqn) (member simple acc1))
|
||||||
|
acc1 (cons simple acc1))))
|
||||||
|
(build (cdr cs) acc2))))))
|
||||||
|
(hashtable-set! jch-tags-cache name result)
|
||||||
|
result)))
|
||||||
|
|
||||||
|
;; Is WANTED (canonical or simple) the class CHILD (canonical) or one of its
|
||||||
|
;; ancestors? Object is every class's root. Matched by full name or last segment so
|
||||||
|
;; "IOException" and "java.io.IOException" both hit.
|
||||||
|
(define (jch-isa? child wanted)
|
||||||
|
(let ((wseg (jch-last-segment wanted)))
|
||||||
|
(or (string=? wanted "java.lang.Object") (string=? wanted "Object")
|
||||||
|
(let loop ((names (cons child (jch-closure child))))
|
||||||
|
(cond ((null? names) #f)
|
||||||
|
((or (string=? wanted (car names))
|
||||||
|
(string=? wseg (jch-last-segment (car names)))) #t)
|
||||||
|
(else (loop (cdr names))))))))
|
||||||
|
|
||||||
|
;; Does the graph model WANTED at all (as a class or as any class's ancestor)? Used
|
||||||
|
;; by instance? to decide between a definitive #f and 'pass (defer to other arms).
|
||||||
|
(define jch-known-cache #f)
|
||||||
|
(define (jch-known? wanted)
|
||||||
|
(when (not jch-known-cache)
|
||||||
|
(set! jch-known-cache (make-hashtable string-hash string=?))
|
||||||
|
(let-values (((keys vals) (hashtable-entries jvm-class-parents)))
|
||||||
|
(vector-for-each
|
||||||
|
(lambda (k supers)
|
||||||
|
(hashtable-set! jch-known-cache k #t)
|
||||||
|
(hashtable-set! jch-known-cache (jch-last-segment k) #t)
|
||||||
|
(for-each (lambda (s)
|
||||||
|
(hashtable-set! jch-known-cache s #t)
|
||||||
|
(hashtable-set! jch-known-cache (jch-last-segment s) #t))
|
||||||
|
supers))
|
||||||
|
keys vals)))
|
||||||
|
(or (hashtable-ref jch-known-cache wanted #f)
|
||||||
|
(hashtable-ref jch-known-cache (jch-last-segment wanted) #f)))
|
||||||
|
|
||||||
|
;; simple last-segment -> canonical FQN for a modeled class (first registered
|
||||||
|
;; wins). Lets a simple exception name (from chez-condition-exc-class) resolve to
|
||||||
|
;; its graph key so the exception hierarchy answers through the one graph.
|
||||||
|
(define jch-simple->fqn-cache #f)
|
||||||
|
(define (jch-fqn-of-simple name)
|
||||||
|
(when (not jch-simple->fqn-cache)
|
||||||
|
(set! jch-simple->fqn-cache (make-hashtable string-hash string=?))
|
||||||
|
(let-values (((keys vals) (hashtable-entries jvm-class-parents)))
|
||||||
|
(vector-for-each
|
||||||
|
(lambda (k supers)
|
||||||
|
(for-each (lambda (n)
|
||||||
|
(let ((seg (jch-last-segment n)))
|
||||||
|
(when (not (hashtable-ref jch-simple->fqn-cache seg #f))
|
||||||
|
(hashtable-set! jch-simple->fqn-cache seg n))))
|
||||||
|
(cons k supers)))
|
||||||
|
keys vals)))
|
||||||
|
(or (hashtable-ref jch-simple->fqn-cache name #f) name))
|
||||||
|
|
||||||
|
;; A register also invalidates the derived caches.
|
||||||
|
(define jch-register-supers!-inner jch-register-supers!)
|
||||||
|
(set! jch-register-supers!
|
||||||
|
(lambda (name supers)
|
||||||
|
(set! jch-known-cache #f)
|
||||||
|
(set! jch-simple->fqn-cache #f)
|
||||||
|
(jch-register-supers!-inner name supers)))
|
||||||
|
|
||||||
|
;; ---- interface marking ---------------------------------------------------------
|
||||||
|
;; The JVM distinguishes a concrete class (whose bases/supers chain roots at
|
||||||
|
;; Object) from an interface (whose don't). The graph marks the modeled
|
||||||
|
;; interfaces; anything unmarked is treated as a concrete class.
|
||||||
|
(define jch-interface-set (make-hashtable string-hash string=?))
|
||||||
|
(define (jch-mark-interface! name) (hashtable-set! jch-interface-set name #t))
|
||||||
|
(define (jch-interface? name) (hashtable-ref jch-interface-set name #f))
|
||||||
|
(for-each jch-mark-interface!
|
||||||
|
'("clojure.lang.Seqable" "clojure.lang.Sequential" "clojure.lang.Sorted"
|
||||||
|
"clojure.lang.Reversible" "clojure.lang.Indexed" "clojure.lang.Counted"
|
||||||
|
"clojure.lang.Named" "clojure.lang.Fn" "clojure.lang.IFn"
|
||||||
|
"clojure.lang.IPersistentCollection" "clojure.lang.ISeq"
|
||||||
|
"clojure.lang.Associative" "clojure.lang.ILookup"
|
||||||
|
"clojure.lang.IPersistentStack" "clojure.lang.IPersistentVector"
|
||||||
|
"clojure.lang.IPersistentMap" "clojure.lang.IPersistentSet"
|
||||||
|
"clojure.lang.IPersistentList" "clojure.lang.IObj" "clojure.lang.IMeta"
|
||||||
|
"clojure.lang.IDeref" "clojure.lang.IRecord" "clojure.lang.IType"
|
||||||
|
"clojure.lang.IHashEq" "clojure.lang.IEditableCollection"
|
||||||
|
"clojure.lang.IExceptionInfo" "clojure.lang.IReduceInit"
|
||||||
|
"java.util.List" "java.util.Set" "java.util.Collection" "java.util.Map"
|
||||||
|
"java.util.Iterator" "java.lang.Iterable" "java.lang.CharSequence"
|
||||||
|
"java.lang.Comparable" "java.lang.Runnable"
|
||||||
|
"java.util.concurrent.Callable" "java.io.Serializable"))
|
||||||
|
|
||||||
|
;; ---- seed the built-in graph: direct supers only, faithful to the JVM ---------
|
||||||
|
;; core clojure.lang interfaces
|
||||||
|
(jch-register-supers! "clojure.lang.IPersistentCollection" '("clojure.lang.Seqable"))
|
||||||
|
(jch-register-supers! "clojure.lang.ISeq" '("clojure.lang.IPersistentCollection"))
|
||||||
|
(jch-register-supers! "clojure.lang.Associative" '("clojure.lang.IPersistentCollection" "clojure.lang.ILookup"))
|
||||||
|
(jch-register-supers! "clojure.lang.IPersistentStack" '("clojure.lang.IPersistentCollection"))
|
||||||
|
(jch-register-supers! "clojure.lang.IPersistentVector" '("clojure.lang.Associative" "clojure.lang.Sequential"
|
||||||
|
"clojure.lang.IPersistentStack" "clojure.lang.Reversible"
|
||||||
|
"clojure.lang.Indexed"))
|
||||||
|
(jch-register-supers! "clojure.lang.IPersistentMap" '("java.lang.Iterable" "clojure.lang.Associative" "clojure.lang.Counted"))
|
||||||
|
(jch-register-supers! "clojure.lang.IPersistentSet" '("clojure.lang.IPersistentCollection" "clojure.lang.Counted"))
|
||||||
|
(jch-register-supers! "clojure.lang.IPersistentList" '("clojure.lang.Sequential" "clojure.lang.IPersistentStack"))
|
||||||
|
(jch-register-supers! "clojure.lang.IObj" '("clojure.lang.IMeta"))
|
||||||
|
(jch-register-supers! "clojure.lang.IFn" '("java.lang.Runnable" "java.util.concurrent.Callable"))
|
||||||
|
(jch-register-supers! "clojure.lang.Fn" '("clojure.lang.IFn"))
|
||||||
|
(jch-register-supers! "clojure.lang.AFn" '("clojure.lang.IFn"))
|
||||||
|
(jch-register-supers! "clojure.lang.AFunction" '("clojure.lang.AFn" "clojure.lang.Fn"))
|
||||||
|
;; java.util collection interfaces
|
||||||
|
(jch-register-supers! "java.util.List" '("java.util.Collection"))
|
||||||
|
(jch-register-supers! "java.util.Set" '("java.util.Collection"))
|
||||||
|
(jch-register-supers! "java.util.Collection" '("java.lang.Iterable"))
|
||||||
|
;; concrete collection classes
|
||||||
|
(jch-register-supers! "clojure.lang.APersistentVector" '("clojure.lang.IPersistentVector" "java.util.List"))
|
||||||
|
(jch-register-supers! "clojure.lang.PersistentVector" '("clojure.lang.APersistentVector" "clojure.lang.IObj"
|
||||||
|
"java.util.List" "java.lang.Comparable"))
|
||||||
|
(jch-register-supers! "clojure.lang.APersistentMap" '("clojure.lang.IPersistentMap" "java.util.Map"))
|
||||||
|
(jch-register-supers! "clojure.lang.PersistentArrayMap" '("clojure.lang.APersistentMap" "clojure.lang.IObj"))
|
||||||
|
(jch-register-supers! "clojure.lang.PersistentHashMap" '("clojure.lang.APersistentMap" "clojure.lang.IObj"))
|
||||||
|
(jch-register-supers! "clojure.lang.PersistentTreeMap" '("clojure.lang.APersistentMap" "clojure.lang.IObj" "clojure.lang.Sorted" "clojure.lang.Reversible"))
|
||||||
|
(jch-register-supers! "clojure.lang.APersistentSet" '("clojure.lang.IPersistentSet" "java.util.Set"))
|
||||||
|
(jch-register-supers! "clojure.lang.PersistentHashSet" '("clojure.lang.APersistentSet" "clojure.lang.IObj"))
|
||||||
|
(jch-register-supers! "clojure.lang.PersistentTreeSet" '("clojure.lang.APersistentSet" "clojure.lang.IObj" "clojure.lang.Sorted" "clojure.lang.Reversible"))
|
||||||
|
(jch-register-supers! "clojure.lang.ASeq" '("clojure.lang.ISeq" "clojure.lang.Sequential" "java.util.List"))
|
||||||
|
(jch-register-supers! "clojure.lang.PersistentList" '("clojure.lang.ASeq" "clojure.lang.IPersistentList" "clojure.lang.Counted"))
|
||||||
|
(jch-register-supers! "clojure.lang.PersistentList$EmptyList" '("clojure.lang.PersistentList"))
|
||||||
|
(jch-register-supers! "clojure.lang.LazySeq" '("clojure.lang.ISeq" "clojure.lang.Sequential" "java.util.List" "clojure.lang.IObj"))
|
||||||
|
(jch-register-supers! "clojure.lang.Cons" '("clojure.lang.ASeq"))
|
||||||
|
(jch-register-supers! "clojure.lang.PersistentQueue" '("clojure.lang.IPersistentList" "clojure.lang.IPersistentCollection" "java.util.Collection"))
|
||||||
|
;; scalars / named / callable
|
||||||
|
(jch-register-supers! "clojure.lang.Keyword" '("clojure.lang.IFn" "clojure.lang.Named" "java.lang.Comparable"))
|
||||||
|
(jch-register-supers! "clojure.lang.Symbol" '("clojure.lang.IObj" "clojure.lang.IFn" "clojure.lang.Named" "java.lang.Comparable"))
|
||||||
|
(jch-register-supers! "clojure.lang.Var" '("clojure.lang.IDeref" "clojure.lang.IFn"))
|
||||||
|
(jch-register-supers! "clojure.lang.Atom" '("clojure.lang.IDeref"))
|
||||||
|
(jch-register-supers! "clojure.lang.Ratio" '("java.lang.Number" "java.lang.Comparable"))
|
||||||
|
(jch-register-supers! "clojure.lang.BigInt" '("java.lang.Number"))
|
||||||
|
(jch-register-supers! "java.lang.String" '("java.lang.CharSequence" "java.lang.Comparable"))
|
||||||
|
(jch-register-supers! "java.lang.Long" '("java.lang.Number" "java.lang.Comparable"))
|
||||||
|
(jch-register-supers! "java.lang.Integer" '("java.lang.Number" "java.lang.Comparable"))
|
||||||
|
(jch-register-supers! "java.lang.Double" '("java.lang.Number" "java.lang.Comparable"))
|
||||||
|
(jch-register-supers! "java.lang.Float" '("java.lang.Number" "java.lang.Comparable"))
|
||||||
|
(jch-register-supers! "java.math.BigDecimal" '("java.lang.Number" "java.lang.Comparable"))
|
||||||
|
(jch-register-supers! "java.math.BigInteger" '("java.lang.Number" "java.lang.Comparable"))
|
||||||
|
(jch-register-supers! "java.lang.Boolean" '("java.lang.Comparable"))
|
||||||
|
(jch-register-supers! "java.lang.Character" '("java.lang.Comparable"))
|
||||||
|
(jch-register-supers! "java.util.UUID" '("java.lang.Comparable"))
|
||||||
|
;; exception hierarchy (folds in the former exception-parent table)
|
||||||
|
(jch-register-supers! "java.lang.Exception" '("java.lang.Throwable"))
|
||||||
|
(jch-register-supers! "java.lang.RuntimeException" '("java.lang.Exception"))
|
||||||
|
(jch-register-supers! "clojure.lang.ExceptionInfo" '("java.lang.RuntimeException" "clojure.lang.IExceptionInfo"))
|
||||||
|
(jch-register-supers! "java.lang.IllegalArgumentException" '("java.lang.RuntimeException"))
|
||||||
|
(jch-register-supers! "clojure.lang.ArityException" '("java.lang.IllegalArgumentException"))
|
||||||
|
(jch-register-supers! "java.lang.NumberFormatException" '("java.lang.IllegalArgumentException"))
|
||||||
|
(jch-register-supers! "java.lang.IllegalStateException" '("java.lang.RuntimeException"))
|
||||||
|
(jch-register-supers! "java.lang.UnsupportedOperationException" '("java.lang.RuntimeException"))
|
||||||
|
(jch-register-supers! "java.lang.ArithmeticException" '("java.lang.RuntimeException"))
|
||||||
|
(jch-register-supers! "java.lang.NullPointerException" '("java.lang.RuntimeException"))
|
||||||
|
(jch-register-supers! "java.lang.ClassCastException" '("java.lang.RuntimeException"))
|
||||||
|
(jch-register-supers! "java.lang.IndexOutOfBoundsException" '("java.lang.RuntimeException"))
|
||||||
|
(jch-register-supers! "java.util.ConcurrentModificationException" '("java.lang.RuntimeException"))
|
||||||
|
(jch-register-supers! "java.util.NoSuchElementException" '("java.lang.RuntimeException"))
|
||||||
|
(jch-register-supers! "java.io.UncheckedIOException" '("java.lang.RuntimeException"))
|
||||||
|
(jch-register-supers! "java.time.DateTimeException" '("java.lang.RuntimeException"))
|
||||||
|
(jch-register-supers! "java.time.format.DateTimeParseException" '("java.time.DateTimeException"))
|
||||||
|
(jch-register-supers! "java.lang.InterruptedException" '("java.lang.Exception"))
|
||||||
|
(jch-register-supers! "java.io.IOException" '("java.lang.Exception"))
|
||||||
|
(jch-register-supers! "java.io.InterruptedIOException" '("java.io.IOException"))
|
||||||
|
(jch-register-supers! "java.io.FileNotFoundException" '("java.io.IOException"))
|
||||||
|
(jch-register-supers! "java.io.UnsupportedEncodingException" '("java.io.IOException"))
|
||||||
|
(jch-register-supers! "java.net.UnknownHostException" '("java.io.IOException"))
|
||||||
|
(jch-register-supers! "java.net.SocketException" '("java.io.IOException"))
|
||||||
|
(jch-register-supers! "java.net.ConnectException" '("java.net.SocketException"))
|
||||||
|
(jch-register-supers! "java.net.SocketTimeoutException" '("java.io.InterruptedIOException"))
|
||||||
|
(jch-register-supers! "java.net.MalformedURLException" '("java.io.IOException"))
|
||||||
|
(jch-register-supers! "javax.net.ssl.SSLException" '("java.io.IOException"))
|
||||||
|
(jch-register-supers! "java.lang.Error" '("java.lang.Throwable"))
|
||||||
|
(jch-register-supers! "java.lang.AssertionError" '("java.lang.Error"))
|
||||||
|
;; Throwable's only super is Object (universal), so no row needed for it.
|
||||||
|
|
||||||
|
;; Public seam: libraries extend the modeled hierarchy.
|
||||||
|
(def-var! "jolt.host" "register-class-supers!"
|
||||||
|
(lambda (name supers) (jch-register-supers! name (seq->list supers)) jolt-nil))
|
||||||
|
|
@ -151,16 +151,31 @@
|
||||||
(mutable queue) (mutable running?) mu cv)
|
(mutable queue) (mutable running?) mu cv)
|
||||||
(nongenerative jolt-agent-v1))
|
(nongenerative jolt-agent-v1))
|
||||||
|
|
||||||
;; (agent state) / (agent state :validator f :error-mode m :meta x): only :validator
|
;; (agent state :meta m :validator f :error-mode e): the ARef ctor contract like
|
||||||
;; has runtime behaviour here; other opts are accepted/ignored.
|
;; atom's — the validator runs against the initial state, :meta must be a map.
|
||||||
|
;; :error-mode is accepted/ignored (jolt agents are always :fail).
|
||||||
(define (jolt-agent-new state . opts)
|
(define (jolt-agent-new state . opts)
|
||||||
(let loop ((o opts) (validator jolt-nil))
|
(let loop ((o opts) (validator jolt-nil) (m #f))
|
||||||
(cond
|
(cond
|
||||||
((or (null? o) (null? (cdr o)))
|
((or (null? o) (null? (cdr o)))
|
||||||
(make-jolt-agent state jolt-nil validator (vector '() '()) #f (make-mutex) (make-condition)))
|
(let ((a (make-jolt-agent state jolt-nil validator (vector '() '()) #f (make-mutex) (make-condition))))
|
||||||
|
(when (and (not (jolt-nil? validator)) (jolt-not (jolt-invoke validator state)))
|
||||||
|
(jolt-iref-state-throw))
|
||||||
|
(when (and m (not (jolt-nil? m)))
|
||||||
|
(unless (jolt-map? m)
|
||||||
|
(jolt-throw (jolt-host-throwable
|
||||||
|
"java.lang.ClassCastException"
|
||||||
|
(string-append "class " (jolt-class-name m)
|
||||||
|
" cannot be cast to class clojure.lang.IPersistentMap"))))
|
||||||
|
(hashtable-set! meta-table a m))
|
||||||
|
a))
|
||||||
((and (keyword-t? (car o)) (string=? (keyword-t-name (car o)) "validator"))
|
((and (keyword-t? (car o)) (string=? (keyword-t-name (car o)) "validator"))
|
||||||
(loop (cddr o) (cadr o)))
|
(loop (cddr o) (cadr o) m))
|
||||||
(else (loop (cddr o) validator)))))
|
((and (keyword-t? (car o)) (string=? (keyword-t-name (car o)) "meta"))
|
||||||
|
(loop (cddr o) validator (cadr o)))
|
||||||
|
(else (loop (cddr o) validator m)))))
|
||||||
|
;; agents are watchable IRefs; the worker notifies on each state change.
|
||||||
|
(register-iref-arm! jolt-agent?)
|
||||||
|
|
||||||
;; The action queue is an amortized-O(1) FIFO held as a mutable #(out in): `out` is
|
;; The action queue is an amortized-O(1) FIFO held as a mutable #(out in): `out` is
|
||||||
;; the front, `in` holds sends reversed onto it (an append-to-a-list send was O(n)).
|
;; the front, `in` holds sends reversed onto it (an append-to-a-list send was O(n)).
|
||||||
|
|
@ -189,11 +204,13 @@
|
||||||
(guard (e (#t (with-mutex (jolt-agent-mu a)
|
(guard (e (#t (with-mutex (jolt-agent-mu a)
|
||||||
(jolt-agent-err-set! a e)
|
(jolt-agent-err-set! a e)
|
||||||
(condition-broadcast (jolt-agent-cv a)))))
|
(condition-broadcast (jolt-agent-cv a)))))
|
||||||
(let ((nv (apply jolt-invoke (car act) (jolt-agent-state a) (cdr act))))
|
(let* ((old (jolt-agent-state a))
|
||||||
|
(nv (apply jolt-invoke (car act) old (cdr act))))
|
||||||
(let ((vf (jolt-agent-validator a)))
|
(let ((vf (jolt-agent-validator a)))
|
||||||
(when (and (not (jolt-nil? vf)) (jolt-not (jolt-invoke vf nv)))
|
(when (and (not (jolt-nil? vf)) (jolt-not (jolt-invoke vf nv)))
|
||||||
(error #f "Invalid reference state")))
|
(jolt-iref-state-throw)))
|
||||||
(jolt-agent-state-set! a nv)))
|
(jolt-agent-state-set! a nv)
|
||||||
|
(iref-notify a old nv)))
|
||||||
(loop)))))
|
(loop)))))
|
||||||
|
|
||||||
;; send / send-off: enqueue the action, start the worker if idle. (jolt treats them
|
;; send / send-off: enqueue the action, start the worker if idle. (jolt treats them
|
||||||
|
|
@ -284,6 +301,16 @@
|
||||||
(def-var! "clojure.core" "future-cancelled?" jolt-native-future-cancelled?)
|
(def-var! "clojure.core" "future-cancelled?" jolt-native-future-cancelled?)
|
||||||
(def-var! "clojure.core" "promise" jolt-promise-new)
|
(def-var! "clojure.core" "promise" jolt-promise-new)
|
||||||
(def-var! "clojure.core" "deliver" jolt-deliver)
|
(def-var! "clojure.core" "deliver" jolt-deliver)
|
||||||
|
;; a promise is an IFn on the JVM: (p val) delivers. Registered as a cold
|
||||||
|
;; invoke arm; callable-host? feeds the ifn? overlay (multimethods included).
|
||||||
|
(register-invoke-arm! jolt-promise?
|
||||||
|
(lambda (p args)
|
||||||
|
(if (and (pair? args) (null? (cdr args)))
|
||||||
|
(jolt-deliver p (car args))
|
||||||
|
(jolt-throw (jolt-host-throwable "clojure.lang.ArityException"
|
||||||
|
"Wrong number of args passed to a promise")))))
|
||||||
|
(def-var! "jolt.host" "callable-host?"
|
||||||
|
(lambda (x) (if (or (jolt-multifn? x) (jolt-promise? x)) #t jolt-nil)))
|
||||||
(def-var! "clojure.core" "agent" jolt-agent-new)
|
(def-var! "clojure.core" "agent" jolt-agent-new)
|
||||||
(def-var! "clojure.core" "agent?" jolt-agent?)
|
(def-var! "clojure.core" "agent?" jolt-agent?)
|
||||||
(def-var! "clojure.core" "send" jolt-agent-send)
|
(def-var! "clojure.core" "send" jolt-agent-send)
|
||||||
|
|
@ -459,6 +486,27 @@
|
||||||
(jolt-main-job-val job)
|
(jolt-main-job-val job)
|
||||||
(raise (jolt-main-job-val job))))))))
|
(raise (jolt-main-job-val job))))))))
|
||||||
|
|
||||||
|
(define jolt-pump-kih
|
||||||
|
(lambda ()
|
||||||
|
(for-each (lambda (th) (guard (e (#t #f)) (th)))
|
||||||
|
(reverse (unbox jolt-shutdown-hooks)))
|
||||||
|
(exit 0)))
|
||||||
|
|
||||||
|
;; Park the calling thread until a keyboard interrupt (^C), then run the shutdown
|
||||||
|
;; hooks and exit. Unlike run-main-pump (whose tight recursive condition-wait
|
||||||
|
;; loop elides Chez's interrupt poll points, so the handler never fires), this
|
||||||
|
;; uses a single condition-wait — the form Chez reliably interrupts. The nREPL
|
||||||
|
;; server parks here; SIGINT is unblocked in this thread first (it was masked by
|
||||||
|
;; jolt-block-sigint so the accept loop inherited a blocked mask and couldn't
|
||||||
|
;; absorb ^C in its foreign accept() call).
|
||||||
|
(define jolt-park-mu (make-mutex))
|
||||||
|
(define jolt-park-cv (make-condition))
|
||||||
|
(define (jolt-park-until-interrupt)
|
||||||
|
(keyboard-interrupt-handler jolt-pump-kih)
|
||||||
|
(jolt-set-sigint-blocked #f)
|
||||||
|
(with-mutex jolt-park-mu (condition-wait jolt-park-cv jolt-park-mu))
|
||||||
|
jolt-nil)
|
||||||
|
|
||||||
(define (jolt-run-main-pump)
|
(define (jolt-run-main-pump)
|
||||||
(with-mutex jolt-main-queue-mu
|
(with-mutex jolt-main-queue-mu
|
||||||
(set-box! jolt-main-pump-stop #f)
|
(set-box! jolt-main-pump-stop #f)
|
||||||
|
|
@ -508,6 +556,56 @@
|
||||||
(condition-broadcast jolt-main-queue-cv))
|
(condition-broadcast jolt-main-queue-cv))
|
||||||
jolt-nil)
|
jolt-nil)
|
||||||
|
|
||||||
|
;; Shutdown hooks run by jolt-pump-kih (the keyboard-interrupt-handler installed by
|
||||||
|
;; park-until-interrupt) before (exit 0), so a foreground server (nREPL) can close
|
||||||
|
;; its socket and drop .nrepl-port on ^C instead of Chez's default mutex-corrupting
|
||||||
|
;; abort. Newest-first; each hook is isolated so one failing hook can't block the exit.
|
||||||
|
(define jolt-shutdown-hooks (box '()))
|
||||||
|
(define (jolt-add-shutdown-hook thunk)
|
||||||
|
(set-box! jolt-shutdown-hooks (cons thunk (unbox jolt-shutdown-hooks)))
|
||||||
|
jolt-nil)
|
||||||
|
|
||||||
|
;; Per-thread SIGINT mask. A worker thread parked in a foreign call (the nREPL
|
||||||
|
;; accept loop in c-accept, or a conn handler) can't run Chez's keyboard-interrupt
|
||||||
|
;; handler on ^C, so if SIGINT is delivered there the process hangs. Block SIGINT
|
||||||
|
;; in the primordial thread BEFORE forking such workers (they inherit the mask),
|
||||||
|
;; then park-until-interrupt unblocks it in the primordial once its handler is
|
||||||
|
;; installed, so ^C is always delivered to the parked thread. pthread_sigmask/
|
||||||
|
;; sigaddset are libc/libpthread symbols, resolvable once the process object is
|
||||||
|
;; loaded (as the socket fns already are). 128 bytes covers Linux's 1024-bit
|
||||||
|
;; sigset_t and is larger than macOS's 4-byte one.
|
||||||
|
;; foreign-procedure resolves its symbol eagerly, and these POSIX signal fns don't
|
||||||
|
;; exist on Windows — resolving them unguarded aborted startup ("no entry for
|
||||||
|
;; 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).
|
||||||
|
(define c-pthread-sigmask
|
||||||
|
(jolt-foreign-proc-safe "pthread_sigmask" '(int u8* u8*) 'int))
|
||||||
|
(define c-sigemptyset (jolt-foreign-proc-safe "sigemptyset" '(u8*) 'int))
|
||||||
|
(define c-sigaddset (jolt-foreign-proc-safe "sigaddset" '(u8* int) 'int))
|
||||||
|
;; 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
|
||||||
|
;; this host from the machine-type symbol — macOS builds contain "osx".
|
||||||
|
(define jolt-sig-block-how
|
||||||
|
(let* ((s (symbol->string (machine-type)))
|
||||||
|
(n (string-length s)))
|
||||||
|
(let loop ((i 0))
|
||||||
|
(cond
|
||||||
|
((> (+ i 3) n) 0) ; default: Linux/glibc
|
||||||
|
((string=? (substring s i (+ i 3)) "osx") 1) ; Darwin/macOS
|
||||||
|
(else (loop (+ i 1)))))))
|
||||||
|
(define (jolt-set-sigint-blocked block?)
|
||||||
|
(when (and c-pthread-sigmask c-sigemptyset c-sigaddset)
|
||||||
|
(let ((set (make-bytevector 128 0))
|
||||||
|
(old (make-bytevector 128 0)))
|
||||||
|
(c-sigemptyset set)
|
||||||
|
(c-sigaddset set 2) ; SIGINT = 2
|
||||||
|
(c-pthread-sigmask (if block? jolt-sig-block-how (+ jolt-sig-block-how 1)) set old)))
|
||||||
|
jolt-nil)
|
||||||
|
|
||||||
(def-var! "jolt.host" "call-on-main-thread" jolt-call-on-main-thread)
|
(def-var! "jolt.host" "call-on-main-thread" jolt-call-on-main-thread)
|
||||||
(def-var! "jolt.host" "run-main-pump" jolt-run-main-pump)
|
(def-var! "jolt.host" "run-main-pump" jolt-run-main-pump)
|
||||||
(def-var! "jolt.host" "stop-main-pump" jolt-stop-main-pump)
|
(def-var! "jolt.host" "stop-main-pump" jolt-stop-main-pump)
|
||||||
|
(def-var! "jolt.host" "add-shutdown-hook" jolt-add-shutdown-hook)
|
||||||
|
(def-var! "jolt.host" "block-sigint" (lambda () (jolt-set-sigint-blocked #t)))
|
||||||
|
(def-var! "jolt.host" "park-until-interrupt" jolt-park-until-interrupt)
|
||||||
|
(def-var! "jolt.host" "delete-file" delete-file)
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,6 @@
|
||||||
;; A record (jrec) is jolt-map? here (records.ss makes it so) and a collection,
|
;; A record (jrec) is jolt-map? here (records.ss makes it so) and a collection,
|
||||||
;; so its protocol method (no dash, not a coll method) lands in the base.
|
;; so its protocol method (no dash, not a coll method) lands in the base.
|
||||||
|
|
||||||
(define %dot-rmd record-method-dispatch)
|
|
||||||
|
|
||||||
;; Vectors / maps / sets only (records are jolt-map? here). Raw seqs are excluded:
|
;; Vectors / maps / sets only (records are jolt-map? here). Raw seqs are excluded:
|
||||||
;; coll-interop accepts some seq representations and not others (a
|
;; coll-interop accepts some seq representations and not others (a
|
||||||
;; plain (seq v) returns nil from .count, a lazy-seq returns the count), an
|
;; plain (seq v) returns nil from .count, a lazy-seq returns the count), an
|
||||||
|
|
@ -62,6 +60,12 @@
|
||||||
;; branch and is mis-read as a missing :iterator key (nil). Some libraries
|
;; branch and is mis-read as a missing :iterator key (nil). Some libraries
|
||||||
;; (e.g. malli's -vmap) iterate a map this way.
|
;; (e.g. malli's -vmap) iterate a map this way.
|
||||||
((string=? name "iterator") (list (make-jiterator (jolt-seq obj))))
|
((string=? name "iterator") (list (make-jiterator (jolt-seq obj))))
|
||||||
|
;; (.reduce coll f) / (.reduce coll f init): clojure.lang.IReduce — every
|
||||||
|
;; persistent collection reduces itself on the JVM.
|
||||||
|
((string=? name "reduce")
|
||||||
|
(list (if (pair? (cdr args))
|
||||||
|
(jolt-reduce (car args) (cadr args) obj)
|
||||||
|
(jolt-reduce (car args) obj))))
|
||||||
(else #f)))
|
(else #f)))
|
||||||
|
|
||||||
;; Universal object-methods: on a
|
;; Universal object-methods: on a
|
||||||
|
|
@ -84,7 +88,7 @@
|
||||||
((string=? name "equals") (list (if (jolt= obj (car args)) #t #f)))
|
((string=? name "equals") (list (if (jolt= obj (car args)) #t #f)))
|
||||||
(else #f)))
|
(else #f)))
|
||||||
|
|
||||||
(set! record-method-dispatch
|
(register-method-arm! 30
|
||||||
(lambda (obj method-name rest-args)
|
(lambda (obj method-name rest-args)
|
||||||
(let* ((rest (if (jolt-nil? rest-args) '() (seq->list rest-args)))
|
(let* ((rest (if (jolt-nil? rest-args) '() (seq->list rest-args)))
|
||||||
(field? (and (> (string-length method-name) 0)
|
(field? (and (> (string-length method-name) 0)
|
||||||
|
|
@ -93,9 +97,6 @@
|
||||||
(substring method-name 1 (string-length method-name))
|
(substring method-name 1 (string-length method-name))
|
||||||
method-name)))
|
method-name)))
|
||||||
(cond
|
(cond
|
||||||
;; (.getClass x) universal — the class token for any value, before the
|
|
||||||
;; collection/map field-lookup arms below would read it as a missing key.
|
|
||||||
((string=? method-name "getClass") (jolt-class obj))
|
|
||||||
;; clojure.lang.MultiFn .dispatchFn / .getMethod — clojure.spec.alpha's
|
;; clojure.lang.MultiFn .dispatchFn / .getMethod — clojure.spec.alpha's
|
||||||
;; multi-spec walks a multimethod through these.
|
;; multi-spec walks a multimethod through these.
|
||||||
((jolt-multifn? obj)
|
((jolt-multifn? obj)
|
||||||
|
|
@ -107,7 +108,7 @@
|
||||||
(mm-find-isa obj dv)
|
(mm-find-isa obj dv)
|
||||||
(hashtable-ref methods (jolt-multifn-default obj) #f)
|
(hashtable-ref methods (jolt-multifn-default obj) #f)
|
||||||
jolt-nil)))
|
jolt-nil)))
|
||||||
(else (%dot-rmd obj method-name rest-args))))
|
(else 'pass)))
|
||||||
;; (.applyTo f args): apply a fn to a seq of args (clojure.spec instrument).
|
;; (.applyTo f args): apply a fn to a seq of args (clojure.spec instrument).
|
||||||
((and (procedure? obj) (string=? mname "applyTo"))
|
((and (procedure? obj) (string=? mname "applyTo"))
|
||||||
(apply jolt-invoke obj (seq->list (jolt-seq (car rest)))))
|
(apply jolt-invoke obj (seq->list (jolt-seq (car rest)))))
|
||||||
|
|
@ -119,7 +120,7 @@
|
||||||
((or (string=? mname "valAt") (string=? mname "get"))
|
((or (string=? mname "valAt") (string=? mname "get"))
|
||||||
(t-get obj (car rest) (if (null? (cdr rest)) jolt-nil (cadr rest))))
|
(t-get obj (car rest) (if (null? (cdr rest)) jolt-nil (cadr rest))))
|
||||||
((string=? mname "count") (t-count obj))
|
((string=? mname "count") (t-count obj))
|
||||||
(else (%dot-rmd obj method-name rest-args))))
|
(else 'pass)))
|
||||||
;; a deftype/record's OWN declared method (matched by name AND arity) wins
|
;; a deftype/record's OWN declared method (matched by name AND arity) wins
|
||||||
;; over the generic collection interop below — e.g. data.priority-map
|
;; over the generic collection interop below — e.g. data.priority-map
|
||||||
;; declares both seq[this] (Seqable) and seq[this ascending] (Sorted), and
|
;; declares both seq[this] (Seqable) and seq[this ascending] (Sorted), and
|
||||||
|
|
@ -145,4 +146,4 @@
|
||||||
(else
|
(else
|
||||||
(let ((v (jolt-get obj (keyword #f mname) jolt-nil)))
|
(let ((v (jolt-get obj (keyword #f mname) jolt-nil)))
|
||||||
(if (procedure? v) (apply jolt-invoke v obj rest) v)))))
|
(if (procedure? v) (apply jolt-invoke v obj rest) v)))))
|
||||||
(else (%dot-rmd obj method-name rest-args))))))
|
(else 'pass)))))
|
||||||
|
|
|
||||||
|
|
@ -176,14 +176,26 @@
|
||||||
;; push to the port (so (.write *out* s) and (binding [*out* *err*] …) work);
|
;; push to the port (so (.write *out* s) and (binding [*out* *err*] …) work);
|
||||||
;; it isn't a buffer, so toString is empty. Lets libraries that touch *out*/*err*
|
;; it isn't a buffer, so toString is empty. Lets libraries that touch *out*/*err*
|
||||||
;; (tools.logging, selmer) compile and run.
|
;; (tools.logging, selmer) compile and run.
|
||||||
|
;; *out*/*err* resolve their port LIVE — 'out -> (current-output-port), 'err ->
|
||||||
|
;; (current-error-port) — so a (.write *out* …) / (.flush *out*) follows a
|
||||||
|
;; with-out-str redirect (with-output-to-string rebinds current-output-port) the
|
||||||
|
;; same way print/__write do. Storing the startup port instead pinned *out* to the
|
||||||
|
;; real stdout, so rewrite-clj's (z/print) — which writes via *out* — escaped the
|
||||||
|
;; capture. A stored port object (should any other code make a port-writer) is used
|
||||||
|
;; as-is.
|
||||||
|
(define (port-writer-port self)
|
||||||
|
(let ((p (vector-ref (jhost-state self) 0)))
|
||||||
|
(cond ((eq? p 'out) (current-output-port))
|
||||||
|
((eq? p 'err) (current-error-port))
|
||||||
|
(else p))))
|
||||||
(register-host-methods! "port-writer"
|
(register-host-methods! "port-writer"
|
||||||
(list (cons "write" (lambda (self x) (display (writer-piece x) (vector-ref (jhost-state self) 0)) jolt-nil))
|
(list (cons "write" (lambda (self x) (display (writer-piece x) (port-writer-port self)) jolt-nil))
|
||||||
(cons "append" (lambda (self x . rest) (display (append-text x rest) (vector-ref (jhost-state self) 0)) self))
|
(cons "append" (lambda (self x . rest) (display (append-text x rest) (port-writer-port self)) self))
|
||||||
(cons "flush" (lambda (self) (flush-output-port (vector-ref (jhost-state self) 0)) jolt-nil))
|
(cons "flush" (lambda (self) (flush-output-port (port-writer-port self)) jolt-nil))
|
||||||
(cons "close" (lambda (self) jolt-nil))
|
(cons "close" (lambda (self) jolt-nil))
|
||||||
(cons "toString" (lambda (self) ""))))
|
(cons "toString" (lambda (self) ""))))
|
||||||
(def-var! "clojure.core" "*out*" (make-jhost "port-writer" (vector (current-output-port))))
|
(def-var! "clojure.core" "*out*" (make-jhost "port-writer" (vector 'out)))
|
||||||
(def-var! "clojure.core" "*err*" (make-jhost "port-writer" (vector (current-error-port))))
|
(def-var! "clojure.core" "*err*" (make-jhost "port-writer" (vector 'err)))
|
||||||
|
|
||||||
;; PrintWriter — a thin wrapper over a target writer. write/append/print forward
|
;; PrintWriter — a thin wrapper over a target writer. write/append/print forward
|
||||||
;; the rendered text to the target. clojure.data.json's pretty printer builds
|
;; the rendered text to the target. clojure.data.json's pretty printer builds
|
||||||
|
|
@ -375,6 +387,11 @@
|
||||||
;; state: a vector #(wrapped-reader pushed-list)
|
;; state: a vector #(wrapped-reader pushed-list)
|
||||||
(register-class-ctor! "PushbackReader"
|
(register-class-ctor! "PushbackReader"
|
||||||
(lambda (rdr . _) (make-jhost "pushback-reader" (vector rdr '()))))
|
(lambda (rdr . _) (make-jhost "pushback-reader" (vector rdr '()))))
|
||||||
|
;; Fully-qualified aliases so (java.io.PushbackReader. …) / (java.io.StringReader. …)
|
||||||
|
;; resolve to these built-ins even when a library defines a deftype of the same
|
||||||
|
;; simple name (tools.reader), which would otherwise take the bare-name slot.
|
||||||
|
(register-class-ctor! "java.io.PushbackReader" (lookup-class class-ctors-tbl "PushbackReader"))
|
||||||
|
(register-class-ctor! "java.io.StringReader" (lookup-class class-ctors-tbl "StringReader"))
|
||||||
;; LineNumberingPushbackReader: a pushback-reader (jolt doesn't track line
|
;; LineNumberingPushbackReader: a pushback-reader (jolt doesn't track line
|
||||||
;; numbers; getLineNumber is a stub for error-reporting paths that read it).
|
;; numbers; getLineNumber is a stub for error-reporting paths that read it).
|
||||||
(register-class-ctor! "LineNumberingPushbackReader"
|
(register-class-ctor! "LineNumberingPushbackReader"
|
||||||
|
|
@ -438,7 +455,15 @@
|
||||||
(let ((toks (vector-ref (jhost-state self) 0)) (p (vector-ref (jhost-state self) 1)))
|
(let ((toks (vector-ref (jhost-state self) 0)) (p (vector-ref (jhost-state self) 1)))
|
||||||
(if (< p (length toks))
|
(if (< p (length toks))
|
||||||
(begin (vector-set! (jhost-state self) 1 (+ p 1)) (list-ref toks p))
|
(begin (vector-set! (jhost-state self) 1 (+ p 1)) (list-ref toks p))
|
||||||
(error #f "NoSuchElementException")))))))
|
(jolt-throw (jolt-host-throwable "java.util.NoSuchElementException" "no more tokens"))))))
|
||||||
|
;; StringTokenizer implements java.util.Enumeration — enumeration-seq drives
|
||||||
|
;; it through these, so alias them onto the token methods.
|
||||||
|
(cons "hasMoreElements" (lambda (self) (< (vector-ref (jhost-state self) 1) (length (vector-ref (jhost-state self) 0)))))
|
||||||
|
(cons "nextElement" (lambda (self)
|
||||||
|
(let ((toks (vector-ref (jhost-state self) 0)) (p (vector-ref (jhost-state self) 1)))
|
||||||
|
(if (< p (length toks))
|
||||||
|
(begin (vector-set! (jhost-state self) 1 (+ p 1)) (list-ref toks p))
|
||||||
|
(jolt-throw (jolt-host-throwable "java.util.NoSuchElementException" "no more tokens"))))))))
|
||||||
|
|
||||||
;; ---- String / BigInteger / MapEntry constructors ----------------------------
|
;; ---- String / BigInteger / MapEntry constructors ----------------------------
|
||||||
;; (String. bytes [charset]) decodes bytes (a bytevector OR a jolt byte-array)
|
;; (String. bytes [charset]) decodes bytes (a bytevector OR a jolt byte-array)
|
||||||
|
|
@ -617,8 +642,7 @@
|
||||||
;; record-method-dispatch already routes string? -> jolt-string-method. Add a
|
;; record-method-dispatch already routes string? -> jolt-string-method. Add a
|
||||||
;; regex-t arm (Pattern .split / .matcher-less surface used by corpus) by wrapping
|
;; regex-t arm (Pattern .split / .matcher-less surface used by corpus) by wrapping
|
||||||
;; once more — a regex-t isn't a jhost.
|
;; once more — a regex-t isn't a jhost.
|
||||||
(define %hs-rmd2 record-method-dispatch)
|
(register-method-arm! 42
|
||||||
(set! record-method-dispatch
|
|
||||||
(lambda (obj method-name rest-args)
|
(lambda (obj method-name rest-args)
|
||||||
(let ((rest (if (jolt-nil? rest-args) '() (seq->list rest-args))))
|
(let ((rest (if (jolt-nil? rest-args) '() (seq->list rest-args))))
|
||||||
(cond
|
(cond
|
||||||
|
|
@ -642,7 +666,7 @@
|
||||||
((string=? method-name "group") (apply jolt-matcher-group obj rest))
|
((string=? method-name "group") (apply jolt-matcher-group obj rest))
|
||||||
((string=? method-name "groupCount") (jolt-matcher-group-count obj))
|
((string=? method-name "groupCount") (jolt-matcher-group-count obj))
|
||||||
(else (error #f (string-append "No method " method-name " on Matcher")))))
|
(else (error #f (string-append "No method " method-name " on Matcher")))))
|
||||||
(else (%hs-rmd2 obj method-name rest-args))))))
|
(else 'pass)))))
|
||||||
|
|
||||||
;; ---- def-var! the registry entry points so emit can also reach them ---------
|
;; ---- def-var! the registry entry points so emit can also reach them ---------
|
||||||
(def-var! "clojure.core" "host-static-ref" host-static-ref)
|
(def-var! "clojure.core" "host-static-ref" host-static-ref)
|
||||||
|
|
@ -684,15 +708,14 @@
|
||||||
|
|
||||||
;; htable arm: dispatch (.method obj a*) through the table's tag method registry;
|
;; htable arm: dispatch (.method obj a*) through the table's tag method registry;
|
||||||
;; an unregistered method falls through (sorted colls are htables too).
|
;; an unregistered method falls through (sorted colls are htables too).
|
||||||
(define %hs-rmd-htable record-method-dispatch)
|
(register-method-arm! 43
|
||||||
(set! record-method-dispatch
|
|
||||||
(lambda (obj method-name rest-args)
|
(lambda (obj method-name rest-args)
|
||||||
(let ((tag (and (htable? obj) (hashtable-ref (htable-h obj) "jolt/type" #f))))
|
(let ((tag (and (htable? obj) (hashtable-ref (htable-h obj) "jolt/type" #f))))
|
||||||
(let* ((mh (and tag (hashtable-ref tagged-methods-tbl (tag->method-key tag) #f)))
|
(let* ((mh (and tag (hashtable-ref tagged-methods-tbl (tag->method-key tag) #f)))
|
||||||
(f (and mh (hashtable-ref mh method-name #f))))
|
(f (and mh (hashtable-ref mh method-name #f))))
|
||||||
(if f
|
(if f
|
||||||
(apply f obj (if (jolt-nil? rest-args) '() (seq->list rest-args)))
|
(apply f obj (if (jolt-nil? rest-args) '() (seq->list rest-args)))
|
||||||
(%hs-rmd-htable obj method-name rest-args))))))
|
'pass)))))
|
||||||
|
|
||||||
(def-var! "clojure.core" "__register-class-methods!"
|
(def-var! "clojure.core" "__register-class-methods!"
|
||||||
(lambda (tag members) (register-tagged-methods! tag (jmap->static-alist members)) jolt-nil))
|
(lambda (tag members) (register-tagged-methods! tag (jmap->static-alist members)) jolt-nil))
|
||||||
|
|
@ -742,6 +765,12 @@
|
||||||
(register-instance-check-arm!
|
(register-instance-check-arm!
|
||||||
(lambda (type-sym val)
|
(lambda (type-sym val)
|
||||||
(let ((iface (hsc-last-segment (symbol-t-name type-sym))))
|
(let ((iface (hsc-last-segment (symbol-t-name type-sym))))
|
||||||
|
;; the value's own class-graph tags (value-host-tags) are authoritative — the
|
||||||
|
;; SAME source protocol dispatch reads, so instance? and extend-protocol can't
|
||||||
|
;; disagree about the interfaces a builtin implements.
|
||||||
|
(if (let ((tags (value-host-tags val)))
|
||||||
|
(or (member (symbol-t-name type-sym) tags) (member iface tags)))
|
||||||
|
#t
|
||||||
(let ((hit (cond
|
(let ((hit (cond
|
||||||
((or (string=? iface "IObj") (string=? iface "IMeta")) (hsc-imeta? val))
|
((or (string=? iface "IObj") (string=? iface "IMeta")) (hsc-imeta? val))
|
||||||
((or (string=? iface "IMapEntry") (string=? iface "MapEntry")) (jolt-map-entry? val))
|
((or (string=? iface "IMapEntry") (string=? iface "MapEntry")) (jolt-map-entry? val))
|
||||||
|
|
@ -802,7 +831,7 @@
|
||||||
((or (string=? iface "Reader") (string=? iface "BufferedReader"))
|
((or (string=? iface "Reader") (string=? iface "BufferedReader"))
|
||||||
(reader-jhost? val))
|
(reader-jhost? val))
|
||||||
(else 'none))))
|
(else 'none))))
|
||||||
(if (eq? hit 'none) 'pass (if hit #t #f))))))
|
(if (eq? hit 'none) 'pass (if hit #t #f)))))))
|
||||||
|
|
||||||
;; java.lang.Class value: (class x) / (.getClass x) return one. It renders like
|
;; java.lang.Class value: (class x) / (.getClass x) return one. It renders like
|
||||||
;; the JVM — str/.toString -> "class <name>", pr -> "<name>", .getName -> "<name>"
|
;; the JVM — str/.toString -> "class <name>", pr -> "<name>", .getName -> "<name>"
|
||||||
|
|
@ -812,7 +841,12 @@
|
||||||
(define (make-class-obj name) (make-jhost "class" (vector name)))
|
(define (make-class-obj name) (make-jhost "class" (vector name)))
|
||||||
(define (jclass? x) (and (jhost? x) (string=? (jhost-tag x) "class")))
|
(define (jclass? x) (and (jhost? x) (string=? (jhost-tag x) "class")))
|
||||||
(define (jclass-name x) (vector-ref (jhost-state x) 0))
|
(define (jclass-name x) (vector-ref (jhost-state x) 0))
|
||||||
(define (class-key x) (cond ((jclass? x) (jclass-name x)) ((string? x) x) (else #f)))
|
(define (class-key x)
|
||||||
|
(cond ((jclass? x) (jclass-name x))
|
||||||
|
((string? x) x)
|
||||||
|
;; a deftype/defrecord NAME var holds its ctor; treat it as the class
|
||||||
|
((procedure? x) (hashtable-ref chez-deftype-ctor-tag x #f))
|
||||||
|
(else #f)))
|
||||||
(register-eq-arm! (lambda (a b) (or (jclass? a) (jclass? b)))
|
(register-eq-arm! (lambda (a b) (or (jclass? a) (jclass? b)))
|
||||||
(lambda (a b) (let ((ka (class-key a)) (kb (class-key b)))
|
(lambda (a b) (let ((ka (class-key a)) (kb (class-key b)))
|
||||||
(and ka kb (string=? ka kb) #t))))
|
(and ka kb (string=? ka kb) #t))))
|
||||||
|
|
@ -960,9 +994,20 @@
|
||||||
(define (str-has-dollar? s)
|
(define (str-has-dollar? s)
|
||||||
(let loop ((i 0)) (and (< i (string-length s)) (or (char=? (string-ref s i) #\$) (loop (+ i 1))))))
|
(let loop ((i 0)) (and (< i (string-length s)) (or (char=? (string-ref s i) #\$) (loop (+ i 1))))))
|
||||||
(define (class-direct-supers name)
|
(define (class-direct-supers name)
|
||||||
(or (hashtable-ref class-supers-tbl name #f)
|
;; union the modeled class graph (jch, direct edges) with any legacy table entry,
|
||||||
(and (str-has-dollar? name) '("clojure.lang.AFunction"))
|
;; so isa?/supers/ancestors see the single hierarchy source plus anything not yet
|
||||||
'()))
|
;; migrated. The closure below traverses these to the full transitive set.
|
||||||
|
(let ((jch (jch-direct-supers name))
|
||||||
|
(old (hashtable-ref class-supers-tbl name #f)))
|
||||||
|
(cond ((and (pair? jch) old)
|
||||||
|
(let merge ((ss old) (acc jch))
|
||||||
|
(cond ((null? ss) acc)
|
||||||
|
((member (car ss) acc) (merge (cdr ss) acc))
|
||||||
|
(else (merge (cdr ss) (append acc (list (car ss))))))))
|
||||||
|
((pair? jch) jch)
|
||||||
|
(old old)
|
||||||
|
((str-has-dollar? name) '("clojure.lang.AFunction"))
|
||||||
|
(else '()))))
|
||||||
;; transitive closure of direct supers (set semantics via an accumulator list)
|
;; transitive closure of direct supers (set semantics via an accumulator list)
|
||||||
(define (class-ancestors-list name)
|
(define (class-ancestors-list name)
|
||||||
(let loop ((pending (class-direct-supers name)) (seen '()))
|
(let loop ((pending (class-direct-supers name)) (seen '()))
|
||||||
|
|
@ -1007,18 +1052,62 @@
|
||||||
#t jolt-nil))
|
#t jolt-nil))
|
||||||
jolt-nil))))
|
jolt-nil))))
|
||||||
|
|
||||||
|
;; is NAME a class the host models (registered in the class graph, a legacy
|
||||||
|
;; supers-table entry, or a fn class)? Object itself is modeled.
|
||||||
|
(define (hsc-class-known? name)
|
||||||
|
(or (string=? name "java.lang.Object")
|
||||||
|
(jch-known? name)
|
||||||
|
(and (hashtable-ref class-supers-tbl name #f) #t)
|
||||||
|
(str-has-dollar? name)))
|
||||||
|
|
||||||
|
;; transitive ancestry, rooted at Object for a concrete class like (supers c);
|
||||||
|
;; an interface's chain has no Object (its getSuperclass is null). '() for
|
||||||
|
;; Object itself and for a name the host doesn't model.
|
||||||
|
(define (class-ancestors-rooted name)
|
||||||
|
(if (or (string=? name "java.lang.Object") (jch-interface? name))
|
||||||
|
(class-ancestors-list name)
|
||||||
|
(let ((as (class-ancestors-list name)))
|
||||||
|
(cond ((member "java.lang.Object" as) as)
|
||||||
|
((null? as) (if (hsc-class-known? name) '("java.lang.Object") '()))
|
||||||
|
(else (append as '("java.lang.Object")))))))
|
||||||
|
|
||||||
;; (jolt.host/class-supers name) / (jolt.host/class-ancestors name) — a jolt seq of
|
;; (jolt.host/class-supers name) / (jolt.host/class-ancestors name) — a jolt seq of
|
||||||
;; super / ancestor class-name strings, or nil when jolt models no hierarchy for it.
|
;; super / ancestor class-name strings (transitive, Object-rooted), or nil when
|
||||||
|
;; jolt models no hierarchy for it. class-bases is the DIRECT supers (clojure.core
|
||||||
|
;; `bases` / the class arm of `parents`).
|
||||||
(def-var! "jolt.host" "class-supers"
|
(def-var! "jolt.host" "class-supers"
|
||||||
(lambda (x)
|
(lambda (x)
|
||||||
(let ((name (class-key x)))
|
(let ((name (class-key x)))
|
||||||
(if (and name (hashtable-contains? class-supers-tbl name))
|
(if name
|
||||||
(list->cseq (hashtable-ref class-supers-tbl name '()))
|
(let ((as (class-ancestors-rooted name)))
|
||||||
|
(if (null? as) jolt-nil (list->cseq as)))
|
||||||
jolt-nil))))
|
jolt-nil))))
|
||||||
(def-var! "jolt.host" "class-ancestors"
|
(def-var! "jolt.host" "class-ancestors"
|
||||||
(lambda (x)
|
(lambda (x)
|
||||||
(let ((name (class-key x)))
|
(let ((name (class-key x)))
|
||||||
(if name
|
(if name
|
||||||
(let ((as (class-ancestors-list name)))
|
(let ((as (class-ancestors-rooted name)))
|
||||||
(if (null? as) jolt-nil (list->cseq as)))
|
(if (null? as) jolt-nil (list->cseq as)))
|
||||||
jolt-nil))))
|
jolt-nil))))
|
||||||
|
(def-var! "jolt.host" "class-bases"
|
||||||
|
(lambda (x)
|
||||||
|
(let ((name (class-key x)))
|
||||||
|
(if name
|
||||||
|
(let* ((ds (class-direct-supers name))
|
||||||
|
;; a concrete class's bases include its superclass — Object when
|
||||||
|
;; nothing more specific is modeled (interfaces have none).
|
||||||
|
(ds (if (or (string=? name "java.lang.Object")
|
||||||
|
(jch-interface? name)
|
||||||
|
(member "java.lang.Object" ds))
|
||||||
|
ds
|
||||||
|
(append ds '("java.lang.Object")))))
|
||||||
|
(if (null? ds) jolt-nil (list->cseq ds)))
|
||||||
|
jolt-nil))))
|
||||||
|
;; is X a class value — a jclass, a deftype ctor, or a name string the host
|
||||||
|
;; graph models?
|
||||||
|
(def-var! "jolt.host" "class-value?"
|
||||||
|
(lambda (x)
|
||||||
|
(if (jclass? x)
|
||||||
|
#t
|
||||||
|
(let ((n (class-key x)))
|
||||||
|
(if (and n (hsc-class-known? n)) #t jolt-nil)))))
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,7 @@
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(unless tried?
|
(unless tried?
|
||||||
(set! tried? #t)
|
(set! tried? #t)
|
||||||
(set! fp (guard (e (#t #f))
|
(set! fp (jolt-foreign-proc-safe "sched_yield" '() 'int)))
|
||||||
(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)))
|
||||||
|
|
||||||
|
|
@ -202,7 +200,8 @@
|
||||||
(bitwise-ior (bitwise-arithmetic-shift-left r 1)
|
(bitwise-ior (bitwise-arithmetic-shift-left r 1)
|
||||||
(bitwise-and (bitwise-arithmetic-shift-right v i) 1)))))))
|
(bitwise-and (bitwise-arithmetic-shift-right v i) 1)))))))
|
||||||
(register-class-statics! "Long"
|
(register-class-statics! "Long"
|
||||||
(list (cons "MAX_VALUE" (->num 9223372036854775807))
|
(list (cons "TYPE" "long")
|
||||||
|
(cons "MAX_VALUE" (->num 9223372036854775807))
|
||||||
(cons "MIN_VALUE" (->num -9223372036854775808))
|
(cons "MIN_VALUE" (->num -9223372036854775808))
|
||||||
(cons "bitCount" (lambda (n) (->num (bitwise-bit-count (bitwise-and (jnum->exact n) long-mask64)))))
|
(cons "bitCount" (lambda (n) (->num (bitwise-bit-count (bitwise-and (jnum->exact n) long-mask64)))))
|
||||||
(cons "numberOfLeadingZeros" (lambda (n) (->num (long-nlz n))))
|
(cons "numberOfLeadingZeros" (lambda (n) (->num (long-nlz n))))
|
||||||
|
|
@ -214,6 +213,8 @@
|
||||||
(define (int->u32 n) (if (< n 0) (+ n 4294967296) n))
|
(define (int->u32 n) (if (< n 0) (+ n 4294967296) n))
|
||||||
(register-class-statics! "Integer"
|
(register-class-statics! "Integer"
|
||||||
(list (cons "MAX_VALUE" (->num 2147483647)) (cons "MIN_VALUE" (->num -2147483648))
|
(list (cons "MAX_VALUE" (->num 2147483647)) (cons "MIN_VALUE" (->num -2147483648))
|
||||||
|
;; the primitive class token (int.class); jolt models a class as its name
|
||||||
|
(cons "TYPE" "int")
|
||||||
(cons "valueOf" (lambda (x . r)
|
(cons "valueOf" (lambda (x . r)
|
||||||
(if (number? x) (->num x)
|
(if (number? x) (->num x)
|
||||||
(parse-int-or-throw x (if (null? r) 10 (jnum->exact (car r))) "valueOf"))))
|
(parse-int-or-throw x (if (null? r) 10 (jnum->exact (car r))) "valueOf"))))
|
||||||
|
|
@ -227,12 +228,14 @@
|
||||||
;; Byte / Short bounds (their values are plain integers on jolt; the statics let
|
;; Byte / Short bounds (their values are plain integers on jolt; the statics let
|
||||||
;; libraries reference the JVM ranges — clojure.test.check generates over them).
|
;; libraries reference the JVM ranges — clojure.test.check generates over them).
|
||||||
(register-class-statics! "Byte"
|
(register-class-statics! "Byte"
|
||||||
(list (cons "MAX_VALUE" (->num 127)) (cons "MIN_VALUE" (->num -128))
|
(list (cons "TYPE" "byte")
|
||||||
|
(cons "MAX_VALUE" (->num 127)) (cons "MIN_VALUE" (->num -128))
|
||||||
(cons "valueOf" (lambda (x . r) (->num (if (number? x) x (parse-int-or-throw x 10 "valueOf")))))
|
(cons "valueOf" (lambda (x . r) (->num (if (number? x) x (parse-int-or-throw x 10 "valueOf")))))
|
||||||
(cons "parseByte" (lambda (x . r) (parse-int-or-throw x (if (null? r) 10 (jnum->exact (car r))) "parseByte")))
|
(cons "parseByte" (lambda (x . r) (parse-int-or-throw x (if (null? r) 10 (jnum->exact (car r))) "parseByte")))
|
||||||
(cons "toString" (lambda (x . r) (number->string (jnum->exact x))))))
|
(cons "toString" (lambda (x . r) (number->string (jnum->exact x))))))
|
||||||
(register-class-statics! "Short"
|
(register-class-statics! "Short"
|
||||||
(list (cons "MAX_VALUE" (->num 32767)) (cons "MIN_VALUE" (->num -32768))
|
(list (cons "TYPE" "short")
|
||||||
|
(cons "MAX_VALUE" (->num 32767)) (cons "MIN_VALUE" (->num -32768))
|
||||||
(cons "valueOf" (lambda (x . r) (->num (if (number? x) x (parse-int-or-throw x 10 "valueOf")))))
|
(cons "valueOf" (lambda (x . r) (->num (if (number? x) x (parse-int-or-throw x 10 "valueOf")))))
|
||||||
(cons "parseShort" (lambda (x . r) (parse-int-or-throw x (if (null? r) 10 (jnum->exact (car r))) "parseShort")))
|
(cons "parseShort" (lambda (x . r) (parse-int-or-throw x (if (null? r) 10 (jnum->exact (car r))) "parseShort")))
|
||||||
(cons "toString" (lambda (x . r) (number->string (jnum->exact x))))))
|
(cons "toString" (lambda (x . r) (number->string (jnum->exact x))))))
|
||||||
|
|
@ -247,13 +250,15 @@
|
||||||
(cons "ROOT" "und") (cons "US" "en-US") (cons "ENGLISH" "en")))
|
(cons "ROOT" "und") (cons "US" "en-US") (cons "ENGLISH" "en")))
|
||||||
|
|
||||||
(register-class-statics! "Boolean"
|
(register-class-statics! "Boolean"
|
||||||
(list (cons "parseBoolean" (lambda (s) (string=? "true" (ascii-string-down (if (string? s) s (jolt-str-render-one s))))))
|
(list (cons "TYPE" "boolean")
|
||||||
|
(cons "parseBoolean" (lambda (s) (string=? "true" (ascii-string-down (if (string? s) s (jolt-str-render-one s))))))
|
||||||
(cons "TRUE" #t) (cons "FALSE" #f)))
|
(cons "TRUE" #t) (cons "FALSE" #f)))
|
||||||
|
|
||||||
(register-class-ctor! "Double" ->double)
|
(register-class-ctor! "Double" ->double)
|
||||||
(register-class-ctor! "Float" ->double)
|
(register-class-ctor! "Float" ->double)
|
||||||
(register-class-statics! "Double"
|
(register-class-statics! "Double"
|
||||||
(list (cons "parseDouble" parse-double-or-throw)
|
(list (cons "TYPE" "double")
|
||||||
|
(cons "parseDouble" parse-double-or-throw)
|
||||||
(cons "valueOf" ->double)
|
(cons "valueOf" ->double)
|
||||||
(cons "toString" (lambda (x) (jolt-str-render-one (->double x))))
|
(cons "toString" (lambda (x) (jolt-str-render-one (->double x))))
|
||||||
(cons "isNaN" (lambda (x) (and (flonum? x) (nan? x))))
|
(cons "isNaN" (lambda (x) (and (flonum? x) (nan? x))))
|
||||||
|
|
@ -261,14 +266,21 @@
|
||||||
(cons "MAX_VALUE" 1.7976931348623157e308) (cons "MIN_VALUE" 4.9e-324)
|
(cons "MAX_VALUE" 1.7976931348623157e308) (cons "MIN_VALUE" 4.9e-324)
|
||||||
(cons "POSITIVE_INFINITY" +inf.0) (cons "NEGATIVE_INFINITY" -inf.0) (cons "NaN" +nan.0)))
|
(cons "POSITIVE_INFINITY" +inf.0) (cons "NEGATIVE_INFINITY" -inf.0) (cons "NaN" +nan.0)))
|
||||||
(register-class-statics! "Float"
|
(register-class-statics! "Float"
|
||||||
(list (cons "parseFloat" parse-double-or-throw) (cons "valueOf" ->double)))
|
(list (cons "TYPE" "float")
|
||||||
|
(cons "parseFloat" parse-double-or-throw) (cons "valueOf" ->double)))
|
||||||
|
|
||||||
;; Character: ASCII predicates (the engine is byte/ASCII oriented).
|
;; Character: ASCII predicates (the engine is byte/ASCII oriented).
|
||||||
(register-class-statics! "Character"
|
(register-class-statics! "Character"
|
||||||
(list (cons "isUpperCase" (lambda (c) (let ((n (char-code c))) (and (>= n 65) (<= n 90)))))
|
(list (cons "TYPE" "char")
|
||||||
|
(cons "isUpperCase" (lambda (c) (let ((n (char-code c))) (and (>= n 65) (<= n 90)))))
|
||||||
(cons "isLowerCase" (lambda (c) (let ((n (char-code c))) (and (>= n 97) (<= n 122)))))
|
(cons "isLowerCase" (lambda (c) (let ((n (char-code c))) (and (>= n 97) (<= n 122)))))
|
||||||
(cons "isDigit" (lambda (c) (let ((n (char-code c))) (and (>= n 48) (<= n 57)))))
|
(cons "isDigit" (lambda (c) (let ((n (char-code c))) (and (>= n 48) (<= n 57)))))
|
||||||
(cons "isWhitespace" (lambda (c) (char<=? (integer->char (char-code c)) #\space)))))
|
;; JVM Character.isWhitespace: Unicode whitespace (so U+2028 line separator
|
||||||
|
;; counts, like the JVM) MINUS the no-break spaces the JVM excludes
|
||||||
|
;; (U+00A0/U+2007/U+202F). char<=?space missed everything above ASCII.
|
||||||
|
(cons "isWhitespace" (lambda (c) (let ((cp (char-code c)))
|
||||||
|
(and (char-whitespace? (integer->char cp))
|
||||||
|
(not (fx=? cp #xA0)) (not (fx=? cp #x2007)) (not (fx=? cp #x202F))))))))
|
||||||
|
|
||||||
;; String/valueOf(Object): "null" for nil, else jolt's str semantics.
|
;; String/valueOf(Object): "null" for nil, else jolt's str semantics.
|
||||||
;; String/format(fmt args…) / (locale fmt args…) -> the clojure.core format engine.
|
;; String/format(fmt args…) / (locale fmt args…) -> the clojure.core format engine.
|
||||||
|
|
|
||||||
|
|
@ -90,13 +90,9 @@
|
||||||
(list->cseq (if asc keep (reverse keep)))))
|
(list->cseq (if asc keep (reverse keep)))))
|
||||||
(else (error #f (string-append "No method " method " on sorted collection")))))
|
(else (error #f (string-append "No method " method " on sorted collection")))))
|
||||||
|
|
||||||
(define %hs-record-method-dispatch record-method-dispatch)
|
(register-method-arm! 44
|
||||||
(set! record-method-dispatch
|
|
||||||
(lambda (obj method-name rest-args)
|
(lambda (obj method-name rest-args)
|
||||||
(cond
|
(cond
|
||||||
;; (.getClass x) is universal — the class token for any value (incl. numbers
|
|
||||||
;; / jhost) — before the per-type arms that would otherwise reject it.
|
|
||||||
((string=? method-name "getClass") (jolt-class obj))
|
|
||||||
((jhost? obj)
|
((jhost? obj)
|
||||||
(let ((mh (hashtable-ref host-methods-tbl (jhost-tag obj) #f)))
|
(let ((mh (hashtable-ref host-methods-tbl (jhost-tag obj) #f)))
|
||||||
(let ((f (and mh (hashtable-ref mh method-name #f))))
|
(let ((f (and mh (hashtable-ref mh method-name #f))))
|
||||||
|
|
@ -104,7 +100,7 @@
|
||||||
(apply f obj (if (jolt-nil? rest-args) '() (seq->list rest-args)))
|
(apply f obj (if (jolt-nil? rest-args) '() (seq->list rest-args)))
|
||||||
(error #f (string-append "No method " method-name " on host " (jhost-tag obj)))))))
|
(error #f (string-append "No method " method-name " on host " (jhost-tag obj)))))))
|
||||||
((number? obj) (apply number-method method-name obj (if (jolt-nil? rest-args) '() (seq->list rest-args))))
|
((number? obj) (apply number-method method-name obj (if (jolt-nil? rest-args) '() (seq->list rest-args))))
|
||||||
(else (%hs-record-method-dispatch obj method-name rest-args)))))
|
(else 'pass))))
|
||||||
|
|
||||||
;; java.lang.Number method surface (the boxed-number methods cljc code calls). The
|
;; java.lang.Number method surface (the boxed-number methods cljc code calls). The
|
||||||
;; integer projections wrap modulo their width (ring-codec relies on byteValue
|
;; integer projections wrap modulo their width (ring-codec relies on byteValue
|
||||||
|
|
@ -117,7 +113,12 @@
|
||||||
((string=? method "longValue") (->num (jnum->exact n)))
|
((string=? method "longValue") (->num (jnum->exact n)))
|
||||||
((string=? method "doubleValue") (->num n))
|
((string=? method "doubleValue") (->num n))
|
||||||
((string=? method "floatValue") (->num n))
|
((string=? method "floatValue") (->num n))
|
||||||
((string=? method "toString") (jolt-num->string n))
|
;; .toString(radix) — BigInteger/Integer render in a base, lowercase like the
|
||||||
|
;; JVM (rewrite-clj's integer node reconstructs 0xff / 0377 / 2r1001 this way).
|
||||||
|
((string=? method "toString")
|
||||||
|
(if (pair? args)
|
||||||
|
(string-downcase (number->string (jnum->exact n) (jnum->exact (car args))))
|
||||||
|
(jolt-num->string n)))
|
||||||
((string=? method "hashCode") (->num (jnum->exact n)))
|
((string=? method "hashCode") (->num (jnum->exact n)))
|
||||||
;; Double/Float .isNaN / .isInfinite (a non-flonum is neither).
|
;; Double/Float .isNaN / .isInfinite (a non-flonum is neither).
|
||||||
((string=? method "isNaN") (and (flonum? n) (not (= n n))))
|
((string=? method "isNaN") (and (flonum? n) (not (= n n))))
|
||||||
|
|
@ -193,8 +194,9 @@
|
||||||
(and n (integer? n) (->num n))))
|
(and n (integer? n) (->num n))))
|
||||||
(define (parse-int-or-throw s radix what)
|
(define (parse-int-or-throw s radix what)
|
||||||
(or (parse-int-str s radix)
|
(or (parse-int-str s radix)
|
||||||
(error #f (string-append "NumberFormatException: For input string: \""
|
(jolt-throw (jolt-host-throwable "java.lang.NumberFormatException"
|
||||||
(if (string? s) s (jolt-str-render-one s)) "\""))))
|
(string-append "For input string: \""
|
||||||
|
(if (string? s) s (jolt-str-render-one s)) "\"")))))
|
||||||
(define (char-code c) (if (char? c) (char->integer c) (jnum->exact c)))
|
(define (char-code c) (if (char? c) (char->integer c) (jnum->exact c)))
|
||||||
|
|
||||||
;; parse a double string (Double/parseDouble, (Double. s)); JVM accepts NaN /
|
;; parse a double string (Double/parseDouble, (Double. s)); JVM accepts NaN /
|
||||||
|
|
@ -208,7 +210,8 @@
|
||||||
(else (let ((n (string->number t))) (and n (real? n) (exact->inexact n)))))))
|
(else (let ((n (string->number t))) (and n (real? n) (exact->inexact n)))))))
|
||||||
(define (parse-double-or-throw s)
|
(define (parse-double-or-throw s)
|
||||||
(or (parse-double-str s)
|
(or (parse-double-str s)
|
||||||
(error #f (string-append "NumberFormatException: For input string: \""
|
(jolt-throw (jolt-host-throwable "java.lang.NumberFormatException"
|
||||||
(if (string? s) s (jolt-str-render-one s)) "\""))))
|
(string-append "For input string: \""
|
||||||
|
(if (string? s) s (jolt-str-render-one s)) "\"")))))
|
||||||
(define (->double x) (if (number? x) (exact->inexact x) (parse-double-or-throw x)))
|
(define (->double x) (if (number? x) (exact->inexact x) (parse-double-or-throw x)))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -562,8 +562,7 @@
|
||||||
(cons "format" (lambda (self d) (format-ms (vector-ref (jhost-state self) 0) (ms-of d))))))
|
(cons "format" (lambda (self d) (format-ms (vector-ref (jhost-state self) 0) (ms-of d))))))
|
||||||
|
|
||||||
;; a jinst's java.util.Date method surface (record-method-dispatch arm).
|
;; a jinst's java.util.Date method surface (record-method-dispatch arm).
|
||||||
(define %it-rmd record-method-dispatch)
|
(register-method-arm! 40
|
||||||
(set! record-method-dispatch
|
|
||||||
(lambda (obj method-name rest-args)
|
(lambda (obj method-name rest-args)
|
||||||
(cond
|
(cond
|
||||||
((jinst? obj)
|
((jinst? obj)
|
||||||
|
|
@ -586,7 +585,7 @@
|
||||||
((string=? method-name "before") (< (jinst-ms obj) (ms-of (car (seq->list rest-args)))))
|
((string=? method-name "before") (< (jinst-ms obj) (ms-of (car (seq->list rest-args)))))
|
||||||
((string=? method-name "after") (> (jinst-ms obj) (ms-of (car (seq->list rest-args)))))
|
((string=? method-name "after") (> (jinst-ms obj) (ms-of (car (seq->list rest-args)))))
|
||||||
(else (error #f (string-append "No method " method-name " on Date")))))
|
(else (error #f (string-append "No method " method-name " on Date")))))
|
||||||
(else (%it-rmd obj method-name rest-args)))))
|
(else 'pass))))
|
||||||
|
|
||||||
;; Clojure's built-in data readers, so a library that merges default-data-readers
|
;; Clojure's built-in data readers, so a library that merges default-data-readers
|
||||||
;; or binds *data-readers* (e.g. aero's reader opts) resolves #inst / #uuid.
|
;; or binds *data-readers* (e.g. aero's reader opts) resolves #inst / #uuid.
|
||||||
|
|
|
||||||
|
|
@ -80,13 +80,18 @@
|
||||||
;; 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 (guard (e (#t #f))
|
(let ((c (jolt-foreign-proc-safe "chmod" '(string int) 'int)))
|
||||||
(load-shared-object #f)
|
|
||||||
(foreign-procedure "chmod" (string int) int))))
|
|
||||||
(lambda (path)
|
(lambda (path)
|
||||||
(if c
|
(cond
|
||||||
(c path #o755)
|
(c (c path #o755))
|
||||||
(system (string-append "chmod 755 '" path "'"))))))
|
;; Windows has no chmod and needs none (execute is by extension)
|
||||||
|
((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
|
||||||
|
|
@ -240,14 +245,13 @@
|
||||||
(else (loop (- i 1))))))
|
(else (loop (- i 1))))))
|
||||||
(else #f))))
|
(else #f))))
|
||||||
|
|
||||||
(define %io-rmd record-method-dispatch)
|
(register-method-arm! 41
|
||||||
(set! record-method-dispatch
|
|
||||||
(lambda (obj method-name rest-args)
|
(lambda (obj method-name rest-args)
|
||||||
(if (jfile? obj)
|
(if (jfile? obj)
|
||||||
(let* ((rest (if (jolt-nil? rest-args) '() (seq->list rest-args)))
|
(let* ((rest (if (jolt-nil? rest-args) '() (seq->list rest-args)))
|
||||||
(r (jfile-method obj method-name rest)))
|
(r (jfile-method obj method-name rest)))
|
||||||
(if r (car r) (error #f "no File method" method-name)))
|
(if r (car r) (error #f "no File method" method-name)))
|
||||||
(%io-rmd obj method-name rest-args))))
|
'pass)))
|
||||||
|
|
||||||
;; .isDirectory / .listFiles emit to jolt-host-call (rt.ss), not record-method-
|
;; .isDirectory / .listFiles emit to jolt-host-call (rt.ss), not record-method-
|
||||||
;; dispatch — the shims there assume a path STRING target. Make them jfile-aware
|
;; dispatch — the shims there assume a path STRING target. Make them jfile-aware
|
||||||
|
|
@ -412,6 +416,11 @@
|
||||||
;; method (a no-op for in-memory streams); absent method -> no-op.
|
;; method (a no-op for in-memory streams); absent method -> no-op.
|
||||||
((htable? x) (guard (e (#t jolt-nil)) (record-method-dispatch x "close" jolt-nil)) jolt-nil)
|
((htable? x) (guard (e (#t jolt-nil)) (record-method-dispatch x "close" jolt-nil)) jolt-nil)
|
||||||
((jfile? x) jolt-nil)
|
((jfile? x) jolt-nil)
|
||||||
|
;; a deftype/defrecord that implements a `close` method (java.io.Closeable /
|
||||||
|
;; AutoCloseable, e.g. tools.reader's reader types) closes through it — the
|
||||||
|
;; same method (.close x) would dispatch to.
|
||||||
|
((and (jrec? x) (jrec-cl x "close"))
|
||||||
|
(record-method-dispatch x "close" jolt-nil) jolt-nil)
|
||||||
(else
|
(else
|
||||||
(let ((closef (jolt-get x (keyword #f "close") jolt-nil)))
|
(let ((closef (jolt-get x (keyword #f "close") jolt-nil)))
|
||||||
(if (and (not (jolt-nil? closef)) (procedure? closef))
|
(if (and (not (jolt-nil? closef)) (procedure? closef))
|
||||||
|
|
@ -620,6 +629,31 @@
|
||||||
;; (Long. n) / (Long. "n"): a Long is just jolt's integer; return it (parse a string).
|
;; (Long. n) / (Long. "n"): a Long is just jolt's integer; return it (parse a string).
|
||||||
(register-class-ctor! "Long" (lambda (x) (if (string? x) (parse-int-or-throw x 10 "Long") (->num (jnum->exact x)))))
|
(register-class-ctor! "Long" (lambda (x) (if (string? x) (parse-int-or-throw x 10 "Long") (->num (jnum->exact x)))))
|
||||||
(register-class-ctor! "java.lang.Long" (lambda (x) (if (string? x) (parse-int-or-throw x 10 "Long") (->num (jnum->exact x)))))
|
(register-class-ctor! "java.lang.Long" (lambda (x) (if (string? x) (parse-int-or-throw x 10 "Long") (->num (jnum->exact x)))))
|
||||||
|
;; (Integer. n) / (Integer. "n"): jolt's integer, range-checked like intCast.
|
||||||
|
(define (integer-ctor x)
|
||||||
|
(jolt-int-cast (if (string? x) (parse-int-or-throw x 10 "Integer") x)))
|
||||||
|
(register-class-ctor! "Integer" integer-ctor)
|
||||||
|
(register-class-ctor! "java.lang.Integer" integer-ctor)
|
||||||
|
;; (Double. x) / (Double. "x"): jolt's double.
|
||||||
|
(define (double-ctor x)
|
||||||
|
(if (string? x)
|
||||||
|
(let ((n (string->number x)))
|
||||||
|
(if n (exact->inexact n)
|
||||||
|
(jolt-throw (jolt-host-throwable "java.lang.NumberFormatException"
|
||||||
|
(string-append "For input string: \"" x "\"")))))
|
||||||
|
(jolt-double x)))
|
||||||
|
(register-class-ctor! "Double" double-ctor)
|
||||||
|
(register-class-ctor! "java.lang.Double" double-ctor)
|
||||||
|
|
||||||
|
;; (Boolean. "true") / (Boolean. b): true for the string "true" (case-insensitive,
|
||||||
|
;; anything else false) or the boolean itself — Boolean.valueOf semantics; the
|
||||||
|
;; box is jolt's plain boolean.
|
||||||
|
(define (boolean-ctor x)
|
||||||
|
(cond ((string? x) (string-ci=? x "true"))
|
||||||
|
((boolean? x) x)
|
||||||
|
(else #f)))
|
||||||
|
(register-class-ctor! "Boolean" boolean-ctor)
|
||||||
|
(register-class-ctor! "java.lang.Boolean" boolean-ctor)
|
||||||
|
|
||||||
;; --- java.net.URI -----------------------------------------------------------
|
;; --- java.net.URI -----------------------------------------------------------
|
||||||
;; A minimal RFC-3986 split into scheme/authority/host/port/path/query/fragment,
|
;; A minimal RFC-3986 split into scheme/authority/host/port/path/query/fragment,
|
||||||
|
|
|
||||||
|
|
@ -85,10 +85,8 @@
|
||||||
(define (na-bytes x) (if (and (jolt-array? x) (eq? (jolt-array-kind x) 'byte)) x (na-byte-array x)))
|
(define (na-bytes x) (if (and (jolt-array? x) (eq? (jolt-array-kind x) 'byte)) x (na-byte-array x)))
|
||||||
(define (na-bytes? x) (and (jolt-array? x) (eq? (jolt-array-kind x) 'byte)))
|
(define (na-bytes? x) (and (jolt-array? x) (eq? (jolt-array-kind x) 'byte)))
|
||||||
(define (na-identity x) x)
|
(define (na-identity x) x)
|
||||||
(define (na-byte x)
|
(define (na-byte x) (jolt-byte-cast x))
|
||||||
(let ((b (bitwise-and (exact (floor x)) #xff))) (if (>= b 128) (- b 256) b)))
|
(define (na-short x) (jolt-short-cast x))
|
||||||
(define (na-short x)
|
|
||||||
(let ((s (bitwise-and (exact (floor x)) #xffff))) (if (>= s #x8000) (- s #x10000) s)))
|
|
||||||
|
|
||||||
;; --- chunked seqs -----------------------------------------------------------
|
;; --- chunked seqs -----------------------------------------------------------
|
||||||
;; The chunked-seq accessors (chunked-seq? / chunk-first / chunk-rest / chunk-next)
|
;; The chunked-seq accessors (chunked-seq? / chunk-first / chunk-rest / chunk-next)
|
||||||
|
|
@ -117,10 +115,11 @@
|
||||||
(let ((v (jolt-array-vec c)) (j (exact (na-idx i))))
|
(let ((v (jolt-array-vec c)) (j (exact (na-idx i))))
|
||||||
(if (and (>= j 0) (< j (vector-length v))) (vector-ref v j) d))
|
(if (and (>= j 0) (< j (vector-length v))) (vector-ref v j) d))
|
||||||
(%na-nth c i d)))))
|
(%na-nth c i d)))))
|
||||||
|
(def-var! "jolt.host" "array-value?" (lambda (x) (if (jolt-array? x) #t jolt-nil)))
|
||||||
(define %na-get jolt-get)
|
(define %na-get jolt-get)
|
||||||
(set! jolt-get
|
(set! jolt-get
|
||||||
(case-lambda
|
(case-lambda
|
||||||
((c k) (if (jolt-array? c) (jolt-nth c k) (%na-get c k)))
|
((c k) (if (jolt-array? c) (jolt-nth c k jolt-nil) (%na-get c k)))
|
||||||
((c k d) (if (jolt-array? c) (jolt-nth c k d) (%na-get c k d)))))
|
((c k d) (if (jolt-array? c) (jolt-nth c k d) (%na-get c k d)))))
|
||||||
;; aset (overlay) writes through jolt.host/ref-put! — mutate the slot, return arr.
|
;; aset (overlay) writes through jolt.host/ref-put! — mutate the slot, return arr.
|
||||||
;; count/nth/seq/get above are NATIVE-OPS (inlined at call sites), so aget/alength/
|
;; count/nth/seq/get above are NATIVE-OPS (inlined at call sites), so aget/alength/
|
||||||
|
|
|
||||||
|
|
@ -10,47 +10,12 @@
|
||||||
(define (ex-info-class v)
|
(define (ex-info-class v)
|
||||||
(let ((c (jolt-get v jolt-kw-class jolt-nil)))
|
(let ((c (jolt-get v jolt-kw-class jolt-nil)))
|
||||||
(if (string? c) c "clojure.lang.ExceptionInfo")))
|
(if (string? c) c "clojure.lang.ExceptionInfo")))
|
||||||
;; immediate-parent chain of the JVM exception hierarchy (simple names). Drives
|
;; Is `wanted` (simple name) `cls` or a supertype of it? The exception hierarchy
|
||||||
;; instance? across exception supertypes — (instance? Throwable (ex-info …)) etc.
|
;; lives in the one class graph (class-hierarchy.ss) — resolve the simple name to
|
||||||
(define exception-parent
|
;; its graph key and ask jch-isa?, so exceptions and every other class share a
|
||||||
'(("ExceptionInfo" . "RuntimeException")
|
;; single source of truth (ExceptionInfo -> IExceptionInfo is a graph edge).
|
||||||
("RuntimeException" . "Exception")
|
|
||||||
("IllegalArgumentException" . "RuntimeException")
|
|
||||||
("ArityException" . "IllegalArgumentException")
|
|
||||||
("NumberFormatException" . "IllegalArgumentException")
|
|
||||||
("IllegalStateException" . "RuntimeException")
|
|
||||||
("UnsupportedOperationException" . "RuntimeException")
|
|
||||||
("ArithmeticException" . "RuntimeException")
|
|
||||||
("NullPointerException" . "RuntimeException")
|
|
||||||
("ClassCastException" . "RuntimeException")
|
|
||||||
("IndexOutOfBoundsException" . "RuntimeException")
|
|
||||||
("ConcurrentModificationException" . "RuntimeException")
|
|
||||||
("NoSuchElementException" . "RuntimeException")
|
|
||||||
("UncheckedIOException" . "RuntimeException")
|
|
||||||
("DateTimeException" . "RuntimeException")
|
|
||||||
("DateTimeParseException" . "DateTimeException")
|
|
||||||
("InterruptedException" . "Exception")
|
|
||||||
("IOException" . "Exception")
|
|
||||||
("FileNotFoundException" . "IOException")
|
|
||||||
("UnsupportedEncodingException" . "IOException")
|
|
||||||
("UnknownHostException" . "IOException")
|
|
||||||
("SocketException" . "IOException")
|
|
||||||
("ConnectException" . "IOException")
|
|
||||||
("SocketTimeoutException" . "IOException")
|
|
||||||
("MalformedURLException" . "IOException")
|
|
||||||
("SSLException" . "IOException")
|
|
||||||
("Exception" . "Throwable")
|
|
||||||
("Error" . "Throwable")
|
|
||||||
("AssertionError" . "Error")
|
|
||||||
("Throwable" . "Object")))
|
|
||||||
;; Is `wanted` (simple name) `cls` or a supertype of it? ExceptionInfo also
|
|
||||||
;; implements the IExceptionInfo interface.
|
|
||||||
(define (exception-isa? cls wanted)
|
(define (exception-isa? cls wanted)
|
||||||
(let loop ((c cls))
|
(jch-isa? (jch-fqn-of-simple cls) wanted))
|
||||||
(cond ((not c) #f)
|
|
||||||
((string=? c wanted) #t)
|
|
||||||
((and (string=? c "ExceptionInfo") (string=? wanted "IExceptionInfo")) #t)
|
|
||||||
(else (let ((p (assoc c exception-parent))) (loop (and p (cdr p))))))))
|
|
||||||
|
|
||||||
;; A raw Chez condition (an arity or non-seqable error Chez itself raised, not a
|
;; A raw Chez condition (an arity or non-seqable error Chez itself raised, not a
|
||||||
;; jolt ex-info) carries no jolt exception class. Map the ones Clojure raises a
|
;; jolt ex-info) carries no jolt exception class. Map the ones Clojure raises a
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,20 @@ 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.
|
||||||
|
|
@ -73,4 +87,36 @@ if [ "$got" != "$want" ]; then
|
||||||
echo "--- got ----"; echo "$got"
|
echo "--- got ----"; echo "$got"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "joltc self-build smoke: passed (joltc runs + builds a working app with no external toolchain)"
|
|
||||||
|
# 5. Static native linking through the distributed joltc: it bundles the Chez
|
||||||
|
# kernel, so with the system cc (but still no external Chez) it re-links a stub
|
||||||
|
# that bakes a :jolt/native :static archive into the app. The app then calls the
|
||||||
|
# C function with the archive removed from disk. Uses the normal PATH so cc — and
|
||||||
|
# the kernel's link deps (lz4/…) — are found, but Chez stays out of the build.
|
||||||
|
napp="$(mktemp -d)/native-app"
|
||||||
|
mkdir -p "$napp/src/app"
|
||||||
|
printf 'int jolt_static_answer(void){return 42;}\n' > "$napp/greet.c"
|
||||||
|
cc -c "$napp/greet.c" -o "$napp/greet.o" && ar rcs "$napp/libgreet.a" "$napp/greet.o"
|
||||||
|
cat > "$napp/src/app/core.clj" <<'EOF'
|
||||||
|
(ns app.core (:require [jolt.ffi :as ffi]))
|
||||||
|
(ffi/defcfn answer "jolt_static_answer" [] :int)
|
||||||
|
(defn -main [& _] (println "answer:" (answer)))
|
||||||
|
EOF
|
||||||
|
cat > "$napp/deps.edn" <<EOF
|
||||||
|
{:paths ["src"]
|
||||||
|
:jolt/native [{:name "greet" :static {:archive "$napp/libgreet.a"}}]}
|
||||||
|
EOF
|
||||||
|
nout="$napp/app"
|
||||||
|
echo "joltc self-build smoke: static-linking a native lib via the binary (no external Chez)"
|
||||||
|
if ! JOLT_PWD="$napp" "$joltc" build -m app.core -o "$nout" >/dev/null 2>&1; then
|
||||||
|
echo " FAIL: static native build via distributed joltc exited non-zero"
|
||||||
|
rm -rf "$(dirname "$napp")"; exit 1
|
||||||
|
fi
|
||||||
|
rm -f "$napp/libgreet.a" "$napp/greet.o" # nothing to load at runtime
|
||||||
|
got_n="$(cd / && "$nout" 2>&1)"
|
||||||
|
rm -rf "$(dirname "$napp")"
|
||||||
|
if [ "$got_n" != "answer: 42" ]; then
|
||||||
|
echo " FAIL: static-linked app (via distributed joltc) output mismatch"
|
||||||
|
echo "--- got ----"; echo "$got_n"; exit 1
|
||||||
|
fi
|
||||||
|
echo "joltc self-build smoke: passed (joltc runs + builds a working app with no external toolchain, incl. static native linking)"
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,23 @@
|
||||||
((and (pmap? x) (eq? (jolt-get x rdr-kw-jolt-type) rdr-kw-jolt-tagged))
|
((and (pmap? x) (eq? (jolt-get x rdr-kw-jolt-type) rdr-kw-jolt-tagged))
|
||||||
(let ((rdr (data-reader-symbol (jolt-get x rdr-kw-tag)))
|
(let ((rdr (data-reader-symbol (jolt-get x rdr-kw-tag)))
|
||||||
(inner (ldr-apply-readers (jolt-get x rdr-kw-form))))
|
(inner (ldr-apply-readers (jolt-get x rdr-kw-form))))
|
||||||
(cond (rdr (jolt-list rdr (jolt-list (jolt-symbol #f "quote") inner)))
|
(cond
|
||||||
|
(rdr
|
||||||
|
;; Clojure applies a data reader at read time and substitutes its result
|
||||||
|
;; as code. A reader that returns a FORM (a list — e.g. borkdude.html's
|
||||||
|
;; #html expands to (->Html (str …))) must be compiled, so splice it in.
|
||||||
|
;; A reader that returns a VALUE (time-literals #time/date -> a Date) is
|
||||||
|
;; left as a runtime call (reader-fn 'inner): the value rebuilds at
|
||||||
|
;; startup, which also keeps a non-serializable constant out of an AOT
|
||||||
|
;; build. Apply is guarded — a reader that can't run at load time (its
|
||||||
|
;; deps not ready) falls back to the runtime call too.
|
||||||
|
(let ((result (and (symbol-t? rdr) (not (jolt-nil? (symbol-t-ns rdr)))
|
||||||
|
(guard (e (#t #f))
|
||||||
|
(let ((fn (var-deref (symbol-t-ns rdr) (symbol-t-name rdr))))
|
||||||
|
(and (procedure? fn) (jolt-invoke fn inner)))))))
|
||||||
|
(if (cseq? result)
|
||||||
|
result
|
||||||
|
(jolt-list rdr (jolt-list (jolt-symbol #f "quote") inner)))))
|
||||||
((eq? inner (jolt-get x rdr-kw-form)) x)
|
((eq? inner (jolt-get x rdr-kw-form)) x)
|
||||||
(else (rdr-make-tagged (jolt-get x rdr-kw-tag) inner)))))
|
(else (rdr-make-tagged (jolt-get x rdr-kw-tag) inner)))))
|
||||||
((rdr-set-form? x)
|
((rdr-set-form? x)
|
||||||
|
|
@ -368,3 +384,14 @@
|
||||||
(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")))
|
||||||
|
|
|
||||||
|
|
@ -61,26 +61,36 @@
|
||||||
|
|
||||||
(define (jolt-defmulti-setup name-sym dispatch . opts)
|
(define (jolt-defmulti-setup name-sym dispatch . opts)
|
||||||
(let-values (((dk h) (parse-mm-opts opts)))
|
(let-values (((dk h) (parse-mm-opts opts)))
|
||||||
(let ((mf (make-jolt-multifn (symbol-t-name name-sym) dispatch
|
(let* ((sns (symbol-t-ns name-sym))
|
||||||
|
;; the macro qualifies the name with its EXPANSION ns, so a defmulti
|
||||||
|
;; deferred inside a fn (a deftest body) still defines in the ns it
|
||||||
|
;; was written in, not whatever ns is current when it finally runs.
|
||||||
|
(ns (if (string? sns) sns (chez-current-ns)))
|
||||||
|
(mf (make-jolt-multifn (symbol-t-name name-sym) dispatch
|
||||||
(new-mm-table) dk h (new-mm-table))))
|
(new-mm-table) dk h (new-mm-table))))
|
||||||
(def-var! (chez-current-ns) (symbol-t-name name-sym) mf)
|
(def-var! ns (symbol-t-name name-sym) mf)
|
||||||
mf)))
|
mf)))
|
||||||
|
|
||||||
;; (defmethod-setup 'mm dispatch-val impl) — add a method. Auto-creates the multifn
|
;; (defmethod-setup 'mm dispatch-val impl) — add a method. Auto-creates the multifn
|
||||||
;; if absent (defmethod before defmulti — rare; identity dispatch as a fallback).
|
;; if absent (defmethod before defmulti — rare; identity dispatch as a fallback).
|
||||||
(define (jolt-defmethod-setup mm-sym dval impl)
|
(define (jolt-defmethod-setup mm-sym dval impl . rest)
|
||||||
(let* ((nm (symbol-t-name mm-sym))
|
(let* ((nm (symbol-t-name mm-sym))
|
||||||
(sns (symbol-t-ns mm-sym))
|
(sns (symbol-t-ns mm-sym))
|
||||||
(qns (and sns (not (jolt-nil? sns)) (not (null? sns)) sns))
|
(qns (and sns (not (jolt-nil? sns)) (not (null? sns)) sns))
|
||||||
|
;; the macro passes its EXPANSION ns so a defmethod deferred inside a
|
||||||
|
;; fn resolves like the JVM (against the ns it was written in, not the
|
||||||
|
;; ns current when it runs); absent (old emitted code) fall back to the
|
||||||
|
;; runtime ns.
|
||||||
|
(here (if (and (pair? rest) (string? (car rest))) (car rest) (chez-current-ns)))
|
||||||
;; qualified (cf.mm/ext) resolves in its own ns (cross-ns defmethod);
|
;; qualified (cf.mm/ext) resolves in its own ns (cross-ns defmethod);
|
||||||
;; unqualified resolves in the current ns, else a :refer's home ns (so a
|
;; unqualified resolves in the writing ns, else a :refer's home ns (so a
|
||||||
;; defmethod on a referred multifn lands on the real one), else stays in
|
;; defmethod on a referred multifn lands on the real one), else stays in
|
||||||
;; the current ns (a shadow, as before).
|
;; the writing ns (a shadow, as before).
|
||||||
(mns (cond
|
(mns (cond
|
||||||
(qns (or (chez-resolve-alias (chez-current-ns) qns) qns))
|
(qns (or (chez-resolve-alias here qns) qns))
|
||||||
((var-cell-lookup (chez-current-ns) nm) (chez-current-ns))
|
((var-cell-lookup here nm) here)
|
||||||
((chez-resolve-refer (chez-current-ns) nm) => values)
|
((chez-resolve-refer here nm) => values)
|
||||||
(else (chez-current-ns))))
|
(else here)))
|
||||||
(cur (var-deref mns nm))
|
(cur (var-deref mns nm))
|
||||||
(mf (if (jolt-multifn? cur) cur
|
(mf (if (jolt-multifn? cur) cur
|
||||||
;; auto-create: copy the dispatch fn + default from a same-named
|
;; auto-create: copy the dispatch fn + default from a same-named
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@
|
||||||
(define (jolt-array-map . kvs) (jolt-array-map-build kvs))
|
(define (jolt-array-map . kvs) (jolt-array-map-build kvs))
|
||||||
(define (jolt-hash-map-fn . kvs) (jolt-hash-map-build kvs))
|
(define (jolt-hash-map-fn . kvs) (jolt-hash-map-build kvs))
|
||||||
|
|
||||||
;; set: realize any seqable to a list, then dedup through the set ctor. nil -> #{}.
|
;; set lives in the kernel overlay tier (clojure/core/00-kernel.clj): it's a pure
|
||||||
(define (jolt-set coll)
|
;; composition (apply hash-set (seq coll)) the compiler uses only off the emit path,
|
||||||
(if (jolt-nil? coll) (jolt-hash-set) (apply jolt-hash-set (seq->list coll))))
|
;; so the Clojure version lowers to the same code without a bootstrap cycle.
|
||||||
|
|
||||||
;; rand: a flonum in [0, n) (n defaults to 1.0) — jolt is all-flonum, so the
|
;; rand: a flonum in [0, n) (n defaults to 1.0) — jolt is all-flonum, so the
|
||||||
;; result is a double like every other number.
|
;; result is a double like every other number.
|
||||||
|
|
@ -24,6 +24,5 @@
|
||||||
(def-var! "clojure.core" "hash-map" jolt-hash-map-fn)
|
(def-var! "clojure.core" "hash-map" jolt-hash-map-fn)
|
||||||
(def-var! "clojure.core" "hash-set" jolt-hash-set)
|
(def-var! "clojure.core" "hash-set" jolt-hash-set)
|
||||||
(def-var! "clojure.core" "array-map" jolt-array-map)
|
(def-var! "clojure.core" "array-map" jolt-array-map)
|
||||||
(def-var! "clojure.core" "set" jolt-set)
|
|
||||||
(def-var! "clojure.core" "rand" jolt-rand)
|
(def-var! "clojure.core" "rand" jolt-rand)
|
||||||
(def-var! "clojure.core" "map-entry?" jolt-map-entry?)
|
(def-var! "clojure.core" "map-entry?" jolt-map-entry?)
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,9 @@
|
||||||
;; so dispatch to its meta method rather than the identity side-table — which
|
;; so dispatch to its meta method rather than the identity side-table — which
|
||||||
;; the deftype's reconstructed instances would not share.
|
;; the deftype's reconstructed instances would not share.
|
||||||
((and (jrec? x) (jrec-cl x "meta")) => (lambda (m) (jolt-invoke m x)))
|
((and (jrec? x) (jrec-cl x "meta")) => (lambda (m) (jolt-invoke m x)))
|
||||||
((or (pvec? x) (pmap? x) (pset? x) (cseq? x) (empty-list-t? x) (jolt-lazyseq? x) (jrec? x) (jreify? x) (procedure? x))
|
;; everything else (collections, fns, reify, atoms/agents and any reference
|
||||||
(hashtable-ref meta-table x jolt-nil))
|
;; type) reads the identity side-table; a value with no entry is nil meta.
|
||||||
(else jolt-nil)))
|
(else (hashtable-ref meta-table x jolt-nil))))
|
||||||
|
|
||||||
;; fresh-identity copy of a metadatable value (so attaching meta doesn't mutate
|
;; fresh-identity copy of a metadatable value (so attaching meta doesn't mutate
|
||||||
;; the original). cseq/procedure can't be copied meaningfully — keyed in place.
|
;; the original). cseq/procedure can't be copied meaningfully — keyed in place.
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,12 @@
|
||||||
jolt-nil)
|
jolt-nil)
|
||||||
|
|
||||||
;; --- reader-conditional: a tagged map (reader-conditional? is an overlay
|
;; --- reader-conditional: a tagged map (reader-conditional? is an overlay
|
||||||
;; tagged-value predicate that reads :jolt/type). re-matcher / re-find / re-groups
|
;; tagged-value predicate that reads :jolt/type). STAYS NATIVE: building a
|
||||||
;; are the stateful matcher API in regex.ss.
|
;; :jolt/type-tagged map is part of the native value model — an overlay defn
|
||||||
|
;; returning {:jolt/type ...} silently fails to bind during the seed mint (the
|
||||||
|
;; guard around each prelude form swallows the load-time error), the same reason
|
||||||
|
;; every other tagged-value constructor (atom/volatile!/tagged-literal) is native.
|
||||||
|
;; re-matcher / re-find / re-groups are the stateful matcher API in regex.ss.
|
||||||
(define nr-kw-type (keyword "jolt" "type"))
|
(define nr-kw-type (keyword "jolt" "type"))
|
||||||
(define nr-kw-rc (keyword "jolt" "reader-conditional"))
|
(define nr-kw-rc (keyword "jolt" "reader-conditional"))
|
||||||
(define nr-kw-form (keyword #f "form"))
|
(define nr-kw-form (keyword #f "form"))
|
||||||
|
|
|
||||||
|
|
@ -25,18 +25,11 @@
|
||||||
(def-var! "clojure.core" "volatile!" jolt-volatile!)
|
(def-var! "clojure.core" "volatile!" jolt-volatile!)
|
||||||
(def-var! "clojure.core" "deref" jolt-deref)
|
(def-var! "clojure.core" "deref" jolt-deref)
|
||||||
|
|
||||||
;; --- transduce / sequence ----------------------------------------------------
|
;; --- sequence ----------------------------------------------------------------
|
||||||
;; (transduce xform f coll) / (transduce xform f init coll): build the transformed
|
;; transduce lives in the overlay (clojure/core/22-coll.clj): it's a pure
|
||||||
;; reducing fn (xform f), reduce it over coll (reduce-seq honors `reduced`), then
|
;; composition (xf (reduce xf init coll)) over reduce, so the Clojure version
|
||||||
;; run the completion (1-arg) arity. The 3-arg init defaults to (f) — the rf's
|
;; lowers to the same code the native shim did. sequence stays native (below):
|
||||||
;; 0-arity, e.g. (+) = 0, (conj) = [].
|
;; its transformer iterator drives the reduced box + lazy realization directly.
|
||||||
(define jolt-transduce
|
|
||||||
(case-lambda
|
|
||||||
((xform f coll) (jolt-transduce xform f (jolt-invoke f) coll))
|
|
||||||
((xform f init coll)
|
|
||||||
(let* ((xf (jolt-invoke xform f))
|
|
||||||
(res (reduce-seq xf init (jolt-seq coll))))
|
|
||||||
(jolt-invoke xf res)))))
|
|
||||||
|
|
||||||
;; (sequence coll) -> a seq; (sequence xform coll) -> a LAZY seq of coll transformed
|
;; (sequence coll) -> a seq; (sequence xform coll) -> a LAZY seq of coll transformed
|
||||||
;; by xform. A transformer iterator (mirrors clojure.core's TransformerIterator):
|
;; by xform. A transformer iterator (mirrors clojure.core's TransformerIterator):
|
||||||
|
|
@ -87,7 +80,6 @@
|
||||||
((coll) (jolt-seq coll))
|
((coll) (jolt-seq coll))
|
||||||
((xform coll) (sequence-xf xform coll))))
|
((xform coll) (sequence-xf xform coll))))
|
||||||
|
|
||||||
(def-var! "clojure.core" "transduce" jolt-transduce)
|
|
||||||
(def-var! "clojure.core" "sequence" jolt-sequence)
|
(def-var! "clojure.core" "sequence" jolt-sequence)
|
||||||
|
|
||||||
;; --- cat ---------------------------------------------------------------------
|
;; --- cat ---------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -260,6 +260,9 @@
|
||||||
;; intern: create/set a var ns/sym to val (or an unbound cell). Returns the var.
|
;; intern: create/set a var ns/sym to val (or an unbound cell). Returns the var.
|
||||||
(define (jolt-intern ns-desig sym . vopt)
|
(define (jolt-intern ns-desig sym . vopt)
|
||||||
(let ((nm (ns-desig->name ns-desig)) (s (symbol-t-name sym)))
|
(let ((nm (ns-desig->name ns-desig)) (s (symbol-t-name sym)))
|
||||||
|
;; the namespace must exist (Namespace.find), like the JVM's intern
|
||||||
|
(unless (hashtable-ref ns-registry nm #f)
|
||||||
|
(jolt-throw (jolt-ex-info (string-append "No namespace: " nm " found") empty-pmap)))
|
||||||
(if (pair? vopt) (def-var! nm s (car vopt)) (declare-var! nm s))))
|
(if (pair? vopt) (def-var! nm s (car vopt)) (declare-var! nm s))))
|
||||||
|
|
||||||
;; alias / ns-unalias: register/drop an :as alias under the current (or given) ns.
|
;; alias / ns-unalias: register/drop an :as alias under the current (or given) ns.
|
||||||
|
|
@ -306,13 +309,24 @@
|
||||||
(loop (cddr a)))))
|
(loop (cddr a)))))
|
||||||
jolt-nil)
|
jolt-nil)
|
||||||
|
|
||||||
;; alter-meta! / reset-meta!: update a var's metadata (var-meta-table, rt.ss).
|
;; alter-meta! / reset-meta!: a var's metadata lives in var-meta-table (rt.ss);
|
||||||
|
;; any other reference (atom/agent/namespace) uses the identity meta side-table
|
||||||
|
;; jolt-meta reads.
|
||||||
(define (jolt-alter-meta! ref f . args)
|
(define (jolt-alter-meta! ref f . args)
|
||||||
|
(if (var-cell? ref)
|
||||||
(let* ((cur (or (hashtable-ref var-meta-table ref #f) (jolt-hash-map)))
|
(let* ((cur (or (hashtable-ref var-meta-table ref #f) (jolt-hash-map)))
|
||||||
(new (apply jolt-invoke f cur args)))
|
(new (apply jolt-invoke f cur args)))
|
||||||
(hashtable-set! var-meta-table ref new)
|
(hashtable-set! var-meta-table ref new)
|
||||||
new))
|
new)
|
||||||
(define (jolt-reset-meta! ref m) (hashtable-set! var-meta-table ref m) m)
|
(let* ((cur (let ((m (jolt-meta ref))) (if (jolt-nil? m) (jolt-hash-map) m)))
|
||||||
|
(new (apply jolt-invoke f cur args)))
|
||||||
|
(hashtable-set! meta-table ref new)
|
||||||
|
new)))
|
||||||
|
(define (jolt-reset-meta! ref m)
|
||||||
|
(if (var-cell? ref)
|
||||||
|
(hashtable-set! var-meta-table ref m)
|
||||||
|
(hashtable-set! meta-table ref m))
|
||||||
|
m)
|
||||||
|
|
||||||
;; --- RESOLVE FRICTION: native-op cells -------------------------------------
|
;; --- RESOLVE FRICTION: native-op cells -------------------------------------
|
||||||
;; Native-op primitives (+ map reduce …) are INLINED at emit, so they have no
|
;; Native-op primitives (+ map reduce …) are INLINED at emit, so they have no
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,17 @@
|
||||||
;; a lazy-seq carries its own realized? flag (lazy-bridge.ss). The overlay
|
;; a lazy-seq carries its own realized? flag (lazy-bridge.ss). The overlay
|
||||||
;; realized? reads :jolt/type and throws on a jolt-lazyseq record.
|
;; realized? reads :jolt/type and throws on a jolt-lazyseq record.
|
||||||
((jolt-lazyseq? x) (jolt-lazyseq-realized? x))
|
((jolt-lazyseq? x) (jolt-lazyseq-realized? x))
|
||||||
|
;; a seq cell answers by its forced flag: the rest of a realized lazy
|
||||||
|
;; chain is a cseq under jolt's seq model, and (realized? (rest s)) after
|
||||||
|
;; a next must be true like the JVM's realized LazySeq — never a throw
|
||||||
|
;; whose message renders the (possibly infinite) seq.
|
||||||
|
;; a PLAIN seq (list/cons/range — not a lazy-seq wrapper) is not an
|
||||||
|
;; IPending on the JVM: realized? throws.
|
||||||
|
((or (cseq? x) (empty-list-t? x))
|
||||||
|
(jolt-throw (jolt-host-throwable
|
||||||
|
"java.lang.ClassCastException"
|
||||||
|
(string-append "class " (guard (e (#t "?")) (jolt-class-name x))
|
||||||
|
" cannot be cast to class clojure.lang.IPending"))))
|
||||||
(else (jolt-invoke overlay-realized? x))))))
|
(else (jolt-invoke overlay-realized? x))))))
|
||||||
;; clojure.edn/read over a reader: drain the jhost reader, then read through the
|
;; clojure.edn/read over a reader: drain the jhost reader, then read through the
|
||||||
;; overlay read-string so the opts map (:readers/:default/:eof) is honored.
|
;; overlay read-string so the opts map (:readers/:default/:eof) is honored.
|
||||||
|
|
@ -78,18 +89,19 @@
|
||||||
(def-var! "clojure.core" "line-seq"
|
(def-var! "clojure.core" "line-seq"
|
||||||
(lambda (rdr)
|
(lambda (rdr)
|
||||||
(if (reader-jhost? rdr) (chez-line-seq rdr) (jolt-invoke overlay-line-seq rdr)))))
|
(if (reader-jhost? rdr) (chez-line-seq rdr) (jolt-invoke overlay-line-seq rdr)))))
|
||||||
;; JVM-parity numeric tower: the overlay (20-coll.clj) carries an
|
;; JVM-parity numeric tower. integer?/float? are on the compiler emit/inference
|
||||||
;; all-flonum number-predicate web with no Ratio concept (ratio? -> false,
|
;; path (so they stay native) but the overlay (20-coll.clj) still carries an
|
||||||
;; double? -> not-integer, float? -> double?, rational? -> int?), which
|
;; all-flonum int?/double? (int? -> integer?, double? -> not-integer) that
|
||||||
;; misclassifies exact rationals on the Chez tower (e.g. (double? 1/2) -> true).
|
;; misclassifies exact rationals (e.g. (double? 1/2) -> true). Re-assert the
|
||||||
;; Re-assert the native tower-correct versions (predicates.ss) so they win over
|
;; native tower-correct versions so they win over those overlay defs. int?/double?
|
||||||
;; the overlay defs. int?/double? alias integer?/float?. == is value-equality.
|
;; alias integer?/float?. == is value-equality. (ratio?/rational? are now correct
|
||||||
|
;; in the overlay, built on jolt.host tower tests, so they need no re-assertion.)
|
||||||
(def-var! "clojure.core" "integer?" jolt-integer?)
|
(def-var! "clojure.core" "integer?" jolt-integer?)
|
||||||
(def-var! "clojure.core" "int?" jolt-integer?)
|
(def-var! "clojure.core" "int?" jolt-integer?)
|
||||||
(def-var! "clojure.core" "float?" jolt-float?)
|
(def-var! "clojure.core" "float?" jolt-float?)
|
||||||
(def-var! "clojure.core" "double?" jolt-float?)
|
(def-var! "clojure.core" "double?" jolt-float?)
|
||||||
(def-var! "clojure.core" "ratio?" jolt-ratio?)
|
;; ratio?/rational? now live (correctly) in the overlay, so they no longer need a
|
||||||
(def-var! "clojure.core" "rational?" jolt-rational?)
|
;; native re-assertion here. decimal? stays (bigdec re-binds it).
|
||||||
(def-var! "clojure.core" "decimal?" jolt-decimal?)
|
(def-var! "clojure.core" "decimal?" jolt-decimal?)
|
||||||
(def-var! "clojure.core" "==" jolt-num-equiv)
|
(def-var! "clojure.core" "==" jolt-num-equiv)
|
||||||
;; chunked-seq? is true for a vector's seq (a real chunked-seq); the overlay's
|
;; chunked-seq? is true for a vector's seq (a real chunked-seq); the overlay's
|
||||||
|
|
@ -111,13 +123,13 @@
|
||||||
((stream)
|
((stream)
|
||||||
(if (reader-jhost? stream)
|
(if (reader-jhost? stream)
|
||||||
(let-values (((form found?) (host-reader-read-form stream)))
|
(let-values (((form found?) (host-reader-read-form stream)))
|
||||||
(if found? form (jolt-throw (jolt-ex-info "EOF while reading" (empty-pmap)))))
|
(if found? form (jolt-throw (jolt-ex-info "EOF while reading" empty-pmap))))
|
||||||
(jolt-invoke ov-read stream)))
|
(jolt-invoke ov-read stream)))
|
||||||
((stream e? ev)
|
((stream e? ev)
|
||||||
(if (reader-jhost? stream)
|
(if (reader-jhost? stream)
|
||||||
(let-values (((form found?) (host-reader-read-form stream)))
|
(let-values (((form found?) (host-reader-read-form stream)))
|
||||||
(cond (found? form)
|
(cond (found? form)
|
||||||
((jolt-truthy? e?) (jolt-throw (jolt-ex-info "EOF while reading" (empty-pmap))))
|
((jolt-truthy? e?) (jolt-throw (jolt-ex-info "EOF while reading" empty-pmap)))
|
||||||
(else ev)))
|
(else ev)))
|
||||||
(jolt-invoke ov-read stream e? ev))))))
|
(jolt-invoke ov-read stream e? ev))))))
|
||||||
(let ((ov-rps (var-deref "clojure.core" "read+string")))
|
(let ((ov-rps (var-deref "clojure.core" "read+string")))
|
||||||
|
|
@ -130,7 +142,7 @@
|
||||||
(let* ((s (drain-reader stream)) (pr (jolt-parse-next s)))
|
(let* ((s (drain-reader stream)) (pr (jolt-parse-next s)))
|
||||||
(if (jolt-nil? pr)
|
(if (jolt-nil? pr)
|
||||||
(begin (reader-refill! stream "")
|
(begin (reader-refill! stream "")
|
||||||
(if (jolt-truthy? e?) (jolt-throw (jolt-ex-info "EOF while reading" (empty-pmap)))
|
(if (jolt-truthy? e?) (jolt-throw (jolt-ex-info "EOF while reading" empty-pmap))
|
||||||
(jolt-vector ev "")))
|
(jolt-vector ev "")))
|
||||||
(let ((rest (jolt-nth pr 1)))
|
(let ((rest (jolt-nth pr 1)))
|
||||||
(reader-refill! stream rest)
|
(reader-refill! stream rest)
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@
|
||||||
(define (jolt-vector? x) (pvec? x))
|
(define (jolt-vector? x) (pvec? x))
|
||||||
(define (jolt-set? x) (pset? x))
|
(define (jolt-set? x) (pset? x))
|
||||||
(define (jolt-seq? x) (or (cseq? x) (empty-list-t? x)))
|
(define (jolt-seq? x) (or (cseq? x) (empty-list-t? x)))
|
||||||
;; (list? x): a list-marked cseq node or the empty list (). A lazy/vector-backed
|
;; list? lives in the overlay (clojure/core/20-coll.clj) — see jolt.host/cseq? etc.
|
||||||
;; seq, (rest list), (seq coll), (map …) are seqs but not lists.
|
|
||||||
(define (jolt-list-pred? x) (or (and (cseq? x) (cseq-list? x)) (empty-list-t? x)))
|
|
||||||
(define (jolt-coll-pred? x)
|
(define (jolt-coll-pred? x)
|
||||||
(or (pvec? x) (pmap? x) (pset? x) (cseq? x) (empty-list-t? x) (jolt-lazyseq? x)))
|
(or (pvec? x) (pmap? x) (pset? x) (cseq? x) (empty-list-t? x) (jolt-lazyseq? x)))
|
||||||
(define (jolt-number? x) (number? x))
|
(define (jolt-number? x) (number? x))
|
||||||
|
|
@ -27,13 +25,18 @@
|
||||||
;; BigDecimal). decimal? is always false (no BigDecimal type).
|
;; BigDecimal). decimal? is always false (no BigDecimal type).
|
||||||
(define (jolt-integer? x) (and (number? x) (exact? x) (integer? x)))
|
(define (jolt-integer? x) (and (number? x) (exact? x) (integer? x)))
|
||||||
(define (jolt-float? x) (and (number? x) (flonum? x)))
|
(define (jolt-float? x) (and (number? x) (flonum? x)))
|
||||||
(define (jolt-ratio? x) (and (number? x) (exact? x) (rational? x) (not (integer? x))))
|
;; ratio?/rational? live in the overlay (clojure/core/20-coll.clj), built on the
|
||||||
(define (jolt-rational? x) (and (number? x) (exact? x)))
|
;; jolt.host tower tests. decimal? stays native: the optional bigdec module
|
||||||
|
;; (java/bigdec.ss) re-binds it to jbigdec?, so it can't be a static overlay const.
|
||||||
(define (jolt-decimal? x) #f)
|
(define (jolt-decimal? x) #f)
|
||||||
(define (jolt-fn? x) (procedure? x))
|
(define (jolt-fn? x) (procedure? x))
|
||||||
(define (jolt-boolean-pred? x) (boolean? x))
|
(define (jolt-boolean-pred? x) (boolean? x))
|
||||||
|
|
||||||
;; (boolean x) coerces truthiness (nil/false -> false, else true).
|
;; (boolean x) coerces truthiness (nil/false -> false, else true). MUST stay native:
|
||||||
|
;; the backend's emit path calls clojure.core/boolean for every :if node
|
||||||
|
;; (backend_scheme.clj bool tracking), so it has to exist before ANY compilation,
|
||||||
|
;; including the kernel overlay tier (whose own fns contain `if`). Migrating it even
|
||||||
|
;; to the kernel tier deadlocks: compiling the tier that defines boolean needs boolean.
|
||||||
(define (jolt-boolean x) (if (jolt-truthy? x) #t #f))
|
(define (jolt-boolean x) (if (jolt-truthy? x) #t #f))
|
||||||
|
|
||||||
;; (name x): keyword/symbol -> name string; string -> itself.
|
;; (name x): keyword/symbol -> name string; string -> itself.
|
||||||
|
|
@ -57,8 +60,6 @@
|
||||||
(def-var! "clojure.core" "char?" jolt-char-pred?)
|
(def-var! "clojure.core" "char?" jolt-char-pred?)
|
||||||
(def-var! "clojure.core" "integer?" jolt-integer?)
|
(def-var! "clojure.core" "integer?" jolt-integer?)
|
||||||
(def-var! "clojure.core" "float?" jolt-float?)
|
(def-var! "clojure.core" "float?" jolt-float?)
|
||||||
(def-var! "clojure.core" "ratio?" jolt-ratio?)
|
|
||||||
(def-var! "clojure.core" "rational?" jolt-rational?)
|
|
||||||
(def-var! "clojure.core" "decimal?" jolt-decimal?)
|
(def-var! "clojure.core" "decimal?" jolt-decimal?)
|
||||||
;; == numeric value-equality (ignores exactness, unlike =): (== 3 3.0) -> true.
|
;; == numeric value-equality (ignores exactness, unlike =): (== 3 3.0) -> true.
|
||||||
;; 1-arity is trivially true; 2+ args must all be numbers (Numbers.equiv throws
|
;; 1-arity is trivially true; 2+ args must all be numbers (Numbers.equiv throws
|
||||||
|
|
@ -80,10 +81,30 @@
|
||||||
(def-var! "clojure.core" "vector?" jolt-vector?)
|
(def-var! "clojure.core" "vector?" jolt-vector?)
|
||||||
(def-var! "clojure.core" "set?" jolt-set?)
|
(def-var! "clojure.core" "set?" jolt-set?)
|
||||||
(def-var! "clojure.core" "seq?" jolt-seq?)
|
(def-var! "clojure.core" "seq?" jolt-seq?)
|
||||||
(def-var! "clojure.core" "list?" jolt-list-pred?)
|
|
||||||
(def-var! "clojure.core" "coll?" jolt-coll-pred?)
|
(def-var! "clojure.core" "coll?" jolt-coll-pred?)
|
||||||
(def-var! "clojure.core" "fn?" jolt-fn?)
|
(def-var! "clojure.core" "fn?" jolt-fn?)
|
||||||
(def-var! "clojure.core" "boolean?" jolt-boolean-pred?)
|
(def-var! "clojure.core" "boolean?" jolt-boolean-pred?)
|
||||||
(def-var! "clojure.core" "boolean" jolt-boolean)
|
(def-var! "clojure.core" "boolean" jolt-boolean)
|
||||||
(def-var! "clojure.core" "name" jolt-name)
|
(def-var! "clojure.core" "name" jolt-name)
|
||||||
(def-var! "clojure.core" "namespace" jolt-namespace)
|
(def-var! "clojure.core" "namespace" jolt-namespace)
|
||||||
|
|
||||||
|
;; --- jolt.host raw type-test primitives -------------------------------------
|
||||||
|
;; Some clojure.core predicates bottom out at host tests overlay Clojure can't
|
||||||
|
;; reach. Expose the ones the migratable predicates need so the overlay versions
|
||||||
|
;; lower to exactly these calls — no perf loss. rational-type? is the Chez TYPE
|
||||||
|
;; test (exact rational), distinct from clojure.core/rational? (which gates on
|
||||||
|
;; number? first). exact? is wrapped TOTAL (Chez's raw exact? errors on a
|
||||||
|
;; non-number); rational-type? already returns #f for a non-match.
|
||||||
|
;;
|
||||||
|
;; Only the tests consumed by the migrated predicates (ratio?/rational? -> exact?,
|
||||||
|
;; rational-type?; list? -> cseq?/cseq-list?/empty-list?) are exposed. The rest of
|
||||||
|
;; the predicate web stays native and is NOT exposed: map?/set?/seq?/coll? are
|
||||||
|
;; extended at runtime with sorted/record/lazy arms, decimal? is extended by the
|
||||||
|
;; optional bigdec module, integer?/float? are on the compiler emit/inference path,
|
||||||
|
;; and vector? is reached by the kernel-tier peek during bootstrap.
|
||||||
|
(define (jh-exact? x) (and (number? x) (exact? x)))
|
||||||
|
(def-var! "jolt.host" "exact?" jh-exact?)
|
||||||
|
(def-var! "jolt.host" "rational-type?" rational?)
|
||||||
|
(def-var! "jolt.host" "cseq?" cseq?)
|
||||||
|
(def-var! "jolt.host" "empty-list?" empty-list-t?)
|
||||||
|
(def-var! "jolt.host" "cseq-list?" cseq-list?)
|
||||||
|
|
|
||||||
|
|
@ -48,12 +48,24 @@
|
||||||
|
|
||||||
(define (rdr-digit? c) (and (char>=? c #\0) (char<=? c #\9)))
|
(define (rdr-digit? c) (and (char>=? c #\0) (char<=? c #\9)))
|
||||||
(define (rdr-octal? c) (and (char>=? c #\0) (char<=? c #\7)))
|
(define (rdr-octal? c) (and (char>=? c #\0) (char<=? c #\7)))
|
||||||
|
(define (rdr-all-digits? s from to)
|
||||||
|
(and (> to from)
|
||||||
|
(let loop ((i from))
|
||||||
|
(cond ((>= i to) #t)
|
||||||
|
((rdr-digit? (string-ref s i)) (loop (+ i 1)))
|
||||||
|
(else #f)))))
|
||||||
;; every char of s in [from,to) is an octal digit (and the span is non-empty).
|
;; every char of s in [from,to) is an octal digit (and the span is non-empty).
|
||||||
(define (rdr-all-octal? s from to)
|
(define (rdr-all-octal? s from to)
|
||||||
(and (fx<? from to)
|
(and (fx<? from to)
|
||||||
(let loop ((i from)) (cond ((fx=? i to) #t) ((rdr-octal? (string-ref s i)) (loop (fx+ i 1))) (else #f)))))
|
(let loop ((i from)) (cond ((fx=? i to) #t) ((rdr-octal? (string-ref s i)) (loop (fx+ i 1))) (else #f)))))
|
||||||
|
|
||||||
;; Advance past whitespace, commas, and ;-to-end-of-line comments.
|
;; Advance past whitespace, commas, and ;-to-end-of-line comments.
|
||||||
|
;; EDN strict mode (clojure.edn): auto-resolved keywords are invalid, and each
|
||||||
|
;; discarded (#_) form is handed to rdr-discard-cb so the edn layer validates
|
||||||
|
;; its tagged elements through :readers/:default like the JVM.
|
||||||
|
(define rdr-edn-mode (make-parameter #f))
|
||||||
|
(define rdr-discard-cb (make-parameter #f))
|
||||||
|
|
||||||
(define (rdr-skip-ws s i end)
|
(define (rdr-skip-ws s i end)
|
||||||
(let loop ((i i))
|
(let loop ((i i))
|
||||||
(cond
|
(cond
|
||||||
|
|
@ -61,7 +73,8 @@
|
||||||
((rdr-ws? (string-ref s i)) (loop (+ i 1)))
|
((rdr-ws? (string-ref s i)) (loop (+ i 1)))
|
||||||
((char=? (string-ref s i) #\;)
|
((char=? (string-ref s i) #\;)
|
||||||
(let eol ((j (+ i 1)))
|
(let eol ((j (+ i 1)))
|
||||||
(if (or (>= j end) (char=? (string-ref s j) #\newline))
|
(if (or (>= j end) (char=? (string-ref s j) #\newline)
|
||||||
|
(char=? (string-ref s j) #\return))
|
||||||
(loop j)
|
(loop j)
|
||||||
(eol (+ j 1)))))
|
(eol (+ j 1)))))
|
||||||
(else i))))
|
(else i))))
|
||||||
|
|
@ -115,12 +128,17 @@
|
||||||
(slash (rdr-string-index-char body #\/)))
|
(slash (rdr-string-index-char body #\/)))
|
||||||
(cond
|
(cond
|
||||||
;; ratio a/b -> exact rational (= JVM Ratio); reduces to an exact integer
|
;; ratio a/b -> exact rational (= JVM Ratio); reduces to an exact integer
|
||||||
;; when d divides n.
|
;; when d divides n. Both parts must be plain digit runs (1/-1 is an
|
||||||
|
;; invalid token); a zero denominator is the JVM's divide error.
|
||||||
(slash
|
(slash
|
||||||
(let ((n (string->number (substring body 0 slash)))
|
(let ((ns (substring body 0 slash))
|
||||||
(d (string->number (substring body (+ slash 1) blen))))
|
(ds (substring body (+ slash 1) blen)))
|
||||||
(and (integer? n) (integer? d) (not (= d 0))
|
(and (rdr-all-digits? ns 0 (string-length ns))
|
||||||
(* sign (/ n d)))))
|
(rdr-all-digits? ds 0 (string-length ds))
|
||||||
|
(let ((n (string->number ns)) (d (string->number ds)))
|
||||||
|
(when (= d 0)
|
||||||
|
(jolt-throw (jolt-host-throwable "java.lang.ArithmeticException" "Divide by zero")))
|
||||||
|
(* sign (/ n d))))))
|
||||||
;; hex 0x..
|
;; hex 0x..
|
||||||
((and (>= blen 2) (char=? (string-ref body 0) #\0)
|
((and (>= blen 2) (char=? (string-ref body 0) #\0)
|
||||||
(or (char=? (string-ref body 1) #\x) (char=? (string-ref body 1) #\X)))
|
(or (char=? (string-ref body 1) #\x) (char=? (string-ref body 1) #\X)))
|
||||||
|
|
@ -139,6 +157,11 @@
|
||||||
;; elsewhere or fall through (a non-octal digit fails rdr-all-octal?).
|
;; elsewhere or fall through (a non-octal digit fails rdr-all-octal?).
|
||||||
((and (>= blen 2) (char=? (string-ref body 0) #\0) (rdr-all-octal? body 1 blen))
|
((and (>= blen 2) (char=? (string-ref body 0) #\0) (rdr-all-octal? body 1 blen))
|
||||||
(let ((o (rdr-parse-radix (substring body 1 blen) 8))) (and o (* sign o))))
|
(let ((o (rdr-parse-radix (substring body 1 blen) 8))) (and o (* sign o))))
|
||||||
|
;; a leading zero on a plain multi-digit integer is invalid (the octal
|
||||||
|
;; branch above accepted real octals; 08/09 match the JVM's trailing
|
||||||
|
;; "invalid number" alternative)
|
||||||
|
((and (>= blen 2) (char=? (string-ref body 0) #\0) (rdr-all-digits? body 1 blen))
|
||||||
|
#f)
|
||||||
;; bigint suffix N
|
;; bigint suffix N
|
||||||
((and (> blen 1) (char=? (string-ref body (- blen 1)) #\N))
|
((and (> blen 1) (char=? (string-ref body (- blen 1)) #\N))
|
||||||
(let ((n (string->number (substring body 0 (- blen 1)))))
|
(let ((n (string->number (substring body 0 (- blen 1)))))
|
||||||
|
|
@ -170,7 +193,7 @@
|
||||||
;; opening quote already consumed; read to the closing quote, processing escapes.
|
;; opening quote already consumed; read to the closing quote, processing escapes.
|
||||||
(define (rdr-read-string-lit s i end)
|
(define (rdr-read-string-lit s i end)
|
||||||
(let loop ((i i) (acc '()))
|
(let loop ((i i) (acc '()))
|
||||||
(when (>= i end) (jolt-throw (jolt-ex-info "EOF while reading string" (empty-pmap))))
|
(when (>= i end) (jolt-throw (jolt-ex-info "EOF while reading string" empty-pmap)))
|
||||||
(let ((c (string-ref s i)))
|
(let ((c (string-ref s i)))
|
||||||
(cond
|
(cond
|
||||||
((char=? c #\") (values (list->string (reverse acc)) (+ i 1)))
|
((char=? c #\") (values (list->string (reverse acc)) (+ i 1)))
|
||||||
|
|
@ -190,7 +213,10 @@
|
||||||
(let oct ((j (+ i 1)) (val 0) (cnt 0))
|
(let oct ((j (+ i 1)) (val 0) (cnt 0))
|
||||||
(if (and (fx<? cnt 3) (fx<? j end) (rdr-octal? (string-ref s j)))
|
(if (and (fx<? cnt 3) (fx<? j end) (rdr-octal? (string-ref s j)))
|
||||||
(oct (fx+ j 1) (fx+ (fx* val 8) (fx- (char->integer (string-ref s j)) 48)) (fx+ cnt 1))
|
(oct (fx+ j 1) (fx+ (fx* val 8) (fx- (char->integer (string-ref s j)) 48)) (fx+ cnt 1))
|
||||||
(loop j (cons (integer->char val) acc)))))
|
(begin
|
||||||
|
(when (> val 255)
|
||||||
|
(jolt-throw (jolt-ex-info "Octal escape sequence must be in range [0, 377]" empty-pmap)))
|
||||||
|
(loop j (cons (integer->char val) acc))))))
|
||||||
((#\u)
|
((#\u)
|
||||||
(let-values (((cp j) (rdr-hex->int s (+ i 2) 4)))
|
(let-values (((cp j) (rdr-hex->int s (+ i 2) 4)))
|
||||||
;; A \u escape is a UTF-16 code unit. jolt chars are Unicode scalars,
|
;; A \u escape is a UTF-16 code unit. jolt chars are Unicode scalars,
|
||||||
|
|
@ -208,12 +234,13 @@
|
||||||
(loop j (cons #\xFFFD acc)))))
|
(loop j (cons #\xFFFD acc)))))
|
||||||
((and (fx>=? cp #xD800) (fx<=? cp #xDFFF)) (loop j (cons #\xFFFD acc)))
|
((and (fx>=? cp #xD800) (fx<=? cp #xDFFF)) (loop j (cons #\xFFFD acc)))
|
||||||
(else (loop j (cons (integer->char cp) acc))))))
|
(else (loop j (cons (integer->char cp) acc))))))
|
||||||
(else (loop (+ i 2) (cons e acc))))))
|
(else (jolt-throw (jolt-ex-info (string-append "Unsupported escape character: \\" (string e))
|
||||||
|
empty-pmap))))))
|
||||||
(else (loop (+ i 1) (cons c acc)))))))
|
(else (loop (+ i 1) (cons c acc)))))))
|
||||||
|
|
||||||
;; backslash already consumed; read a Clojure character literal.
|
;; backslash already consumed; read a Clojure character literal.
|
||||||
(define (rdr-read-char s i end)
|
(define (rdr-read-char s i end)
|
||||||
(when (>= i end) (jolt-throw (jolt-ex-info "EOF while reading char" (empty-pmap))))
|
(when (>= i end) (jolt-throw (jolt-ex-info "EOF while reading char" empty-pmap)))
|
||||||
(let ((c0 (string-ref s i)))
|
(let ((c0 (string-ref s i)))
|
||||||
(if (char-alphabetic? c0)
|
(if (char-alphabetic? c0)
|
||||||
;; named / unicode / single-letter: collect the alnum run
|
;; named / unicode / single-letter: collect the alnum run
|
||||||
|
|
@ -240,9 +267,12 @@
|
||||||
((char=? (string-ref name 0) #\u)
|
((char=? (string-ref name 0) #\u)
|
||||||
(integer->char (string->number (substring name 1 (string-length name)) 16)))
|
(integer->char (string->number (substring name 1 (string-length name)) 16)))
|
||||||
((char=? (string-ref name 0) #\o)
|
((char=? (string-ref name 0) #\o)
|
||||||
(integer->char (string->number (substring name 1 (string-length name)) 8)))
|
(let ((v (string->number (substring name 1 (string-length name)) 8)))
|
||||||
|
(when (or (not v) (> v 255))
|
||||||
|
(jolt-throw (jolt-ex-info "Octal escape sequence must be in range [0, 377]" empty-pmap)))
|
||||||
|
(integer->char v)))
|
||||||
(else (jolt-throw (jolt-ex-info (string-append "Unsupported character: \\" name)
|
(else (jolt-throw (jolt-ex-info (string-append "Unsupported character: \\" name)
|
||||||
(empty-pmap))))))
|
empty-pmap)))))
|
||||||
|
|
||||||
;; --- token (symbol / keyword / number / nil|true|false) ---------------------
|
;; --- token (symbol / keyword / number / nil|true|false) ---------------------
|
||||||
(define (rdr-read-token s i end)
|
(define (rdr-read-token s i end)
|
||||||
|
|
@ -258,14 +288,39 @@
|
||||||
(values #f tok)
|
(values #f tok)
|
||||||
(values (substring tok 0 slash) (substring tok (+ slash 1) (string-length tok))))))
|
(values (substring tok 0 slash) (substring tok (+ slash 1) (string-length tok))))))
|
||||||
|
|
||||||
|
(define (rdr-numeric-lead? tok)
|
||||||
|
(let ((len (string-length tok)))
|
||||||
|
(and (> len 0)
|
||||||
|
(let ((c0 (string-ref tok 0)))
|
||||||
|
(or (rdr-digit? c0)
|
||||||
|
(and (or (char=? c0 #\+) (char=? c0 #\-)) (> len 1)
|
||||||
|
(rdr-digit? (string-ref tok 1))))))))
|
||||||
|
(define (rdr-invalid-token tok)
|
||||||
|
(jolt-throw (jolt-host-throwable "java.lang.RuntimeException"
|
||||||
|
(string-append "Invalid token: " tok))))
|
||||||
(define (rdr-token->value tok)
|
(define (rdr-token->value tok)
|
||||||
(let ((n (rdr-try-number tok)))
|
(let ((n (rdr-try-number tok)))
|
||||||
(cond
|
(cond
|
||||||
(n n)
|
(n n)
|
||||||
|
;; a token that starts like a number but doesn't parse as one is an
|
||||||
|
;; invalid number (1a, 08, 0x2g, 2r2), never a symbol — like the JVM.
|
||||||
|
((rdr-numeric-lead? tok)
|
||||||
|
(jolt-throw (jolt-host-throwable "java.lang.NumberFormatException"
|
||||||
|
(string-append "Invalid number: " tok))))
|
||||||
((string=? tok "nil") jolt-nil)
|
((string=? tok "nil") jolt-nil)
|
||||||
((string=? tok "true") #t)
|
((string=? tok "true") #t)
|
||||||
((string=? tok "false") #f)
|
((string=? tok "false") #f)
|
||||||
(else (let-values (((ns name) (rdr-sym-parts tok))) (jolt-symbol ns name))))))
|
(else
|
||||||
|
(let ((len (string-length tok)))
|
||||||
|
;; a lone "/" is the division symbol, and "ns//" names it in a
|
||||||
|
;; namespace (clojure.core//); otherwise a leading or trailing slash
|
||||||
|
;; leaves an empty ns/name part — an invalid token.
|
||||||
|
(when (and (> len 1)
|
||||||
|
(or (char=? (string-ref tok 0) #\/)
|
||||||
|
(and (char=? (string-ref tok (- len 1)) #\/)
|
||||||
|
(not (and (> len 2) (char=? (string-ref tok (- len 2)) #\/))))))
|
||||||
|
(rdr-invalid-token tok))
|
||||||
|
(let-values (((ns name) (rdr-sym-parts tok))) (jolt-symbol ns name)))))))
|
||||||
|
|
||||||
;; --- collections ------------------------------------------------------------
|
;; --- collections ------------------------------------------------------------
|
||||||
;; Read forms until the close delimiter; returns (values reversed?-no list j).
|
;; Read forms until the close delimiter; returns (values reversed?-no list j).
|
||||||
|
|
@ -273,7 +328,7 @@
|
||||||
(let loop ((i i) (acc '()))
|
(let loop ((i i) (acc '()))
|
||||||
(let ((i (rdr-skip-ws s i end)))
|
(let ((i (rdr-skip-ws s i end)))
|
||||||
(cond
|
(cond
|
||||||
((>= i end) (jolt-throw (jolt-ex-info "EOF while reading" (empty-pmap))))
|
((>= i end) (jolt-throw (jolt-ex-info "EOF while reading" empty-pmap)))
|
||||||
((char=? (string-ref s i) close) (values (reverse acc) (+ i 1)))
|
((char=? (string-ref s i) close) (values (reverse acc) (+ i 1)))
|
||||||
(else
|
(else
|
||||||
(let-values (((form j) (rdr-read-form s i end)))
|
(let-values (((form j) (rdr-read-form s i end)))
|
||||||
|
|
@ -289,6 +344,14 @@
|
||||||
;; sequence in a weak side-table the host contract's form-map-pairs consults.
|
;; sequence in a weak side-table the host contract's form-map-pairs consults.
|
||||||
(define rdr-map-order (make-weak-eq-hashtable))
|
(define rdr-map-order (make-weak-eq-hashtable))
|
||||||
(define (rdr-make-map es)
|
(define (rdr-make-map es)
|
||||||
|
;; the JVM reader rejects duplicate literal keys before building the map
|
||||||
|
(let dupchk ((kvs es) (seen empty-pset))
|
||||||
|
(when (pair? kvs)
|
||||||
|
(let ((k (car kvs)))
|
||||||
|
(when (jolt-truthy? (jolt-contains? seen k))
|
||||||
|
(jolt-throw (jolt-host-throwable "java.lang.IllegalArgumentException"
|
||||||
|
(string-append "Duplicate key: " (jolt-pr-str k)))))
|
||||||
|
(dupchk (cddr kvs) (pset-conj seen k)))))
|
||||||
(let ((m (apply jolt-hash-map es)))
|
(let ((m (apply jolt-hash-map es)))
|
||||||
(when (pair? es) (hashtable-set! rdr-map-order m es))
|
(when (pair? es) (hashtable-set! rdr-map-order m es))
|
||||||
m))
|
m))
|
||||||
|
|
@ -452,7 +515,7 @@
|
||||||
(let* ((splice (and (< i end) (char=? (string-ref s i) #\@)))
|
(let* ((splice (and (< i end) (char=? (string-ref s i) #\@)))
|
||||||
(start (if splice (+ i 1) i)))
|
(start (if splice (+ i 1) i)))
|
||||||
(let-values (((form j) (rdr-read-form s start end)))
|
(let-values (((form j) (rdr-read-form s start end)))
|
||||||
(when (rdr-eof? form) (jolt-throw (jolt-ex-info "EOF after #?" (empty-pmap))))
|
(when (rdr-eof? form) (jolt-throw (jolt-ex-info "EOF after #?" empty-pmap)))
|
||||||
(let ((items (cond ((pvec? form) (seq->list form))
|
(let ((items (cond ((pvec? form) (seq->list form))
|
||||||
((or (cseq? form) (empty-list-t? form)) (seq->list form))
|
((or (cseq? form) (empty-list-t? form)) (seq->list form))
|
||||||
(else '()))))
|
(else '()))))
|
||||||
|
|
@ -493,7 +556,7 @@
|
||||||
(vector->list (pvec-v form))))
|
(vector->list (pvec-v form))))
|
||||||
(else (jolt-throw (jolt-ex-info
|
(else (jolt-throw (jolt-ex-info
|
||||||
(string-append "Unreadable constructor form: #" tok)
|
(string-append "Unreadable constructor form: #" tok)
|
||||||
(empty-pmap)))))))
|
empty-pmap))))))
|
||||||
|
|
||||||
;; #:ns{…} namespaced map literal: a bare keyword/symbol key gets `ns`, a `:_/x`
|
;; #:ns{…} namespaced map literal: a bare keyword/symbol key gets `ns`, a `:_/x`
|
||||||
;; key is un-namespaced, an already-qualified key stays. #::{…} uses the current
|
;; key is un-namespaced, an already-qualified key stays. #::{…} uses the current
|
||||||
|
|
@ -521,7 +584,7 @@
|
||||||
(i2 (if auto? (+ i 1) i)))
|
(i2 (if auto? (+ i 1) i)))
|
||||||
(let loop ((j i2))
|
(let loop ((j i2))
|
||||||
(cond
|
(cond
|
||||||
((>= j end) (jolt-throw (jolt-ex-info "EOF in namespaced map literal" (empty-pmap))))
|
((>= j end) (jolt-throw (jolt-ex-info "EOF in namespaced map literal" empty-pmap)))
|
||||||
((char=? (string-ref s j) #\{)
|
((char=? (string-ref s j) #\{)
|
||||||
(let* ((nstok (substring s i2 j))
|
(let* ((nstok (substring s i2 j))
|
||||||
(mapns (if auto?
|
(mapns (if auto?
|
||||||
|
|
@ -533,7 +596,7 @@
|
||||||
(else (loop (+ j 1)))))))
|
(else (loop (+ j 1)))))))
|
||||||
|
|
||||||
(define (rdr-read-dispatch s i end) ; i points just past the '#'
|
(define (rdr-read-dispatch s i end) ; i points just past the '#'
|
||||||
(when (>= i end) (jolt-throw (jolt-ex-info "EOF after #" (empty-pmap))))
|
(when (>= i end) (jolt-throw (jolt-ex-info "EOF after #" empty-pmap)))
|
||||||
(let ((c (string-ref s i)))
|
(let ((c (string-ref s i)))
|
||||||
(cond
|
(cond
|
||||||
((char=? c #\{) ; #{...} set
|
((char=? c #\{) ; #{...} set
|
||||||
|
|
@ -548,8 +611,12 @@
|
||||||
(let-values (((src j) (rdr-read-regex s (+ i 1) end)))
|
(let-values (((src j) (rdr-read-regex s (+ i 1) end)))
|
||||||
(values (jolt-re-pattern src) j)))
|
(values (jolt-re-pattern src) j)))
|
||||||
((char=? c #\_) ; #_ discard the next form
|
((char=? c #\_) ; #_ discard the next form
|
||||||
(let-values (((_ j) (rdr-read-form s (+ i 1) end)))
|
(let-values (((d j) (rdr-read-form s (+ i 1) end)))
|
||||||
(when (rdr-eof? _) (jolt-throw (jolt-ex-info "EOF after #_" (empty-pmap))))
|
(when (rdr-eof? d) (jolt-throw (jolt-ex-info "EOF after #_" empty-pmap)))
|
||||||
|
;; edn validates the discarded element (its tags go through the same
|
||||||
|
;; :readers/:default pipeline; an unreadable one throws)
|
||||||
|
(let ((cb (rdr-discard-cb)))
|
||||||
|
(when cb (jolt-invoke cb d)))
|
||||||
(rdr-read-form s j end)))
|
(rdr-read-form s j end)))
|
||||||
((char=? c #\') ; #'x var-quote -> (var x)
|
((char=? c #\') ; #'x var-quote -> (var x)
|
||||||
(let-values (((form j) (rdr-read-form s (+ i 1) end)))
|
(let-values (((form j) (rdr-read-form s (+ i 1) end)))
|
||||||
|
|
@ -558,7 +625,7 @@
|
||||||
(let-values (((mform j) (rdr-read-form s (+ i 1) end)))
|
(let-values (((mform j) (rdr-read-form s (+ i 1) end)))
|
||||||
(let-values (((target k) (rdr-read-form s j end)))
|
(let-values (((target k) (rdr-read-form s j end)))
|
||||||
(when (rdr-eof? target)
|
(when (rdr-eof? target)
|
||||||
(jolt-throw (jolt-ex-info "EOF after #^meta" (empty-pmap))))
|
(jolt-throw (jolt-ex-info "EOF after #^meta" empty-pmap)))
|
||||||
(values (rdr-attach-meta target (rdr-meta-map mform)) k))))
|
(values (rdr-attach-meta target (rdr-meta-map mform)) k))))
|
||||||
((char=? c #\#) ; ## symbolic value: ##Inf / ##-Inf / ##NaN
|
((char=? c #\#) ; ## symbolic value: ##Inf / ##-Inf / ##NaN
|
||||||
(let-values (((tok j) (rdr-read-token s (+ i 1) end)))
|
(let-values (((tok j) (rdr-read-token s (+ i 1) end)))
|
||||||
|
|
@ -566,7 +633,7 @@
|
||||||
((string=? tok "-Inf") -inf.0)
|
((string=? tok "-Inf") -inf.0)
|
||||||
((string=? tok "NaN") +nan.0)
|
((string=? tok "NaN") +nan.0)
|
||||||
(else (jolt-throw (jolt-ex-info (string-append "unknown ## literal: " tok)
|
(else (jolt-throw (jolt-ex-info (string-append "unknown ## literal: " tok)
|
||||||
(empty-pmap)))))
|
empty-pmap))))
|
||||||
j)))
|
j)))
|
||||||
((char=? c #\?) ; #?(...) / #?@(...) reader conditional
|
((char=? c #\?) ; #?(...) / #?@(...) reader conditional
|
||||||
(rdr-read-reader-cond s (+ i 1) end))
|
(rdr-read-reader-cond s (+ i 1) end))
|
||||||
|
|
@ -575,7 +642,7 @@
|
||||||
(else ; #tag form -> tagged {:tag :#tag :form ...}
|
(else ; #tag form -> tagged {:tag :#tag :form ...}
|
||||||
(let-values (((tok j) (rdr-read-token s i end)))
|
(let-values (((tok j) (rdr-read-token s i end)))
|
||||||
(let-values (((form k) (rdr-read-form s j end)))
|
(let-values (((form k) (rdr-read-form s j end)))
|
||||||
(when (rdr-eof? form) (jolt-throw (jolt-ex-info "EOF after #tag" (empty-pmap))))
|
(when (rdr-eof? form) (jolt-throw (jolt-ex-info "EOF after #tag" empty-pmap)))
|
||||||
(if (rdr-record-tag? tok) ; #ns.Type{..}/[..] record literal
|
(if (rdr-record-tag? tok) ; #ns.Type{..}/[..] record literal
|
||||||
(values (rdr-record-ctor-form tok form) k)
|
(values (rdr-record-ctor-form tok form) k)
|
||||||
(values (rdr-make-tagged (keyword #f (string-append "#" tok)) form) k))))))))
|
(values (rdr-make-tagged (keyword #f (string-append "#" tok)) form) k))))))))
|
||||||
|
|
@ -584,7 +651,7 @@
|
||||||
;; every other backslash sequence is kept verbatim (regex engine semantics).
|
;; every other backslash sequence is kept verbatim (regex engine semantics).
|
||||||
(define (rdr-read-regex s i end)
|
(define (rdr-read-regex s i end)
|
||||||
(let loop ((i i) (acc '()))
|
(let loop ((i i) (acc '()))
|
||||||
(when (>= i end) (jolt-throw (jolt-ex-info "EOF while reading regex" (empty-pmap))))
|
(when (>= i end) (jolt-throw (jolt-ex-info "EOF while reading regex" empty-pmap)))
|
||||||
(let ((c (string-ref s i)))
|
(let ((c (string-ref s i)))
|
||||||
(cond
|
(cond
|
||||||
((char=? c #\") (values (list->string (reverse acc)) (+ i 1)))
|
((char=? c #\") (values (list->string (reverse acc)) (+ i 1)))
|
||||||
|
|
@ -601,6 +668,17 @@
|
||||||
(let ((auto? (and (< i end) (char=? (string-ref s i) #\:))))
|
(let ((auto? (and (< i end) (char=? (string-ref s i) #\:))))
|
||||||
(let ((i (if auto? (+ i 1) i)))
|
(let ((i (if auto? (+ i 1) i)))
|
||||||
(let-values (((tok j) (rdr-read-token s i end)))
|
(let-values (((tok j) (rdr-read-token s i end)))
|
||||||
|
(let ((len (string-length tok)))
|
||||||
|
;; ":" and "::" alone, a leading or trailing slash (a name of exactly
|
||||||
|
;; "/" is fine, :ns//), or an auto-resolved keyword in edn (no
|
||||||
|
;; resolution context) are invalid tokens.
|
||||||
|
(when (or (= len 0)
|
||||||
|
(and (> len 1) (char=? (string-ref tok 0) #\/))
|
||||||
|
(and (> len 1) (char=? (string-ref tok (- len 1)) #\/)
|
||||||
|
(not (and (> len 2) (char=? (string-ref tok (- len 2)) #\/)))))
|
||||||
|
(rdr-invalid-token (string-append (if auto? "::" ":") tok)))
|
||||||
|
(when (and auto? (rdr-edn-mode))
|
||||||
|
(rdr-invalid-token (string-append "::" tok))))
|
||||||
(let-values (((ns name) (rdr-sym-parts tok)))
|
(let-values (((ns name) (rdr-sym-parts tok)))
|
||||||
(if auto?
|
(if auto?
|
||||||
(let* ((cur (chez-current-ns))
|
(let* ((cur (chez-current-ns))
|
||||||
|
|
@ -638,7 +716,7 @@
|
||||||
;; inert: ``42 reads as 42, ```"meow" as "meow".
|
;; inert: ``42 reads as 42, ```"meow" as "meow".
|
||||||
((char=? c #\`)
|
((char=? c #\`)
|
||||||
(let-values (((form j) (rdr-read-form s (+ i 1) end)))
|
(let-values (((form j) (rdr-read-form s (+ i 1) end)))
|
||||||
(when (rdr-eof? form) (jolt-throw (jolt-ex-info "EOF after `" (empty-pmap))))
|
(when (rdr-eof? form) (jolt-throw (jolt-ex-info "EOF after `" empty-pmap)))
|
||||||
(values (if (rdr-self-eval-literal? form)
|
(values (if (rdr-self-eval-literal? form)
|
||||||
form
|
form
|
||||||
(jolt-list (jolt-symbol #f "syntax-quote") form))
|
(jolt-list (jolt-symbol #f "syntax-quote") form))
|
||||||
|
|
@ -655,7 +733,7 @@
|
||||||
(let-values (((mform j) (rdr-read-form s (+ i 1) end)))
|
(let-values (((mform j) (rdr-read-form s (+ i 1) end)))
|
||||||
(let-values (((target k) (rdr-read-form s j end)))
|
(let-values (((target k) (rdr-read-form s j end)))
|
||||||
(when (rdr-eof? target)
|
(when (rdr-eof? target)
|
||||||
(jolt-throw (jolt-ex-info "EOF after ^meta" (empty-pmap))))
|
(jolt-throw (jolt-ex-info "EOF after ^meta" empty-pmap)))
|
||||||
(values (rdr-attach-meta target (rdr-meta-map mform)) k))))
|
(values (rdr-attach-meta target (rdr-meta-map mform)) k))))
|
||||||
(else
|
(else
|
||||||
(let-values (((tok j) (rdr-read-token s i end)))
|
(let-values (((tok j) (rdr-read-token s i end)))
|
||||||
|
|
@ -670,7 +748,7 @@
|
||||||
(define (rdr-wrap s i end head)
|
(define (rdr-wrap s i end head)
|
||||||
(let-values (((form j) (rdr-read-form s i end)))
|
(let-values (((form j) (rdr-read-form s i end)))
|
||||||
(when (rdr-eof? form)
|
(when (rdr-eof? form)
|
||||||
(jolt-throw (jolt-ex-info "EOF while reading reader macro" (empty-pmap))))
|
(jolt-throw (jolt-ex-info "EOF while reading reader macro" empty-pmap)))
|
||||||
(values (jolt-list head form) j)))
|
(values (jolt-list head form) j)))
|
||||||
|
|
||||||
;; --- form -> data -----------------------------------------------------------
|
;; --- form -> data -----------------------------------------------------------
|
||||||
|
|
@ -735,20 +813,86 @@
|
||||||
(let ((v (var-deref "clojure.core" "*default-data-reader-fn*")))
|
(let ((v (var-deref "clojure.core" "*default-data-reader-fn*")))
|
||||||
(and (not (jolt-nil? v)) (procedure? v) v))))
|
(and (not (jolt-nil? v)) (procedure? v) v))))
|
||||||
|
|
||||||
|
;; strict #inst validation: RFC-3339 calendar fields must be real (month 1-12,
|
||||||
|
;; day valid for the month incl. leap years, hour < 24, minute/second < 60).
|
||||||
|
(define (rdr-2dig s i)
|
||||||
|
(and (< (+ i 1) (string-length s))
|
||||||
|
(rdr-digit? (string-ref s i)) (rdr-digit? (string-ref s (+ i 1)))
|
||||||
|
(+ (* 10 (- (char->integer (string-ref s i)) 48))
|
||||||
|
(- (char->integer (string-ref s (+ i 1))) 48))))
|
||||||
|
(define (rdr-leap? y) (and (= 0 (modulo y 4)) (or (not (= 0 (modulo y 100))) (= 0 (modulo y 400)))))
|
||||||
|
(define (rdr-inst-throw s)
|
||||||
|
(jolt-throw (jolt-host-throwable "java.lang.RuntimeException"
|
||||||
|
(string-append "Unrecognized date/time syntax: " s))))
|
||||||
|
(define (rdr-validate-inst! s)
|
||||||
|
;; progressive RFC-3339 like clojure.instant: yyyy[-MM[-dd[Thh[:mm[:ss[.f]]]]]]
|
||||||
|
;; with an optional Z/±hh:mm offset; each present field must be in range
|
||||||
|
;; (months 1-12, day valid for the month incl. leap years, hour < 24, min < 60).
|
||||||
|
(let* ((len (string-length s))
|
||||||
|
(y (and (>= len 4) (rdr-all-digits? s 0 4) (string->number (substring s 0 4)))))
|
||||||
|
(unless y (rdr-inst-throw s))
|
||||||
|
(when (>= len 5)
|
||||||
|
(unless (char=? (string-ref s 4) #\-) (rdr-inst-throw s))
|
||||||
|
(let ((mo (rdr-2dig s 5)))
|
||||||
|
(unless (and mo (>= mo 1) (<= mo 12)) (rdr-inst-throw s))
|
||||||
|
(when (>= len 8)
|
||||||
|
(unless (char=? (string-ref s 7) #\-) (rdr-inst-throw s))
|
||||||
|
(let ((d (rdr-2dig s 8)))
|
||||||
|
(unless (and d (>= d 1)
|
||||||
|
(<= d (vector-ref (if (rdr-leap? y)
|
||||||
|
'#(31 29 31 30 31 30 31 31 30 31 30 31)
|
||||||
|
'#(31 28 31 30 31 30 31 31 30 31 30 31))
|
||||||
|
(- mo 1))))
|
||||||
|
(rdr-inst-throw s))
|
||||||
|
(when (>= len 11)
|
||||||
|
(unless (char=? (string-ref s 10) #\T) (rdr-inst-throw s))
|
||||||
|
(let ((h (rdr-2dig s 11)))
|
||||||
|
(unless (and h (<= h 23)) (rdr-inst-throw s))
|
||||||
|
(when (>= len 14)
|
||||||
|
(when (char=? (string-ref s 13) #\:)
|
||||||
|
(let ((mi (rdr-2dig s 14)))
|
||||||
|
(unless (and mi (<= mi 59)) (rdr-inst-throw s)))))))))))))
|
||||||
|
;; strict #uuid: canonical 8-4-4-4-12 hex groups.
|
||||||
|
(define (rdr-validate-uuid! s)
|
||||||
|
(define (hexrun? from to)
|
||||||
|
(let loop ((i from))
|
||||||
|
(cond ((>= i to) #t)
|
||||||
|
((let ((c (char-downcase (string-ref s i))))
|
||||||
|
(or (rdr-digit? c) (and (char>=? c #\a) (char<=? c #\f))))
|
||||||
|
(loop (+ i 1)))
|
||||||
|
(else #f))))
|
||||||
|
(unless (and (= (string-length s) 36)
|
||||||
|
(char=? (string-ref s 8) #\-) (char=? (string-ref s 13) #\-)
|
||||||
|
(char=? (string-ref s 18) #\-) (char=? (string-ref s 23) #\-)
|
||||||
|
(hexrun? 0 8) (hexrun? 9 13) (hexrun? 14 18) (hexrun? 19 23) (hexrun? 24 36))
|
||||||
|
(jolt-throw (jolt-host-throwable "java.lang.IllegalArgumentException"
|
||||||
|
(string-append "Invalid UUID string: " s)))))
|
||||||
|
|
||||||
;; read-string / read data seam: construct the value for a #tag literal. #inst,
|
;; read-string / read data seam: construct the value for a #tag literal. #inst,
|
||||||
;; #uuid and #"regex" are built in; any other tag is applied from *data-readers*,
|
;; #uuid and #"regex" are built in; any other tag is applied from *data-readers*,
|
||||||
;; then *default-data-reader-fn*. An unregistered tag with no default handler stays
|
;; then *default-data-reader-fn*. An unregistered tag with no default handler stays
|
||||||
;; a tagged FORM (lenient — clojure.edn raises instead).
|
;; a tagged FORM (lenient — clojure.edn raises instead).
|
||||||
(define (rdr-construct-tag tag inner)
|
(define (rdr-construct-tag tag inner)
|
||||||
(cond
|
(cond
|
||||||
((eq? tag (keyword #f "#inst")) (jolt-inst-from-string inner))
|
((eq? tag (keyword #f "#inst"))
|
||||||
((eq? tag (keyword #f "#uuid")) (jolt-uuid-from-string inner))
|
(when (string? inner) (rdr-validate-inst! inner))
|
||||||
|
(jolt-inst-from-string inner))
|
||||||
|
((eq? tag (keyword #f "#uuid"))
|
||||||
|
(when (string? inner) (rdr-validate-uuid! inner))
|
||||||
|
(jolt-uuid-from-string inner))
|
||||||
((eq? tag (keyword #f "regex")) (jolt-re-pattern inner))
|
((eq? tag (keyword #f "regex")) (jolt-re-pattern inner))
|
||||||
|
;; the M-literal form: construct the BigDecimal from its numeric text
|
||||||
|
((eq? tag (keyword #f "bigdec")) (jolt-bigdec-from-string inner))
|
||||||
(else (let ((fn (rdr-data-reader-fn tag)))
|
(else (let ((fn (rdr-data-reader-fn tag)))
|
||||||
(if fn (jolt-invoke fn inner)
|
(if fn (jolt-invoke fn inner)
|
||||||
(let ((dfn (rdr-default-data-reader-fn)))
|
(let ((dfn (rdr-default-data-reader-fn)))
|
||||||
(if dfn (jolt-invoke dfn (rdr-tag->symbol tag) inner)
|
(if dfn (jolt-invoke dfn (rdr-tag->symbol tag) inner)
|
||||||
(rdr-make-tagged tag inner))))))))
|
;; no reader for the tag: a proper tagged-literal value, like
|
||||||
|
;; Clojure's *default-data-reader-fn* (tagged-literal), so
|
||||||
|
;; tagged-literal? / :tag / :form / printing all work — not the
|
||||||
|
;; internal reader form. clojure.edn reads raw forms via
|
||||||
|
;; __read-form-raw, so its :readers/:default path is unaffected.
|
||||||
|
(jolt-tagged-literal (rdr-tag->symbol tag) inner))))))))
|
||||||
|
|
||||||
;; rdr-form->data*: convert the VALUE structure (set/tagged/nested forms). The
|
;; rdr-form->data*: convert the VALUE structure (set/tagged/nested forms). The
|
||||||
;; wrapper below adds the metadata, so the unchanged branches return x bare.
|
;; wrapper below adds the metadata, so the unchanged branches return x bare.
|
||||||
|
|
@ -760,7 +904,11 @@
|
||||||
(let ((items (jolt-get x rdr-kw-value)))
|
(let ((items (jolt-get x rdr-kw-value)))
|
||||||
(let loop ((i 0) (s empty-pset))
|
(let loop ((i 0) (s empty-pset))
|
||||||
(if (fx>=? i (pvec-count items)) s
|
(if (fx>=? i (pvec-count items)) s
|
||||||
(loop (fx+ i 1) (pset-conj s (rdr-form->data (pvec-nth-d items i jolt-nil))))))))
|
(let ((v (rdr-form->data (pvec-nth-d items i jolt-nil))))
|
||||||
|
(when (jolt-truthy? (jolt-contains? s v))
|
||||||
|
(jolt-throw (jolt-host-throwable "java.lang.IllegalArgumentException"
|
||||||
|
(string-append "Duplicate key: " (jolt-pr-str v)))))
|
||||||
|
(loop (fx+ i 1) (pset-conj s v)))))))
|
||||||
((pvec? x)
|
((pvec? x)
|
||||||
(let-values (((items changed) (rdr-conv-each (vector->list (pvec-v x)))))
|
(let-values (((items changed) (rdr-conv-each (vector->list (pvec-v x)))))
|
||||||
(if changed (apply jolt-vector items) x)))
|
(if changed (apply jolt-vector items) x)))
|
||||||
|
|
@ -785,12 +933,35 @@
|
||||||
(if (jolt-nil? m) v (jolt-with-meta v (rdr-form->data m)))))
|
(if (jolt-nil? m) v (jolt-with-meta v (rdr-form->data m)))))
|
||||||
|
|
||||||
;; --- the two host seams -----------------------------------------------------
|
;; --- the two host seams -----------------------------------------------------
|
||||||
|
;; a top-level read: a stray close delimiter is unmatched (read-seq consumes the
|
||||||
|
;; close of an open collection; anything reaching here is unbalanced input).
|
||||||
|
(define (rdr-read-top s i end)
|
||||||
|
(let ((k (rdr-skip-ws s i end)))
|
||||||
|
(when (and (< k end)
|
||||||
|
(let ((c (string-ref s k)))
|
||||||
|
(or (char=? c #\)) (char=? c #\]) (char=? c #\}))))
|
||||||
|
(jolt-throw (jolt-ex-info (string-append "Unmatched delimiter: "
|
||||||
|
(string (string-ref s k)))
|
||||||
|
empty-pmap)))
|
||||||
|
(rdr-read-form s k end)))
|
||||||
|
|
||||||
;; clojure.core/read-string: first form, or nil for blank / comment-only input
|
;; clojure.core/read-string: first form, or nil for blank / comment-only input
|
||||||
;; (parse-string wart, matched deliberately). jolt-read-form-raw keeps set FORMS
|
;; (parse-string wart, matched deliberately). jolt-read-form-raw keeps set FORMS
|
||||||
;; for the compiler spine (compile-eval); the data seam converts them to sets.
|
;; for the compiler spine (compile-eval); the data seam converts them to sets.
|
||||||
(define (jolt-read-form-raw s)
|
(define (jolt-read-form-raw s)
|
||||||
(let-values (((form j) (rdr-read-form s 0 (string-length s))))
|
(let-values (((form j) (rdr-read-top s 0 (string-length s))))
|
||||||
(if (rdr-eof? form) jolt-nil form)))
|
(if (rdr-eof? form) jolt-nil form)))
|
||||||
|
|
||||||
|
;; the edn seam: strict mode (no auto-resolved keywords), each #_ discard handed
|
||||||
|
;; to the callback for tag validation, and a distinct EOF sentinel so the edn
|
||||||
|
;; layer can honor its :eof option (nil input is a plain EOF).
|
||||||
|
(define (jolt-read-form-edn s cb)
|
||||||
|
(if (jolt-nil? s)
|
||||||
|
(keyword "jolt" "reader-eof")
|
||||||
|
(parameterize ((rdr-edn-mode #t)
|
||||||
|
(rdr-discard-cb (if (jolt-nil? cb) #f cb)))
|
||||||
|
(let-values (((form j) (rdr-read-top s 0 (string-length s))))
|
||||||
|
(if (rdr-eof? form) (keyword "jolt" "reader-eof") form)))))
|
||||||
(define (jolt-read-string s)
|
(define (jolt-read-string s)
|
||||||
(let ((form (jolt-read-form-raw s)))
|
(let ((form (jolt-read-form-raw s)))
|
||||||
(if (jolt-nil? form) form (rdr-form->data form))))
|
(if (jolt-nil? form) form (rdr-form->data form))))
|
||||||
|
|
@ -798,7 +969,7 @@
|
||||||
;; __parse-next: [form rest-of-string] or nil when only whitespace/comments left.
|
;; __parse-next: [form rest-of-string] or nil when only whitespace/comments left.
|
||||||
(define (jolt-parse-next s)
|
(define (jolt-parse-next s)
|
||||||
(let ((end (string-length s)))
|
(let ((end (string-length s)))
|
||||||
(let-values (((form j) (rdr-read-form s 0 end)))
|
(let-values (((form j) (rdr-read-top s 0 end)))
|
||||||
(if (rdr-eof? form)
|
(if (rdr-eof? form)
|
||||||
jolt-nil
|
jolt-nil
|
||||||
(jolt-vector (rdr-form->data form) (substring s j end))))))
|
(jolt-vector (rdr-form->data form) (substring s j end))))))
|
||||||
|
|
@ -807,8 +978,13 @@
|
||||||
;; is the :#name keyword the reader produced; #uuid/#inst reuse the inst-time ctors.
|
;; is the :#name keyword the reader produced; #uuid/#inst reuse the inst-time ctors.
|
||||||
(define (jolt-read-tagged tag form)
|
(define (jolt-read-tagged tag form)
|
||||||
(cond
|
(cond
|
||||||
((eq? tag (keyword #f "#uuid")) (jolt-uuid-from-string form))
|
((eq? tag (keyword #f "#uuid"))
|
||||||
((eq? tag (keyword #f "#inst")) (jolt-inst-from-string form))
|
(when (string? form) (rdr-validate-uuid! form))
|
||||||
|
(jolt-uuid-from-string form))
|
||||||
|
((eq? tag (keyword #f "#inst"))
|
||||||
|
(when (string? form) (rdr-validate-inst! form))
|
||||||
|
(jolt-inst-from-string form))
|
||||||
|
((eq? tag (keyword #f "bigdec")) (jolt-bigdec-from-string form))
|
||||||
;; No registered reader: consult *default-data-reader-fn*, else throw a clean,
|
;; No registered reader: consult *default-data-reader-fn*, else throw a clean,
|
||||||
;; catchable ex-info naming the tag, like the JVM's "No reader function for tag
|
;; catchable ex-info naming the tag, like the JVM's "No reader function for tag
|
||||||
;; foobar" (empty-pmap is a VALUE — the old (empty-pmap) applied it as a
|
;; foobar" (empty-pmap is a VALUE — the old (empty-pmap) applied it as a
|
||||||
|
|
@ -828,3 +1004,4 @@
|
||||||
;; :default (a #inst can be overridden to defer), rather than read-string building
|
;; :default (a #inst can be overridden to defer), rather than read-string building
|
||||||
;; the built-in #inst eagerly (which fails on a non-string like #inst ^:ref […]).
|
;; the built-in #inst eagerly (which fails on a non-string like #inst ^:ref […]).
|
||||||
(def-var! "clojure.core" "__read-form-raw" jolt-read-form-raw)
|
(def-var! "clojure.core" "__read-form-raw" jolt-read-form-raw)
|
||||||
|
(def-var! "clojure.core" "__read-form-edn" jolt-read-form-edn)
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,10 @@
|
||||||
;; resolves "Raw" to its real tag "a.util.Raw" here instead of prepending the
|
;; resolves "Raw" to its real tag "a.util.Raw" here instead of prepending the
|
||||||
;; calling ns. The local ns is preferred, so a same-named local type still wins.
|
;; calling ns. The local ns is preferred, so a same-named local type still wins.
|
||||||
(define chez-deftype-tag-set (make-hashtable string-hash string=?))
|
(define chez-deftype-tag-set (make-hashtable string-hash string=?))
|
||||||
|
;; ctor procedure -> its class tag: the type NAME var holds the ctor (a jolt-ism;
|
||||||
|
;; the JVM resolves it to the class), so class-key maps the ctor back to the
|
||||||
|
;; class for (ancestors TypeName) / (isa? x TypeName) / derive on the type.
|
||||||
|
(define chez-deftype-ctor-tag (make-weak-eq-hashtable))
|
||||||
(define chez-simple-name-tag (make-hashtable string-hash string=?))
|
(define chez-simple-name-tag (make-hashtable string-hash string=?))
|
||||||
;; a jrec that is coll? — a record, or a deftype implementing a collection
|
;; a jrec that is coll? — a record, or a deftype implementing a collection
|
||||||
;; interface (its seq/count/nth/valAt/cons method is registered). find-method-any-
|
;; interface (its seq/count/nth/valAt/cons method is registered). find-method-any-
|
||||||
|
|
@ -304,7 +308,25 @@
|
||||||
;; implements a clojure.lang collection interface carries the op as an inline
|
;; implements a clojure.lang collection interface carries the op as an inline
|
||||||
;; method — prefer that method, else fall back to the field/map behavior. (jrec-cl
|
;; method — prefer that method, else fall back to the field/map behavior. (jrec-cl
|
||||||
;; finds the method; find-method-any-protocol / jolt-invoke resolve at call time.)
|
;; finds the method; find-method-any-protocol / jolt-invoke resolve at call time.)
|
||||||
(define (jrec-cl coll name) (and (jrec? coll) (find-method-any-protocol (jrec-tag coll) name)))
|
;; Same lookup as collections.ss rec-coll-method — one definition, aliased here.
|
||||||
|
(define jrec-cl rec-coll-method)
|
||||||
|
|
||||||
|
;; iface-method: the single deftype/reify interface-method lookup. Returns the
|
||||||
|
;; impl fn for METHOD declared by V (a deftype/record OR a reify), or #f. NARGS
|
||||||
|
;; (including `this`) selects the matching arity for a deftype; #f means any
|
||||||
|
;; arity. Core fns route interface dispatch through this instead of each
|
||||||
|
;; re-deriving jrec-vs-reify lookup and arity handling.
|
||||||
|
(define (iface-method v method nargs)
|
||||||
|
(cond ((jrec? v)
|
||||||
|
(if nargs (find-method-any-protocol-arity (jrec-tag v) method nargs)
|
||||||
|
(find-method-any-protocol (jrec-tag v) method)))
|
||||||
|
((jreify? v) (let ((rm (reified-methods v))) (and rm (hashtable-ref rm method #f))))
|
||||||
|
(else #f)))
|
||||||
|
;; Call METHOD on V with ARGS (a list, `this` excluded) if V declares it, else run
|
||||||
|
;; FALLBACK. The one seam a core fn's deftype/reify arm collapses to.
|
||||||
|
(define (iface-call v method args fallback)
|
||||||
|
(let ((m (iface-method v method (+ 1 (length args)))))
|
||||||
|
(if m (apply jolt-invoke m v args) (fallback))))
|
||||||
(define %r-jolt-count jolt-count)
|
(define %r-jolt-count jolt-count)
|
||||||
(set! jolt-count (lambda (coll)
|
(set! jolt-count (lambda (coll)
|
||||||
(cond ((jrec-cl coll "count") => (lambda (m) (jolt-invoke m coll)))
|
(cond ((jrec-cl coll "count") => (lambda (m) (jolt-invoke m coll)))
|
||||||
|
|
@ -473,24 +495,22 @@
|
||||||
((number? obj) '("Long" "Integer" "BigInteger" "BigInt" "Number" "Object"))
|
((number? obj) '("Long" "Integer" "BigInteger" "BigInt" "Number" "Object"))
|
||||||
((string? obj) '("String" "CharSequence" "Object"))
|
((string? obj) '("String" "CharSequence" "Object"))
|
||||||
((boolean? obj) '("Boolean" "Object"))
|
((boolean? obj) '("Boolean" "Object"))
|
||||||
((keyword? obj) '("Keyword" "Named" "Object"))
|
((keyword? obj) (jch-tags "clojure.lang.Keyword"))
|
||||||
((jolt-symbol? obj) '("Symbol" "Named" "Object"))
|
((jolt-symbol? obj) (jch-tags "clojure.lang.Symbol"))
|
||||||
((pvec? obj) '("PersistentVector" "APersistentVector" "IPersistentVector" "IPersistentCollection"
|
((pvec? obj) (jch-tags "clojure.lang.PersistentVector"))
|
||||||
"List" "java.util.List" "Sequential" "Collection" "Iterable" "java.lang.Iterable" "Object"))
|
((pmap? obj) (jch-tags "clojure.lang.PersistentArrayMap"))
|
||||||
((pmap? obj) '("PersistentArrayMap" "APersistentMap" "IPersistentMap" "Associative"
|
((pset? obj) (jch-tags "clojure.lang.PersistentHashSet"))
|
||||||
"Map" "java.util.Map" "Iterable" "java.lang.Iterable" "Object"))
|
|
||||||
((pset? obj) '("PersistentHashSet" "APersistentSet" "IPersistentSet" "Set" "java.util.Set" "Collection" "Iterable" "java.lang.Iterable" "Object"))
|
|
||||||
;; jolt models every seq as a list (no distinct LazySeq), so a seq also
|
;; jolt models every seq as a list (no distinct LazySeq), so a seq also
|
||||||
;; reports PersistentList / IPersistentList / IPersistentStack — extend-protocol
|
;; reports PersistentList / IPersistentList / IPersistentStack — extend-protocol
|
||||||
;; clojure.lang.IPersistentList (algo.monads' writer monad) dispatches on one.
|
;; clojure.lang.IPersistentList (algo.monads' writer monad) dispatches on one.
|
||||||
((or (cseq? obj) (empty-list-t? obj)) '("PersistentList" "IPersistentList" "IPersistentStack" "ASeq" "ISeq" "IPersistentCollection" "Sequential" "Collection" "Iterable" "java.lang.Iterable" "Object"))
|
((or (cseq? obj) (empty-list-t? obj)) (jch-tags "clojure.lang.PersistentList"))
|
||||||
;; a lazy seq (map/filter/… result) is clojure.lang.LazySeq: a Sequential
|
;; a lazy seq (map/filter/… result) is clojure.lang.LazySeq: a Sequential
|
||||||
;; ISeq, but not a PersistentList — matching the JVM so extend-protocol /
|
;; ISeq, but not a PersistentList — matching the JVM so extend-protocol /
|
||||||
;; instance? on a deferred seq dispatch like an eager one where they should.
|
;; instance? on a deferred seq dispatch like an eager one where they should.
|
||||||
((jolt-lazyseq? obj) '("LazySeq" "ISeq" "IPersistentCollection" "Sequential" "Collection" "Iterable" "java.lang.Iterable" "Object"))
|
((jolt-lazyseq? obj) (jch-tags "clojure.lang.LazySeq"))
|
||||||
;; a var is clojure.lang.Var (also IDeref / IFn) — reitit's Expand protocol
|
;; a var is clojure.lang.Var (also IDeref / IFn) — reitit's Expand protocol
|
||||||
;; extends to Var so a #'handler route dispatches.
|
;; extends to Var so a #'handler route dispatches.
|
||||||
((var-cell? obj) '("Var" "clojure.lang.Var" "IDeref" "IFn" "Object"))
|
((var-cell? obj) (jch-tags "clojure.lang.Var"))
|
||||||
;; java.net.URI jhost — extend-protocol java.net.URI (hiccup ToURI/ToStr).
|
;; java.net.URI jhost — extend-protocol java.net.URI (hiccup ToURI/ToStr).
|
||||||
((and (jhost? obj) (string=? (jhost-tag obj) "uri")) '("URI" "java.net.URI" "Object"))
|
((and (jhost? obj) (string=? (jhost-tag obj) "uri")) '("URI" "java.net.URI" "Object"))
|
||||||
;; a ByteBuffer — extend-protocol java.nio.ByteBuffer (aws-api util).
|
;; a ByteBuffer — extend-protocol java.nio.ByteBuffer (aws-api util).
|
||||||
|
|
@ -541,7 +561,7 @@
|
||||||
;; extended to both (data.json's JSONWriter) routes a sql.Date to its impl.
|
;; extended to both (data.json's JSONWriter) routes a sql.Date to its impl.
|
||||||
((and (jhost? obj) (string=? (jhost-tag obj) "sql-date")) '("java.sql.Date" "Date" "java.util.Date" "Object"))
|
((and (jhost? obj) (string=? (jhost-tag obj) "sql-date")) '("java.sql.Date" "Date" "java.util.Date" "Object"))
|
||||||
;; a bare procedure (fn) — extend-protocol to clojure.lang.{Fn,IFn,AFn}.
|
;; a bare procedure (fn) — extend-protocol to clojure.lang.{Fn,IFn,AFn}.
|
||||||
((procedure? obj) '("Fn" "IFn" "AFn" "Object"))
|
((procedure? obj) (jch-tags "clojure.lang.AFunction"))
|
||||||
((jolt-nil? obj) '("nil"))
|
((jolt-nil? obj) '("nil"))
|
||||||
;; a defrecord IS the clojure.lang map/record interfaces, so a protocol
|
;; a defrecord IS the clojure.lang map/record interfaces, so a protocol
|
||||||
;; extended to IRecord / IPersistentMap / Associative / Seqable / … (and not
|
;; extended to IRecord / IPersistentMap / Associative / Seqable / … (and not
|
||||||
|
|
@ -587,14 +607,26 @@
|
||||||
(number? a) (not (flonum? a)))
|
(number? a) (not (flonum? a)))
|
||||||
(exact->inexact a) a))
|
(exact->inexact a) a))
|
||||||
(loop (cdr as) (+ i 1)))))))))
|
(loop (cdr as) (+ i 1)))))))))
|
||||||
;; Register the ctor globally by simple class name (like StringBuilder) so
|
;; Register the ctor under its fully-qualified tag ("ns.Name") — a bare
|
||||||
;; (Name. …) interop resolves ns-agnostically: a deftype used across files works
|
;; (Name. …) in the DEFINING ns is qualified to this by the analyzer, so a
|
||||||
;; even when the runtime current ns is the caller's, not the defining ns
|
;; deftype whose simple name collides with a built-in host class (tools.reader's
|
||||||
;; (host-new checks class-ctors-tbl before the current-ns var fallback).
|
;; PushbackReader vs java.io.PushbackReader) still resolves correctly there.
|
||||||
(register-class-ctor! (symbol-t-name name-sym) ctor)
|
(register-class-ctor! tag ctor)
|
||||||
|
;; Also register the simple name so (Name. …) resolves ns-agnostically across
|
||||||
|
;; files — BUT never clobber a built-in host class of the same simple name (an
|
||||||
|
;; unrelated ns's bare (Name. …) must still reach the built-in). A prior deftype
|
||||||
|
;; (tracked in chez-simple-name-tag) is fine to overwrite (last def wins / redef).
|
||||||
|
(when (or (not (hashtable-ref class-ctors-tbl (symbol-t-name name-sym) #f))
|
||||||
|
(hashtable-ref chez-simple-name-tag (symbol-t-name name-sym) #f))
|
||||||
|
(register-class-ctor! (symbol-t-name name-sym) ctor))
|
||||||
;; index the tag so a cross-ns extend-protocol resolves the bare type name.
|
;; index the tag so a cross-ns extend-protocol resolves the bare type name.
|
||||||
(hashtable-set! chez-deftype-tag-set tag #t)
|
(hashtable-set! chez-deftype-tag-set tag #t)
|
||||||
(hashtable-set! chez-simple-name-tag (symbol-t-name name-sym) tag)
|
(hashtable-set! chez-simple-name-tag (symbol-t-name name-sym) tag)
|
||||||
|
;; graft the type onto the class graph so isa?/supers/ancestors see it. A
|
||||||
|
;; bare deftype is an IType; defrecord (which runs register-record-type!
|
||||||
|
;; right after) replaces the row with the record interface set.
|
||||||
|
(jch-set-supers! tag '("clojure.lang.IType"))
|
||||||
|
(hashtable-set! chez-deftype-ctor-tag ctor tag)
|
||||||
;; record the shape for whole-program inference, keyed by the positional
|
;; record the shape for whole-program inference, keyed by the positional
|
||||||
;; ctor var "ns/->Name" the analyzer resolves a (->Name …) call to.
|
;; ctor var "ns/->Name" the analyzer resolves a (->Name …) call to.
|
||||||
(register-record-shape! (string-append (chez-current-ns) "/->" (symbol-t-name name-sym))
|
(register-record-shape! (string-append (chez-current-ns) "/->" (symbol-t-name name-sym))
|
||||||
|
|
@ -664,7 +696,17 @@
|
||||||
(strip-prefix type-name "java.time.")
|
(strip-prefix type-name "java.time.")
|
||||||
(strip-prefix type-name "clojure.lang.")
|
(strip-prefix type-name "clojure.lang.")
|
||||||
type-name)))
|
type-name)))
|
||||||
(and (hashtable-ref host-type-set base #f) base)))
|
;; a host class if the literal set lists it OR the class graph models it — both
|
||||||
|
;; feed value-host-tags (which emits the same bare segment), so a protocol
|
||||||
|
;; extended to any modeled class keys under a tag the value reports. A
|
||||||
|
;; deftype/defrecord is in the graph too (its ancestry), but its VALUES report
|
||||||
|
;; the ns-qualified tag, not the bare segment — so a name that resolves to a
|
||||||
|
;; deftype never canonicalizes through the graph arm.
|
||||||
|
(and (or (hashtable-ref host-type-set base #f)
|
||||||
|
(and (not (hashtable-ref chez-simple-name-tag type-name #f))
|
||||||
|
(not (hashtable-ref chez-deftype-tag-set type-name #f))
|
||||||
|
(or (jch-known? base) (jch-known? type-name))))
|
||||||
|
base)))
|
||||||
;; An extend/extend-type/extend-protocol registration marks the tag as an
|
;; An extend/extend-type/extend-protocol registration marks the tag as an
|
||||||
;; extender of the protocol (recorded inside type-registry so the per-case prune
|
;; extender of the protocol (recorded inside type-registry so the per-case prune
|
||||||
;; restores it). deftype/defrecord inline impls go through register-inline-method
|
;; restores it). deftype/defrecord inline impls go through register-inline-method
|
||||||
|
|
@ -703,6 +745,12 @@
|
||||||
(let ((h (make-hashtable string-hash string=?))) (hashtable-set! type-registry tag h) h))))
|
(let ((h (make-hashtable string-hash string=?))) (hashtable-set! type-registry tag h) h))))
|
||||||
(unless (hashtable-ref ti proto-name #f)
|
(unless (hashtable-ref ti proto-name #f)
|
||||||
(hashtable-set! ti proto-name (make-hashtable string-hash string=?))))
|
(hashtable-set! ti proto-name (make-hashtable string-hash string=?))))
|
||||||
|
;; the protocol's interface joins the type's class ancestry, spelled like the
|
||||||
|
;; JVM interface (munged ns; the defining ns is assumed to be the current one —
|
||||||
|
;; the macro passes only the simple protocol name).
|
||||||
|
(let ((iface (string-append (jch-munge-segments (chez-current-ns)) "." proto-name)))
|
||||||
|
(jch-mark-interface! iface)
|
||||||
|
(jch-register-supers! (string-append (chez-current-ns) "." type-name) (list iface)))
|
||||||
jolt-nil)
|
jolt-nil)
|
||||||
|
|
||||||
;; protocol-resolve: the impl procedure for obj — by record type tag, a reify's
|
;; protocol-resolve: the impl procedure for obj — by record type tag, a reify's
|
||||||
|
|
@ -787,14 +835,9 @@
|
||||||
;; "#<compound condition>".
|
;; "#<compound condition>".
|
||||||
(def-var! "jolt.host" "condition-message"
|
(def-var! "jolt.host" "condition-message"
|
||||||
(lambda (c) (if (condition? c) (condition->message-string c) jolt-nil)))
|
(lambda (c) (if (condition? c) (condition->message-string c) jolt-nil)))
|
||||||
(define (record-method-dispatch obj method-name rest-args)
|
(define (record-method-dispatch-base obj method-name rest-args)
|
||||||
(let ((rest (if (jolt-nil? rest-args) '() (seq->list rest-args))))
|
(let ((rest (if (jolt-nil? rest-args) '() (seq->list rest-args))))
|
||||||
(cond
|
(cond
|
||||||
;; (.getClass x): universal Object method — the class token for any value
|
|
||||||
;; (jolt has no Class objects; the token is the canonical name string, on
|
|
||||||
;; which .getName/.getSimpleName work via the String method shim).
|
|
||||||
((and (string=? method-name "getClass") (not (jrec? obj)) (not (jreify? obj)))
|
|
||||||
(jolt-class obj))
|
|
||||||
((and (jrec? obj) (find-method-any-protocol-arity (jrec-tag obj) method-name (+ 1 (length rest))))
|
((and (jrec? obj) (find-method-any-protocol-arity (jrec-tag obj) method-name (+ 1 (length rest))))
|
||||||
=> (lambda (f) (apply jolt-invoke f obj rest)))
|
=> (lambda (f) (apply jolt-invoke f obj rest)))
|
||||||
;; (.field inst): a deftype/record field read with no matching method.
|
;; (.field inst): a deftype/record field read with no matching method.
|
||||||
|
|
@ -926,6 +969,36 @@
|
||||||
(else (error #f (string-append "No method " method-name " for value: "
|
(else (error #f (string-append "No method " method-name " for value: "
|
||||||
(jolt-pr-str obj)))))))
|
(jolt-pr-str obj)))))))
|
||||||
|
|
||||||
|
;; ---- method-dispatch arm registry ------------------------------------------
|
||||||
|
;; A .method call (record-method-dispatch) is resolved by an ordered list of arms
|
||||||
|
;; (ascending priority), each (obj method-name rest-args) -> result | 'pass.
|
||||||
|
;; This replaces a stack of (set! record-method-dispatch ...) rebindings across
|
||||||
|
;; six files whose precedence was implicit in load order — priority is now
|
||||||
|
;; explicit data. record-method-dispatch-base is the final fallback (the
|
||||||
|
;; string/keyword/symbol/Object-method surface). A host shim / library registers
|
||||||
|
;; an arm with register-method-arm! instead of set!-wrapping the dispatcher.
|
||||||
|
(define method-dispatch-arms '()) ; list of (priority . arm), ascending priority
|
||||||
|
(define (register-method-arm! priority arm)
|
||||||
|
(set! method-dispatch-arms
|
||||||
|
(let ins ((as method-dispatch-arms))
|
||||||
|
(cond ((null? as) (list (cons priority arm)))
|
||||||
|
((< priority (caar as)) (cons (cons priority arm) as))
|
||||||
|
(else (cons (car as) (ins (cdr as))))))))
|
||||||
|
(define (record-method-dispatch obj method-name rest-args)
|
||||||
|
(let loop ((as method-dispatch-arms))
|
||||||
|
(if (null? as)
|
||||||
|
(record-method-dispatch-base obj method-name rest-args)
|
||||||
|
(let ((r ((cdar as) obj method-name rest-args)))
|
||||||
|
(if (eq? r 'pass) (loop (cdr as)) r)))))
|
||||||
|
|
||||||
|
;; (.getClass x): a universal Object method reached by EVERY value before any
|
||||||
|
;; per-type arm — the class token for the value (jolt has no Class objects; the
|
||||||
|
;; token is the canonical name string, on which .getName/.getSimpleName work).
|
||||||
|
;; One arm, so a type arm that only whitelists its own methods can't steal it.
|
||||||
|
(register-method-arm! 5
|
||||||
|
(lambda (obj method-name rest-args)
|
||||||
|
(if (string=? method-name "getClass") (jolt-class obj) 'pass)))
|
||||||
|
|
||||||
;; reify: instance-local method table. obj is a jreify carrying a method ht +
|
;; reify: instance-local method table. obj is a jreify carrying a method ht +
|
||||||
;; the protocol short-names it implements (for satisfies?/instance?).
|
;; the protocol short-names it implements (for satisfies?/instance?).
|
||||||
(define-record-type jreify (fields methods protos) (nongenerative chez-jreify-v1))
|
(define-record-type jreify (fields methods protos) (nongenerative chez-jreify-v1))
|
||||||
|
|
@ -997,8 +1070,18 @@
|
||||||
;; defrecord marks its type a record (deftype does not), keyed by the same
|
;; defrecord marks its type a record (deftype does not), keyed by the same
|
||||||
;; "ns.Name" tag make-deftype-ctor bakes — so jrec-record? distinguishes the two.
|
;; "ns.Name" tag make-deftype-ctor bakes — so jrec-record? distinguishes the two.
|
||||||
(define (register-record-type! name-sym)
|
(define (register-record-type! name-sym)
|
||||||
(hashtable-set! chez-record-type-tbl
|
(let ((tag (string-append (chez-current-ns) "." (symbol-t-name name-sym))))
|
||||||
(string-append (chez-current-ns) "." (symbol-t-name name-sym)) #t)
|
(hashtable-set! chez-record-type-tbl tag #t)
|
||||||
|
;; a defrecord's class ancestry: replace the deftype IType row with the
|
||||||
|
;; record interfaces (their closure supplies Associative/Seqable/ILookup/…),
|
||||||
|
;; keeping any protocol interfaces already grafted by the inline
|
||||||
|
;; registrations that ran between the deftype ctor and this call.
|
||||||
|
(let ((protos (filter (lambda (s) (not (string=? s "clojure.lang.IType")))
|
||||||
|
(jch-direct-supers tag))))
|
||||||
|
(jch-set-supers! tag (append protos
|
||||||
|
'("clojure.lang.IRecord" "clojure.lang.IObj"
|
||||||
|
"clojure.lang.IPersistentMap" "java.util.Map"
|
||||||
|
"clojure.lang.IHashEq" "java.io.Serializable")))))
|
||||||
jolt-nil)
|
jolt-nil)
|
||||||
(def-var! "clojure.core" "register-record-type!" register-record-type!)
|
(def-var! "clojure.core" "register-record-type!" register-record-type!)
|
||||||
(def-var! "clojure.core" "make-protocol" make-protocol)
|
(def-var! "clojure.core" "make-protocol" make-protocol)
|
||||||
|
|
|
||||||
|
|
@ -159,10 +159,23 @@
|
||||||
;; A jolt regex value: the source string (for printing / str) + the compiled
|
;; A jolt regex value: the source string (for printing / str) + the compiled
|
||||||
;; irregex. regex? recognizes it; the printer renders #"source".
|
;; irregex. regex? recognizes it; the printer renders #"source".
|
||||||
(define-record-type regex-t (fields source irx) (nongenerative jolt-regex-v1))
|
(define-record-type regex-t (fields source irx) (nongenerative jolt-regex-v1))
|
||||||
|
;; A capturing pattern is compiled with irregex's BACKTRACKING matcher ('backtrack),
|
||||||
|
;; not its DFA. java.util.regex is itself a leftmost-first backtracking engine, so
|
||||||
|
;; this matches the JVM's submatch semantics; irregex's DFA is POSIX leftmost-longest
|
||||||
|
;; and, worse, leaks a non-participating alternation group's capture (e.g.
|
||||||
|
;; #"(?:([0-9])|([0-9])r([0-9]+))" on "2r11" left group 1 = "2"), which broke
|
||||||
|
;; tools.reader's number reader. Non-capturing patterns keep the fast DFA — with no
|
||||||
|
;; groups to read, its whole-match result is all a caller sees. The count comes from
|
||||||
|
;; a first cheap compile; a capturing pattern is recompiled once (patterns compile
|
||||||
|
;; once and cache in the regex-t).
|
||||||
(define (jolt-regex source)
|
(define (jolt-regex source)
|
||||||
(let-values (((opts pat) (regex-parse-flags source)))
|
(let-values (((opts pat) (regex-parse-flags source)))
|
||||||
|
(let* ((p (translate-prop-classes (escape-class-shorthand-dash pat)))
|
||||||
|
(irx (apply irregex p opts)))
|
||||||
(make-regex-t source
|
(make-regex-t source
|
||||||
(apply irregex (translate-prop-classes (escape-class-shorthand-dash pat)) opts))))
|
(if (> (irregex-num-submatches irx) 0)
|
||||||
|
(apply irregex p 'backtrack opts)
|
||||||
|
irx)))))
|
||||||
(define (jolt-regex? x) (regex-t? x))
|
(define (jolt-regex? x) (regex-t? x))
|
||||||
(define (jolt-re-pattern x) (if (regex-t? x) x (jolt-regex x)))
|
(define (jolt-re-pattern x) (if (regex-t? x) x (jolt-regex x)))
|
||||||
|
|
||||||
|
|
|
||||||
150
host/chez/rt.ss
150
host/chez/rt.ss
|
|
@ -11,6 +11,17 @@
|
||||||
;; 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")
|
||||||
|
|
||||||
|
|
@ -37,16 +48,142 @@
|
||||||
(define (jolt-not x) (if (jolt-truthy? x) #f #t))
|
(define (jolt-not x) (if (jolt-truthy? x) #f #t))
|
||||||
|
|
||||||
;; --- exceptions --------------------------------------------------------------
|
;; --- exceptions --------------------------------------------------------------
|
||||||
;; throw raises the jolt value RAW (no envelope);
|
;; throw raises a Chez condition WRAPPING the jolt value; catch (emitted as
|
||||||
;; catch (emitted as `guard`) binds it directly. Chez `raise` accepts any
|
;; `guard`) and jolt-report-uncaught unwrap it back via jolt-unwrap-throw.
|
||||||
;; object, so a thrown number/map/ex-info all work; uncaught -> non-zero exit.
|
;; Raising the value RAW broke when a throw crossed the host/`eval` boundary:
|
||||||
|
;; Chez re-wrapped the non-condition into a compound condition whose
|
||||||
|
;; message-extraction APPLIES the value (crashing on an empty-map :data ->
|
||||||
|
;; "attempt to apply non-procedure"), and the real message was lost. A real
|
||||||
|
;; condition propagates intact through any number of eval boundaries.
|
||||||
;; Capture the live continuation at the throw site (identity-tagged with the
|
;; Capture the live continuation at the throw site (identity-tagged with the
|
||||||
;; thrown value) so an uncaught error can walk the native frames back to a Clojure
|
;; thrown value) so an uncaught error can walk the native frames back to a Clojure
|
||||||
;; 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
|
||||||
|
make-jolt-throw-condition jolt-throw-condition?
|
||||||
|
(value jolt-throw-condition-value))
|
||||||
|
;; Fallback &message for a leaked condition; the real message always comes from
|
||||||
|
;; the unwrapped value via ex-message.
|
||||||
|
(define (jolt-throw-message v)
|
||||||
|
(if (and (pmap? v)
|
||||||
|
(jolt=2 (jolt-get v jolt-kw-ex-type jolt-nil) jolt-kw-ex-info))
|
||||||
|
(let ((m (jolt-get v jolt-kw-message jolt-nil)))
|
||||||
|
(if (string? m) m "jolt error"))
|
||||||
|
"jolt error"))
|
||||||
(define (jolt-throw v)
|
(define (jolt-throw v)
|
||||||
(call/cc (lambda (k) (jolt-throw-cont (cons v k)) (raise v))))
|
(call/cc (lambda (k)
|
||||||
|
(jolt-throw-cont (cons v k))
|
||||||
|
(raise (condition (make-message-condition (jolt-throw-message v))
|
||||||
|
(make-jolt-throw-condition v))))))
|
||||||
|
(define (jolt-unwrap-throw x)
|
||||||
|
(if (jolt-throw-condition? x) (jolt-throw-condition-value x) x))
|
||||||
;; ex-info builds the tagged map {:jolt/type :jolt/ex-info :message :data :cause}
|
;; ex-info builds the tagged map {:jolt/type :jolt/ex-info :message :data :cause}
|
||||||
;; — a real jolt-hash-map, so the ex-data/ex-message/ex-cause tier fns read it
|
;; — a real jolt-hash-map, so the ex-data/ex-message/ex-cause tier fns read it
|
||||||
;; via jolt-get for free. Arity 2 (msg data) or 3 (msg data cause).
|
;; via jolt-get for free. Arity 2 (msg data) or 3 (msg data cause).
|
||||||
|
|
@ -336,6 +473,11 @@
|
||||||
;; jolt-pr-str (above), and the var-cell machinery — so loaded last.
|
;; jolt-pr-str (above), and the var-cell machinery — so loaded last.
|
||||||
(load "host/chez/multimethods.ss")
|
(load "host/chez/multimethods.ss")
|
||||||
|
|
||||||
|
;; the single JVM class/interface graph — value-host-tags, instance?, isa?/supers,
|
||||||
|
;; and the exception hierarchy all derive from it. Before records.ss so
|
||||||
|
;; value-host-tags can build on jch-tags.
|
||||||
|
(load "host/chez/java/class-hierarchy.ss")
|
||||||
|
|
||||||
;; records + protocols: defrecord/deftype/defprotocol/
|
;; records + protocols: defrecord/deftype/defprotocol/
|
||||||
;; extend-type/reify. A jrec record type set!-extended into the collection
|
;; extend-type/reify. A jrec record type set!-extended into the collection
|
||||||
;; dispatchers + a protocol registry. After multimethods.ss (chez-current-ns) and
|
;; dispatchers + a protocol registry. After multimethods.ss (chez-current-ns) and
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
;; reset between cases so there is no leakage — same isolation a fresh process gives.
|
;; reset between cases so there is no leakage — same isolation a fresh process gives.
|
||||||
;;
|
;;
|
||||||
;; chez --script host/chez/run-corpus.ss
|
;; chez --script host/chez/run-corpus.ss
|
||||||
;; JOLT_CHEZ_ZJ_FLOOR=N override the regression floor (default 2730)
|
;; JOLT_CHEZ_ZJ_FLOOR=N override the regression floor (default 3390)
|
||||||
;; JOLT_CORPUS_LIMIT=N every-Nth stride (fast iteration; floor drops to 0)
|
;; JOLT_CORPUS_LIMIT=N every-Nth stride (fast iteration; floor drops to 0)
|
||||||
;; JOLT_DUMP_CRASH_LABELS=1 list crash + allowlisted labels
|
;; JOLT_DUMP_CRASH_LABELS=1 list crash + allowlisted labels
|
||||||
(import (chezscheme))
|
(import (chezscheme))
|
||||||
|
|
@ -196,7 +196,7 @@
|
||||||
|
|
||||||
;; Regression floor: fail on any NEW divergence or if pass drops below the floor.
|
;; Regression floor: fail on any NEW divergence or if pass drops below the floor.
|
||||||
(define base-floor (let ((s (getenv "JOLT_CHEZ_ZJ_FLOOR")))
|
(define base-floor (let ((s (getenv "JOLT_CHEZ_ZJ_FLOOR")))
|
||||||
(if s (string->number s) 2730)))
|
(if s (string->number s) 3390)))
|
||||||
(define floor (if limit 0 base-floor))
|
(define floor (if limit 0 base-floor))
|
||||||
(when (or (> (length diverged) 0) (< pass floor))
|
(when (or (> (length diverged) 0) (< pass floor))
|
||||||
(printf "REGRESSION: pass ~a < floor ~a or ~a new divergence(s)\n"
|
(printf "REGRESSION: pass ~a < floor ~a or ~a new divergence(s)\n"
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
299
host/chez/seq.ss
299
host/chez/seq.ss
|
|
@ -152,23 +152,224 @@
|
||||||
(if (jolt-nil? s) last (loop (jolt-seq (seq-more s)) (seq-first s)))))
|
(if (jolt-nil? s) last (loop (jolt-seq (seq-more s)) (seq-first s)))))
|
||||||
;; nth over a seq (walks; forces lazily). default? selects the 3-arg behavior.
|
;; nth over a seq (walks; forces lazily). default? selects the 3-arg behavior.
|
||||||
(define (seq-nth coll i default? d)
|
(define (seq-nth coll i default? d)
|
||||||
(if (fx<? i 0) (if default? d (error 'nth "index out of bounds"))
|
(if (fx<? i 0) (if default? d (jolt-throw (jolt-host-throwable "java.lang.IndexOutOfBoundsException" "index out of bounds")))
|
||||||
(let loop ((s (jolt-seq coll)) (i i))
|
(let loop ((s (jolt-seq coll)) (i i))
|
||||||
(cond ((jolt-nil? s) (if default? d (error 'nth "index out of bounds")))
|
(cond ((jolt-nil? s) (if default? d (jolt-throw (jolt-host-throwable "java.lang.IndexOutOfBoundsException" "index out of bounds"))))
|
||||||
((fx=? i 0) (seq-first s))
|
((fx=? i 0) (seq-first s))
|
||||||
(else (loop (jolt-seq (seq-more s)) (fx- i 1)))))))
|
(else (loop (jolt-seq (seq-more s)) (fx- i 1)))))))
|
||||||
|
|
||||||
|
;; --- checked arithmetic: JVM Numbers.ops-style category dispatch -------------
|
||||||
|
;; Every arithmetic/comparison site (the inlined jolt-n* macros in call position,
|
||||||
|
;; the variadic shims in value position) funnels a binary op through ONE dispatch:
|
||||||
|
;; both operands inside Chez's tower take the native op with JVM contagion rules
|
||||||
|
;; patched in (a double operand wins — Chez's exact-zero shortcut must not leak:
|
||||||
|
;; (* 1.5 0) is 0.0, not 0; an exact zero divisor throws ArithmeticException, a
|
||||||
|
;; double zero divisor yields ##Inf/##NaN); an operand OUTSIDE the tower (e.g.
|
||||||
|
;; BigDecimal) falls to a slow hook the numeric shim extends (java/bigdec.ss).
|
||||||
|
;; A non-numeric operand is a ClassCastException, like the JVM.
|
||||||
|
(define (jolt-num-cast-throw x)
|
||||||
|
(if (jolt-nil? x)
|
||||||
|
(jolt-throw (jolt-host-throwable "java.lang.NullPointerException" ""))
|
||||||
|
(jolt-throw (jolt-host-throwable
|
||||||
|
"java.lang.ClassCastException"
|
||||||
|
(string-append "class " (jolt-class-name x)
|
||||||
|
" cannot be cast to class java.lang.Number")))))
|
||||||
|
(define (jolt-div0-throw)
|
||||||
|
(jolt-throw (jolt-host-throwable "java.lang.ArithmeticException" "Divide by zero")))
|
||||||
|
|
||||||
|
;; slow hooks: one per op, taking over when an operand is outside Chez's tower.
|
||||||
|
;; A numeric shim (java/bigdec.ss) set!-extends them; the base case is the JVM's:
|
||||||
|
;; not a number -> ClassCastException. The hooks are BINARY and never re-enter
|
||||||
|
;; the variadic shims, so extension order can't recurse.
|
||||||
|
(define (jolt-add-slow a b) (jolt-num-cast-throw (if (number? a) b a)))
|
||||||
|
(define (jolt-sub-slow a b) (jolt-num-cast-throw (if (number? a) b a)))
|
||||||
|
(define (jolt-mul-slow a b) (jolt-num-cast-throw (if (number? a) b a)))
|
||||||
|
(define (jolt-div-slow a b) (jolt-num-cast-throw (if (number? a) b a)))
|
||||||
|
;; comparison of operands outside the Chez tower: numeric shims extend this to a
|
||||||
|
;; 3-way compare; anything left over is not a number.
|
||||||
|
(define (jolt-num-cmp-slow a b)
|
||||||
|
(jolt-num-cast-throw (if (number? a) b a)))
|
||||||
|
|
||||||
|
(define (jolt-add2 a b)
|
||||||
|
(if (and (number? a) (number? b)) (+ a b) (jolt-add-slow a b)))
|
||||||
|
(define (jolt-sub2 a b)
|
||||||
|
(if (and (number? a) (number? b)) (- a b) (jolt-sub-slow a b)))
|
||||||
|
(define (jolt-mul2 a b)
|
||||||
|
(if (and (number? a) (number? b))
|
||||||
|
(if (or (flonum? a) (flonum? b))
|
||||||
|
(fl* (real->flonum a) (real->flonum b))
|
||||||
|
(* a b))
|
||||||
|
(jolt-mul-slow a b)))
|
||||||
|
(define (jolt-div2 a b)
|
||||||
|
(if (and (number? a) (number? b))
|
||||||
|
(if (or (flonum? a) (flonum? b))
|
||||||
|
(fl/ (real->flonum a) (real->flonum b))
|
||||||
|
(if (eqv? b 0) (jolt-div0-throw) (/ a b)))
|
||||||
|
(jolt-div-slow a b)))
|
||||||
|
(define (jolt-lt2 a b)
|
||||||
|
(if (and (number? a) (number? b)) (< a b) (< (jolt-num-cmp-slow a b) 0)))
|
||||||
|
(define (jolt-gt2 a b)
|
||||||
|
(if (and (number? a) (number? b)) (> a b) (> (jolt-num-cmp-slow a b) 0)))
|
||||||
|
(define (jolt-le2 a b)
|
||||||
|
(if (and (number? a) (number? b)) (<= a b) (<= (jolt-num-cmp-slow a b) 0)))
|
||||||
|
(define (jolt-ge2 a b)
|
||||||
|
(if (and (number? a) (number? b)) (>= a b) (>= (jolt-num-cmp-slow a b) 0)))
|
||||||
|
;; min/max return the ORIGINAL operand (type and exactness kept, like
|
||||||
|
;; Numbers.min): (min 1 2.0) is 1, not 1.0. A NaN operand wins.
|
||||||
|
(define (jolt-min2 a b)
|
||||||
|
(cond ((and (flonum? a) (nan? a)) a)
|
||||||
|
((and (flonum? b) (nan? b)) b)
|
||||||
|
(else (if (jolt-lt2 a b) a b))))
|
||||||
|
(define (jolt-max2 a b)
|
||||||
|
(cond ((and (flonum? a) (nan? a)) a)
|
||||||
|
((and (flonum? b) (nan? b)) b)
|
||||||
|
(else (if (jolt-gt2 a b) a b))))
|
||||||
|
|
||||||
|
;; quot/rem/mod over the full tower: truncating division; a double operand makes
|
||||||
|
;; the result a double; mod has floor semantics (result takes the divisor's
|
||||||
|
;; sign). A zero divisor throws ArithmeticException in both worlds (JVM double
|
||||||
|
;; quot/rem check the divisor before dividing). Non-tower operands hit the
|
||||||
|
;; set!-extensible slow hooks.
|
||||||
|
(define (jolt-quot-slow a b) (jolt-num-cast-throw (if (number? a) b a)))
|
||||||
|
(define (jolt-rem-slow a b) (jolt-num-cast-throw (if (number? a) b a)))
|
||||||
|
(define (jolt-mod-slow a b) (jolt-num-cast-throw (if (number? a) b a)))
|
||||||
|
(define (jolt-quot a b)
|
||||||
|
(cond ((not (and (number? a) (number? b))) (jolt-quot-slow a b))
|
||||||
|
((or (flonum? a) (flonum? b))
|
||||||
|
(let ((n (real->flonum a)) (d (real->flonum b)))
|
||||||
|
(if (fl= d 0.0) (jolt-div0-throw)
|
||||||
|
(let ((q (fl/ n d)))
|
||||||
|
(when (or (nan? q) (infinite? q))
|
||||||
|
(jolt-throw (jolt-host-throwable "java.lang.NumberFormatException"
|
||||||
|
"Infinite or NaN")))
|
||||||
|
(fltruncate q)))))
|
||||||
|
((eqv? b 0) (jolt-div0-throw))
|
||||||
|
((and (integer? a) (integer? b)) (quotient a b))
|
||||||
|
(else (truncate (/ a b)))))
|
||||||
|
(define (jolt-rem a b)
|
||||||
|
(cond ((not (and (number? a) (number? b))) (jolt-rem-slow a b))
|
||||||
|
((or (flonum? a) (flonum? b))
|
||||||
|
(let ((n (real->flonum a)) (d (real->flonum b)))
|
||||||
|
(if (fl= d 0.0) (jolt-div0-throw)
|
||||||
|
(let ((q (fl/ n d)))
|
||||||
|
(when (or (nan? q) (infinite? q))
|
||||||
|
(jolt-throw (jolt-host-throwable "java.lang.NumberFormatException"
|
||||||
|
"Infinite or NaN")))
|
||||||
|
(fl- n (fl* d (fltruncate q)))))))
|
||||||
|
((eqv? b 0) (jolt-div0-throw))
|
||||||
|
((and (integer? a) (integer? b)) (remainder a b))
|
||||||
|
(else (- a (* b (truncate (/ a b)))))))
|
||||||
|
(define (jolt-mod a b)
|
||||||
|
(cond ((not (and (number? a) (number? b))) (jolt-mod-slow a b))
|
||||||
|
((and (integer? a) (integer? b) (not (flonum? a)) (not (flonum? b)))
|
||||||
|
(if (eqv? b 0) (jolt-div0-throw) (modulo a b)))
|
||||||
|
(else
|
||||||
|
(let ((m (jolt-rem a b)))
|
||||||
|
(if (or (zero? m) (eq? (negative? m) (negative? b))) m (jolt-add2 m b))))))
|
||||||
|
|
||||||
;; value-position arithmetic (the higher-order forms: (reduce + []), (apply * xs)).
|
;; value-position arithmetic (the higher-order forms: (reduce + []), (apply * xs)).
|
||||||
;; Scheme's +/-/*// already implement the JVM-parity numeric tower: exact+exact ->
|
;; Folded through the binary dispatch so contagion/edge rules hold; identities
|
||||||
;; exact, exact/exact -> Ratio, any flonum -> flonum. Identities (+)=0 / (*)=1 are
|
;; (+)=0 / (*)=1 are exact, matching exact integer arithmetic. The hot path uses
|
||||||
;; exact, matching exact integer arithmetic. The hot path uses the inlined native
|
;; the inlined native ops, not these.
|
||||||
;; ops, not these.
|
;; recognizer for slow-path numeric types; numeric shims extend it.
|
||||||
(define (jolt-add . xs) (apply + xs))
|
(define (jolt-num-slow? x) #f)
|
||||||
(define (jolt-sub . xs) (apply - xs))
|
(define (jolt-num-check1 x) ; (+ x)/(* x) return x but still type-check it
|
||||||
(define (jolt-mul . xs) (apply * xs))
|
(if (or (number? x) (jolt-num-slow? x)) x (jolt-num-cast-throw x)))
|
||||||
(define (jolt-div . xs) (apply / xs))
|
(define (jolt-add . xs)
|
||||||
(define (jolt-min . xs) (apply min xs))
|
(cond ((null? xs) 0)
|
||||||
(define (jolt-max . xs) (apply max xs))
|
((null? (cdr xs)) (jolt-num-check1 (car xs)))
|
||||||
|
(else (fold-left jolt-add2 (car xs) (cdr xs)))))
|
||||||
|
(define (jolt-arity0-throw name)
|
||||||
|
(jolt-throw (jolt-host-throwable
|
||||||
|
"clojure.lang.ArityException"
|
||||||
|
(string-append "Wrong number of args (0) passed to: clojure.core/" name))))
|
||||||
|
(define (jolt-sub . xs)
|
||||||
|
(cond ((null? xs) (jolt-arity0-throw "-"))
|
||||||
|
((null? (cdr xs)) (jolt-sub2 0 (car xs)))
|
||||||
|
(else (fold-left jolt-sub2 (car xs) (cdr xs)))))
|
||||||
|
(define (jolt-mul . xs)
|
||||||
|
(cond ((null? xs) 1)
|
||||||
|
((null? (cdr xs)) (jolt-num-check1 (car xs)))
|
||||||
|
(else (fold-left jolt-mul2 (car xs) (cdr xs)))))
|
||||||
|
(define (jolt-div . xs)
|
||||||
|
(cond ((null? xs) (jolt-arity0-throw "/"))
|
||||||
|
((null? (cdr xs)) (jolt-div2 1 (car xs)))
|
||||||
|
(else (fold-left jolt-div2 (car xs) (cdr xs)))))
|
||||||
|
(define (jolt-min x . xs) (fold-left jolt-min2 x xs))
|
||||||
|
(define (jolt-max x . xs) (fold-left jolt-max2 x xs))
|
||||||
|
;; variadic comparison chains for value position ((apply < xs)).
|
||||||
|
(define (jolt-cmp-chain op2)
|
||||||
|
(lambda (x . xs)
|
||||||
|
(let loop ((a x) (rest xs))
|
||||||
|
(cond ((null? rest) #t)
|
||||||
|
((op2 a (car rest)) (loop (car rest) (cdr rest)))
|
||||||
|
(else #f)))))
|
||||||
|
(define jolt-lt (jolt-cmp-chain jolt-lt2))
|
||||||
|
(define jolt-gt (jolt-cmp-chain jolt-gt2))
|
||||||
|
(define jolt-le (jolt-cmp-chain jolt-le2))
|
||||||
|
(define jolt-ge (jolt-cmp-chain jolt-ge2))
|
||||||
|
|
||||||
|
;; call-position arithmetic: inlined macros with the both-Chez-numbers fast path
|
||||||
|
;; open-coded; anything else falls to the binary dispatch above. Comparisons
|
||||||
|
;; return a genuine Scheme boolean (the backend's truthy elision relies on it).
|
||||||
|
(define-syntax jolt-n+
|
||||||
|
(syntax-rules ()
|
||||||
|
((_) 0)
|
||||||
|
((_ a) (jolt-add a))
|
||||||
|
((_ ea eb) (let ((a ea) (b eb))
|
||||||
|
(if (and (number? a) (number? b)) (+ a b) (jolt-add a b))))
|
||||||
|
((_ a b c ...) (jolt-n+ (jolt-n+ a b) c ...))))
|
||||||
|
(define-syntax jolt-n-
|
||||||
|
(syntax-rules ()
|
||||||
|
((_) (jolt-sub))
|
||||||
|
((_ a) (jolt-sub a))
|
||||||
|
((_ ea eb) (let ((a ea) (b eb))
|
||||||
|
(if (and (number? a) (number? b)) (- a b) (jolt-sub a b))))
|
||||||
|
((_ a b c ...) (jolt-n- (jolt-n- a b) c ...))))
|
||||||
|
(define-syntax jolt-n*
|
||||||
|
(syntax-rules ()
|
||||||
|
((_) 1)
|
||||||
|
((_ a) (jolt-mul a))
|
||||||
|
((_ ea eb) (let ((a ea) (b eb))
|
||||||
|
(if (and (number? a) (number? b))
|
||||||
|
(if (or (flonum? a) (flonum? b))
|
||||||
|
(fl* (real->flonum a) (real->flonum b))
|
||||||
|
(* a b))
|
||||||
|
(jolt-mul a b))))
|
||||||
|
((_ a b c ...) (jolt-n* (jolt-n* a b) c ...))))
|
||||||
|
(define-syntax jolt-n-div
|
||||||
|
(syntax-rules ()
|
||||||
|
((_) (jolt-div))
|
||||||
|
((_ a) (jolt-div a))
|
||||||
|
((_ a b) (jolt-div2 a b))
|
||||||
|
((_ a b c ...) (jolt-n-div (jolt-div2 a b) c ...))))
|
||||||
|
(define-syntax define-n-cmp
|
||||||
|
(syntax-rules ()
|
||||||
|
((_ name op op2)
|
||||||
|
(define-syntax name
|
||||||
|
(syntax-rules ()
|
||||||
|
((_) (op2))
|
||||||
|
((_ a) (begin a #t))
|
||||||
|
((_ ea eb) (let ((a ea) (b eb))
|
||||||
|
(if (and (number? a) (number? b)) (op a b) (op2 a b))))
|
||||||
|
((_ ea eb c (... ...)) (let ((a ea) (b eb))
|
||||||
|
(and (name a b) (name b c (... ...))))))))))
|
||||||
|
(define-n-cmp jolt-n< < jolt-lt2)
|
||||||
|
(define-n-cmp jolt-n> > jolt-gt2)
|
||||||
|
(define-n-cmp jolt-n<= <= jolt-le2)
|
||||||
|
(define-n-cmp jolt-n>= >= jolt-ge2)
|
||||||
|
(define-syntax jolt-n-min
|
||||||
|
(syntax-rules ()
|
||||||
|
((_) (jolt-min))
|
||||||
|
((_ a) (jolt-min a))
|
||||||
|
((_ a b) (jolt-min2 a b))
|
||||||
|
((_ a b c ...) (jolt-n-min (jolt-min2 a b) c ...))))
|
||||||
|
(define-syntax jolt-n-max
|
||||||
|
(syntax-rules ()
|
||||||
|
((_) (jolt-max))
|
||||||
|
((_ a) (jolt-max a))
|
||||||
|
((_ a b) (jolt-max2 a b))
|
||||||
|
((_ a b c ...) (jolt-n-max (jolt-max2 a b) c ...))))
|
||||||
|
|
||||||
;; --- unchecked (Java long) arithmetic: wrap to signed 64 bits ----------------
|
;; --- unchecked (Java long) arithmetic: wrap to signed 64 bits ----------------
|
||||||
;; Clojure's unchecked-* (and +/-/* under *unchecked-math*) are long ops that
|
;; Clojure's unchecked-* (and +/-/* under *unchecked-math*) are long ops that
|
||||||
|
|
@ -244,11 +445,26 @@
|
||||||
;; can't statically resolve to a procedure (a keyword/coll/proc held in a local)
|
;; can't statically resolve to a procedure (a keyword/coll/proc held in a local)
|
||||||
;; routes here. Off the arithmetic/self-recursion hot path by construction.
|
;; routes here. Off the arithmetic/self-recursion hot path by construction.
|
||||||
;; ============================================================================
|
;; ============================================================================
|
||||||
|
;; (pred . handler) arms making a host type invocable; handler gets (f args).
|
||||||
|
(define jolt-invoke-arms '())
|
||||||
|
(define (register-invoke-arm! pred handler)
|
||||||
|
(set! jolt-invoke-arms (cons (cons pred handler) jolt-invoke-arms)))
|
||||||
|
(define (jolt-invoke-arm-for f)
|
||||||
|
(let loop ((as jolt-invoke-arms))
|
||||||
|
(cond ((null? as) #f)
|
||||||
|
(((caar as) f) (cdar as))
|
||||||
|
(else (loop (cdr as))))))
|
||||||
|
|
||||||
(define (jolt-invoke f . args)
|
(define (jolt-invoke f . args)
|
||||||
(cond
|
(cond
|
||||||
((procedure? f) (apply f args))
|
((procedure? f) (apply f args))
|
||||||
((keyword? f) (apply jolt-get (car args) f (cdr args))) ; (:k m [d]) -> (get m :k [d])
|
((keyword? f) (apply jolt-get (car args) f (cdr args))) ; (:k m [d]) -> (get m :k [d])
|
||||||
((jolt-symbol? f) (apply jolt-get (car args) f (cdr args))) ; ('s m [d]) -> (get m 's [d])
|
((jolt-symbol? f) (apply jolt-get (car args) f (cdr args))) ; ('s m [d]) -> (get m 's [d])
|
||||||
|
;; a VECTOR invokes as nth (a bad index throws, like IPersistentVector.invoke);
|
||||||
|
;; maps and sets invoke as get.
|
||||||
|
((pvec? f) (if (and (pair? args) (null? (cdr args)))
|
||||||
|
(jolt-nth f (car args))
|
||||||
|
(apply jolt-get f args)))
|
||||||
((jolt-coll? f) (apply jolt-get f args)) ; (coll k [d]) -> (get coll k [d])
|
((jolt-coll? f) (apply jolt-get f args)) ; (coll k [d]) -> (get coll k [d])
|
||||||
((jolt-transient? f) (apply jolt-get f args)) ; a transient vec/map/set is callable on the JVM
|
((jolt-transient? f) (apply jolt-get f args)) ; a transient vec/map/set is callable on the JVM
|
||||||
;; a record/reify implementing clojure.lang.IFn is callable: dispatch to its
|
;; a record/reify implementing clojure.lang.IFn is callable: dispatch to its
|
||||||
|
|
@ -257,12 +473,21 @@
|
||||||
=> (lambda (m) (apply jolt-invoke m f args)))
|
=> (lambda (m) (apply jolt-invoke m f args)))
|
||||||
((and (reified-methods f) (hashtable-ref (reified-methods f) "invoke" #f))
|
((and (reified-methods f) (hashtable-ref (reified-methods f) "invoke" #f))
|
||||||
=> (lambda (m) (apply jolt-invoke m f args)))
|
=> (lambda (m) (apply jolt-invoke m f args)))
|
||||||
;; calling a non-fn: a ClassCastException naming the operator, thrown via
|
;; host types registered as callable (promise delivers, …): consulted only
|
||||||
;; jolt-throw so it is catchable and carries the throw-site continuation for a
|
;; after every built-in case missed, so the hot dispatch pays nothing.
|
||||||
;; stack trace.
|
((jolt-invoke-arm-for f) => (lambda (h) (h f args)))
|
||||||
|
;; calling a non-fn: a ClassCastException naming the operator's CLASS (like
|
||||||
|
;; the JVM's "class clojure.lang.LazySeq cannot be cast to ... IFn" — never
|
||||||
|
;; the value, whose printed form may be unbounded: ((range)) must throw, not
|
||||||
|
;; hang rendering an infinite seq). Thrown via jolt-throw so it is catchable
|
||||||
|
;; and carries the throw-site continuation for a stack trace.
|
||||||
(else (jolt-throw (jolt-host-throwable "java.lang.ClassCastException"
|
(else (jolt-throw (jolt-host-throwable "java.lang.ClassCastException"
|
||||||
(string-append (guard (e (#t "value")) (jolt-pr-str f))
|
(string-append
|
||||||
" cannot be cast to clojure.lang.IFn"))))))
|
"class "
|
||||||
|
(guard (e (#t "value"))
|
||||||
|
(let ((c (jolt-class-name f)))
|
||||||
|
(if (string? c) c (jolt-pr-str f))))
|
||||||
|
" cannot be cast to class clojure.lang.IFn"))))))
|
||||||
|
|
||||||
;; ============================================================================
|
;; ============================================================================
|
||||||
;; chunked-seq accessors — the host side of the Clojure IChunkedSeq contract
|
;; chunked-seq accessors — the host side of the Clojure IChunkedSeq contract
|
||||||
|
|
@ -421,11 +646,11 @@
|
||||||
(if (jolt-nil? s) (jolt-invoke f) ; (reduce f []) -> (f)
|
(if (jolt-nil? s) (jolt-invoke f) ; (reduce f []) -> (f)
|
||||||
(reduce-seq f (seq-first s) (jolt-seq (seq-more s))))))
|
(reduce-seq f (seq-first s) (jolt-seq (seq-more s))))))
|
||||||
((f init coll)
|
((f init coll)
|
||||||
;; IReduceInit: a reify/record with its own `reduce` method drives the
|
;; IReduceInit: a deftype/record OR reify with its own `reduce` method drives
|
||||||
;; reduction (reduce f init (reify clojure.lang.IReduceInit (reduce [_ f i] ...))).
|
;; the reduction, e.g. (reduce f init (reify clojure.lang.IReduceInit
|
||||||
|
;; (reduce [_ f i] ...))) or the same on a deftype.
|
||||||
(cond
|
(cond
|
||||||
((and (jreify? coll) (reified-methods coll)
|
((iface-method coll "reduce" 3)
|
||||||
(hashtable-ref (reified-methods coll) "reduce" #f))
|
|
||||||
=> (lambda (m) (let ((r (jolt-invoke m coll f init)))
|
=> (lambda (m) (let ((r (jolt-invoke m coll f init)))
|
||||||
(if (jolt-reduced? r) (jolt-reduced-val r) r))))
|
(if (jolt-reduced? r) (jolt-reduced-val r) r))))
|
||||||
(else (reduce-seq f init (jolt-seq coll)))))))
|
(else (reduce-seq f init (jolt-seq coll)))))))
|
||||||
|
|
@ -436,8 +661,14 @@
|
||||||
;; falls back to a copy-on-write wrapper for other targets (lists, sorted colls,
|
;; falls back to a copy-on-write wrapper for other targets (lists, sorted colls,
|
||||||
;; nil), so those keep the old per-step jolt-conj behaviour.
|
;; nil), so those keep the old per-step jolt-conj behaviour.
|
||||||
(define (jolt-into to from)
|
(define (jolt-into to from)
|
||||||
|
;; only an editable collection rides the transient path; anything else
|
||||||
|
;; (PersistentQueue, sorted colls, seqs) folds through conj, like RT's
|
||||||
|
;; instanceof IEditableCollection split.
|
||||||
|
(if (or (pvec? to) (pmap? to) (pset? to))
|
||||||
(meta-carry to
|
(meta-carry to
|
||||||
(jolt-persistent! (reduce-seq (lambda (t x) (jolt-conj! t x)) (jolt-transient-new to) (jolt-seq from)))))
|
(jolt-persistent! (reduce-seq (lambda (t x) (jolt-conj! t x)) (jolt-transient-new to) (jolt-seq from))))
|
||||||
|
(meta-carry to
|
||||||
|
(reduce-seq (lambda (acc x) (jolt-conj1 acc x)) to (jolt-seq from)))))
|
||||||
|
|
||||||
(define (range-from n) (cseq-lazy n (lambda () (range-from (+ n 1)))))
|
(define (range-from n) (cseq-lazy n (lambda () (range-from (+ n 1)))))
|
||||||
;; A bounded range is a real chunked-seq, like clojure.lang.LongRange: eager, with
|
;; A bounded range is a real chunked-seq, like clojure.lang.LongRange: eager, with
|
||||||
|
|
@ -542,8 +773,14 @@
|
||||||
;; Parity over the full integer range (JVM even?/odd? accept any integer,
|
;; Parity over the full integer range (JVM even?/odd? accept any integer,
|
||||||
;; bignums included); a fixnum-only fxand crashes on a large value (e.g. a hash).
|
;; bignums included); a fixnum-only fxand crashes on a large value (e.g. a hash).
|
||||||
(define (parity-int n) (if (flonum? n) (exact (floor n)) n))
|
(define (parity-int n) (if (flonum? n) (exact (floor n)) n))
|
||||||
(define (jolt-even? n) (even? (parity-int n)))
|
(define (jolt-parity-check n)
|
||||||
(define (jolt-odd? n) (odd? (parity-int n)))
|
(unless (and (number? n) (exact? n) (integer? n))
|
||||||
|
(jolt-throw (jolt-host-throwable
|
||||||
|
"java.lang.IllegalArgumentException"
|
||||||
|
(string-append "Argument must be an integer: "
|
||||||
|
(guard (e (#t "?")) (jolt-str n)))))))
|
||||||
|
(define (jolt-even? n) (jolt-parity-check n) (even? (parity-int n)))
|
||||||
|
(define (jolt-odd? n) (jolt-parity-check n) (odd? (parity-int n)))
|
||||||
(define (jolt-pos? n) (> n 0))
|
(define (jolt-pos? n) (> n 0))
|
||||||
(define (jolt-neg? n) (< n 0))
|
(define (jolt-neg? n) (< n 0))
|
||||||
(define (jolt-zero? n) (= n 0))
|
(define (jolt-zero? n) (= n 0))
|
||||||
|
|
@ -552,8 +789,18 @@
|
||||||
;; ============================================================================
|
;; ============================================================================
|
||||||
;; keys / vals — return seqs (nil on the empty map), HAMT-iteration order
|
;; keys / vals — return seqs (nil on the empty map), HAMT-iteration order
|
||||||
;; ============================================================================
|
;; ============================================================================
|
||||||
(define (jolt-keys m) (if (jolt-nil? m) jolt-nil (list->cseq (pmap-fold m (lambda (k v a) (cons k a)) '()))))
|
;; keys/vals of anything empty is nil (RT.keys over a nil seq); a non-empty
|
||||||
(define (jolt-vals m) (if (jolt-nil? m) jolt-nil (list->cseq (pmap-fold m (lambda (k v a) (cons v a)) '()))))
|
;; non-map still fails (its elements are not MapEntries).
|
||||||
|
(define (jolt-keys m)
|
||||||
|
(cond ((jolt-nil? m) jolt-nil)
|
||||||
|
((pmap? m) (list->cseq (pmap-fold m (lambda (k v a) (cons k a)) '())))
|
||||||
|
((jolt-nil? (jolt-seq m)) jolt-nil)
|
||||||
|
(else (list->cseq (pmap-fold m (lambda (k v a) (cons k a)) '())))))
|
||||||
|
(define (jolt-vals m)
|
||||||
|
(cond ((jolt-nil? m) jolt-nil)
|
||||||
|
((pmap? m) (list->cseq (pmap-fold m (lambda (k v a) (cons v a)) '())))
|
||||||
|
((jolt-nil? (jolt-seq m)) jolt-nil)
|
||||||
|
(else (list->cseq (pmap-fold m (lambda (k v a) (cons v a)) '())))))
|
||||||
|
|
||||||
;; ============================================================================
|
;; ============================================================================
|
||||||
;; sequential equality + hash (hooks called from values.ss / collections.ss);
|
;; sequential equality + hash (hooks called from values.ss / collections.ss);
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,39 @@ 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'
|
||||||
|
|
@ -43,9 +76,70 @@ check '(deref (future (+ 1 2)))' '3'
|
||||||
check '(/ 1 2)' '1/2'
|
check '(/ 1 2)' '1/2'
|
||||||
check '(= 3 3.0)' 'false'
|
check '(= 3 3.0)' 'false'
|
||||||
check '(== 3 3.0)' 'true'
|
check '(== 3 3.0)' 'true'
|
||||||
|
# a deftype whose simple name collides with a built-in host class must not shadow
|
||||||
|
# the java class: (java.io.PushbackReader. …) still builds the java reader (has
|
||||||
|
# .read), while the bare name in the deftype's own ns is the deftype. (Fresh -e
|
||||||
|
# process per check, so the deftype doesn't leak.)
|
||||||
|
check '(do (deftype PushbackReader [x]) (.read (java.io.PushbackReader. (java.io.StringReader. "A") 1)))' '65'
|
||||||
|
check '(do (deftype PushbackReader [x]) (.-x (PushbackReader. 42)))' '42'
|
||||||
check_loc '(throw (ex-info "boom" {}))' ' at 1:'
|
check_loc '(throw (ex-info "boom" {}))' ' at 1:'
|
||||||
|
|
||||||
|
# A throw that crosses the eval boundary (eval / load-string) must surface its
|
||||||
|
# ex-info :message, not Chez's "attempt to apply non-procedure" noise from
|
||||||
|
# re-wrapping a raw value raised through `eval`.
|
||||||
|
check '(try (eval (read-string "(throw (ex-info \"boom\" {}))")) (catch :default e (ex-message e)))' 'boom'
|
||||||
|
check '(try (load-string "(+") (catch :default e (ex-message e)))' 'EOF while reading'
|
||||||
|
# An uncaught throw prints the ex-info message alongside its source location.
|
||||||
|
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
|
||||||
|
|
@ -67,5 +161,103 @@ else
|
||||||
fails=$((fails + 1))
|
fails=$((fails + 1))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# A data reader that returns a CODE form (deps.edn data_readers.clj -> reader fn)
|
||||||
|
# must have its result spliced in and COMPILED, like Clojure — #code [:x] becomes
|
||||||
|
# (+ 40 2) and evaluates to 42, not the literal list. A project run so the source
|
||||||
|
# root's data_readers.clj is picked up.
|
||||||
|
dr_out="$(JOLT_PWD="$root/test/chez/datareader-app" bin/joltc run -m drtest.main 2>/dev/null | tail -1)"
|
||||||
|
if [ "$dr_out" = "42" ]; then
|
||||||
|
pass=$((pass + 1))
|
||||||
|
else
|
||||||
|
echo " FAIL: code-returning data reader (#code) not compiled — got \`$dr_out\`, want 42"
|
||||||
|
fails=$((fails + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A required namespace's own :as aliases must not leak into the requirer: fix.main
|
||||||
|
# aliases clojure.string as ss and requires fix.lib (which aliases clojure.set as
|
||||||
|
# ss); (ss/upper-case "hi") in main must stay clojure.string -> "HI #{1 2}".
|
||||||
|
al_out="$(JOLT_PWD="$root/test/chez/alias-leak-app" bin/joltc run -m fix.main 2>/dev/null | tail -1)"
|
||||||
|
if [ "$al_out" = "HI #{1 2}" ]; then
|
||||||
|
pass=$((pass + 1))
|
||||||
|
else
|
||||||
|
echo " FAIL: a loaded ns's alias leaked into its requirer — got \`$al_out\`, want \`HI #{1 2}\`"
|
||||||
|
fails=$((fails + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Unit-checks the REPL read-until-complete predicate over balanced/unbalanced,
|
||||||
|
# string, comment and regex-literal inputs. A multi-form `joltc run` so jolt.main
|
||||||
|
# is loaded and its private var resolves; the file self-checks and prints a sentinel.
|
||||||
|
rr_out="$(bin/joltc run test/chez/repl-reader-test.clj 2>/dev/null)"
|
||||||
|
if printf '%s' "$rr_out" | grep -q 'REPL-READER OK'; then
|
||||||
|
pass=$((pass + 1))
|
||||||
|
else
|
||||||
|
echo " FAIL: repl-form-complete? predicate"
|
||||||
|
echo " $(printf '%s' "$rr_out" | grep REPL-READER | tail -1)"
|
||||||
|
fails=$((fails + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# REPL must exit on :repl/quit / :exit — a reliable exit that works in any
|
||||||
|
# terminal, unlike ^D (which some terminals/editors don't deliver as EOF).
|
||||||
|
# Pipe: an evaluable form, the quit keyword, then a sentinel that must NOT run.
|
||||||
|
repl_out="$(printf '(+ 1000 23)\n:repl/quit\n(* 999 9)\n' | bin/joltc repl 2>/dev/null)"
|
||||||
|
if printf '%s' "$repl_out" | grep -q '1023' && ! printf '%s' "$repl_out" | grep -q '8991'; then
|
||||||
|
pass=$((pass + 1))
|
||||||
|
else
|
||||||
|
echo " FAIL: repl should exit on :repl/quit before later forms"
|
||||||
|
printf '%s\n' "$repl_out" | sed 's/^/ | /'
|
||||||
|
fails=$((fails + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
repl_out="$(printf '(- 2024 1)\n:exit\n(* 999 9)\n' | bin/joltc repl 2>/dev/null)"
|
||||||
|
if printf '%s' "$repl_out" | grep -q '2023' && ! printf '%s' "$repl_out" | grep -q '8991'; then
|
||||||
|
pass=$((pass + 1))
|
||||||
|
else
|
||||||
|
echo " FAIL: repl should exit on :exit before later forms"
|
||||||
|
printf '%s\n' "$repl_out" | sed 's/^/ | /'
|
||||||
|
fails=$((fails + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A form split across lines is accumulated and evaluated once complete, with a
|
||||||
|
# secondary continuation prompt before each continued line.
|
||||||
|
repl_out="$(printf '(+ 1\n2)\n:exit\n' | bin/joltc repl 2>/dev/null)"
|
||||||
|
if printf '%s' "$repl_out" | grep -q '3' && ! printf '%s' "$repl_out" | grep -q 'error'; then
|
||||||
|
pass=$((pass + 1))
|
||||||
|
else
|
||||||
|
echo " FAIL: repl should accumulate multi-line forms to 3"
|
||||||
|
printf '%s\n' "$repl_out" | sed 's/^/ | /'
|
||||||
|
fails=$((fails + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A single-line regex literal is complete on its own — the #" opens a regex whose
|
||||||
|
# body (delimiters, quotes and all) must not be miscounted as unbalanced parens.
|
||||||
|
repl_out="$(printf '(re-find #"(a)(b)" "ab")\n:exit\n' | bin/joltc repl 2>/dev/null)"
|
||||||
|
if printf '%s' "$repl_out" | grep -q 'ab' && ! printf '%s' "$repl_out" | grep -q 'error'; then
|
||||||
|
pass=$((pass + 1))
|
||||||
|
else
|
||||||
|
echo " FAIL: repl should evaluate a one-line regex literal, not wait for more input"
|
||||||
|
printf '%s\n' "$repl_out" | sed 's/^/ | /'
|
||||||
|
fails=$((fails + 1))
|
||||||
|
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 ]
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,13 @@
|
||||||
;; The continuation to walk for an uncaught value: the one jolt-throw captured for
|
;; The continuation to walk for an uncaught value: the one jolt-throw captured for
|
||||||
;; THIS value (identity-tagged via jolt-throw-cont, so a stale entry from an
|
;; THIS value (identity-tagged via jolt-throw-cont, so a stale entry from an
|
||||||
;; earlier caught throw is never reused), else a host condition's own
|
;; earlier caught throw is never reused), else a host condition's own
|
||||||
;; &continuation, else #f.
|
;; &continuation, else #f. raw may arrive as the &jolt-throw condition wrapping
|
||||||
(define (jolt-error-continuation v)
|
;; the value (the built-binary launcher hands jolt-report-throwable the guard's
|
||||||
(let ((tc (jolt-throw-cont)))
|
;; raw value) or already unwrapped (the cli unwraps first); unwrap here so the
|
||||||
|
;; identity match holds either way.
|
||||||
|
(define (jolt-error-continuation raw)
|
||||||
|
(let* ((v (jolt-unwrap-throw raw))
|
||||||
|
(tc (jolt-throw-cont)))
|
||||||
(cond
|
(cond
|
||||||
((and (pair? tc) (eq? (car tc) v)) (cdr tc))
|
((and (pair? tc) (eq? (car tc) v)) (cdr tc))
|
||||||
((and (condition? v) (continuation-condition? v)) (condition-continuation v))
|
((and (condition? v) (continuation-condition? v)) (condition-continuation v))
|
||||||
|
|
@ -53,10 +57,36 @@
|
||||||
((symbol? nm) (symbol->string nm))
|
((symbol? nm) (symbol->string nm))
|
||||||
(else #f)))))))
|
(else #f)))))))
|
||||||
|
|
||||||
;; Walk a continuation, returning the registered jolt frames (innermost first) as
|
;; Frame names that are pure Chez / jolt-runtime plumbing — the eval boundary,
|
||||||
;; (frame-name . record) pairs, where record is #(ns name file line) or the symbol
|
;; the var-cell trampoline, continuation/winder internals. They carry no Clojure
|
||||||
;; 'ambiguous. Unmapped frames (host spine, anonymous lambdas) are skipped; raw
|
;; meaning, so an unmapped frame with one of these names is dropped from the trace
|
||||||
;; depth is capped.
|
;; (a MAPPED frame is always kept — a jolt fn that happens to share the name still
|
||||||
|
;; 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.
|
||||||
|
|
@ -66,26 +96,32 @@
|
||||||
(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*" "") "\n")
|
(display (string-append " [frame] " nm (if src " *MAPPED*"
|
||||||
|
(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 src (cons (cons nm src) acc) acc))))))))
|
(if keep? (cons (cons nm src) acc) acc))))))))
|
||||||
|
|
||||||
;; Multi-line backtrace for an uncaught value — " ns/name (file:line)" for a
|
;; Render a list of (frame-name . record) pairs (innermost/deepest first) to a
|
||||||
;; mapped frame, the bare frame name for an ambiguous one — or #f when no jolt
|
;; backtrace string. record is a source vector #(ns name file line) -> "ns/name
|
||||||
;; frame maps (the caller then prints just the top-level location). Capped to the
|
;; (file:line)", or 'ambiguous / #f -> the bare frame name. A run of the same
|
||||||
;; innermost frames.
|
;; frame-name collapses to one "name (xN)" line (deep recursion, or a hot fn a
|
||||||
(define (jolt-backtrace-string v)
|
;; loop re-enters), and the number of distinct lines is capped.
|
||||||
(let ((k (jolt-error-continuation v)))
|
(define (jolt-render-recs recs)
|
||||||
(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))
|
||||||
(when (and (pair? rs) (fx<? shown 30))
|
(if (or (null? 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))
|
||||||
|
|
@ -95,15 +131,55 @@
|
||||||
(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: bare name
|
(put-string port frame-name)) ; 'ambiguous / unmapped: bare name
|
||||||
(put-char port #\newline))
|
(when (fx>? cnt 1)
|
||||||
(loop (cdr rs) (fx+ shown 1))))
|
(put-string port " (x") (put-string port (number->string cnt)) (put-string port ")"))
|
||||||
(get-output-string port)))))))
|
(put-char port #\newline)
|
||||||
|
(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
|
||||||
;; pr-str'd. Shared by the cli (cli.ss) and a built binary's launcher (build.ss).
|
;; pr-str'd. Shared by the cli (cli.ss) and a built binary's launcher (build.ss).
|
||||||
(define (jolt-render-throwable v port)
|
(define (jolt-render-throwable raw port)
|
||||||
|
(let ((v (jolt-unwrap-throw raw)))
|
||||||
(if (jolt=2 (jolt-get v jolt-kw-ex-type jolt-nil) jolt-kw-ex-info)
|
(if (jolt=2 (jolt-get v jolt-kw-ex-type jolt-nil) jolt-kw-ex-info)
|
||||||
(begin
|
(begin
|
||||||
(display "Unhandled exception: " port)
|
(display "Unhandled exception: " port)
|
||||||
|
|
@ -120,7 +196,7 @@
|
||||||
(begin
|
(begin
|
||||||
(display "Unhandled exception: " port)
|
(display "Unhandled exception: " port)
|
||||||
(display (if (condition? v) (with-output-to-string (lambda () (display-condition v))) (jolt-pr-str v)) port)
|
(display (if (condition? v) (with-output-to-string (lambda () (display-condition v))) (jolt-pr-str v)) port)
|
||||||
(newline port))))
|
(newline port)))))
|
||||||
|
|
||||||
;; Render the throwable, then its Clojure backtrace when one maps. The caller adds
|
;; Render the throwable, then its Clojure backtrace when one maps. The caller adds
|
||||||
;; any top-level source location (the runtime cli does; a built binary has none).
|
;; any top-level source location (the runtime cli does; a built binary has none).
|
||||||
|
|
|
||||||
115
host/chez/static-native-smoke.sh
Normal file
115
host/chez/static-native-smoke.sh
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# static-native smoke: a project's :jolt/native lib with a :static archive is
|
||||||
|
# LINKED INTO the built binary (the default), so the binary calls the C function
|
||||||
|
# with no shared object on disk at runtime. --dynamic keeps the old behavior —
|
||||||
|
# load a shared object at runtime.
|
||||||
|
root="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
||||||
|
cd "$root"
|
||||||
|
|
||||||
|
# Preflight: needs cc (to build the test libs AND to cc-link the app) + Chez's
|
||||||
|
# kernel dev files, same as build-smoke. Skip otherwise (CI on a distro package).
|
||||||
|
csv="$JOLT_CHEZ_CSV"
|
||||||
|
if [ -z "$csv" ]; then
|
||||||
|
chez_bin="$(command -v chez || command -v scheme || command -v petite || true)"
|
||||||
|
if [ -n "$chez_bin" ]; then
|
||||||
|
base="$(cd "$(dirname "$chez_bin")/.." 2>/dev/null && pwd)"
|
||||||
|
for d in "$base"/lib/csv*/*/; do
|
||||||
|
[ -f "${d}libkernel.a" ] && csv="${d%/}" && break
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if ! command -v cc >/dev/null 2>&1 || [ -z "$csv" ] || [ ! -f "$csv/scheme.h" ] || [ ! -f "$csv/libkernel.a" ]; then
|
||||||
|
echo "static-native smoke: skipped (Chez kernel dev files or C compiler not available)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
export JOLT_CHEZ_CSV="$csv"
|
||||||
|
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Darwin) plat=":darwin"; soext="dylib"; shared="-dynamiclib" ;;
|
||||||
|
*) plat=":linux"; soext="so"; shared="-shared" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
work="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$work"' EXIT
|
||||||
|
app="$work/app"
|
||||||
|
mkdir -p "$app/src/app"
|
||||||
|
|
||||||
|
# 1. a trivial C library, built BOTH as a static archive and a shared object.
|
||||||
|
cat > "$work/greet.c" <<'EOF'
|
||||||
|
int jolt_static_answer(void) { return 42; }
|
||||||
|
EOF
|
||||||
|
cc -c "$work/greet.c" -o "$work/greet.o"
|
||||||
|
ar rcs "$work/libgreet.a" "$work/greet.o"
|
||||||
|
cc $shared "$work/greet.c" -o "$work/libgreet.$soext"
|
||||||
|
|
||||||
|
# 2. an app that binds that symbol via FFI.
|
||||||
|
cat > "$app/src/app/core.clj" <<'EOF'
|
||||||
|
(ns app.core
|
||||||
|
(:require [jolt.ffi :as ffi]))
|
||||||
|
(ffi/defcfn answer "jolt_static_answer" [] :int)
|
||||||
|
(defn -main [& _]
|
||||||
|
(println "answer:" (answer)))
|
||||||
|
EOF
|
||||||
|
|
||||||
|
out="$work/app-bin"
|
||||||
|
|
||||||
|
# --- default: static link ---------------------------------------------------
|
||||||
|
# A static-only spec (no runtime candidate): the build resolves the symbol by
|
||||||
|
# preloading the archive, and the binary links it in — nothing to load at runtime.
|
||||||
|
cat > "$app/deps.edn" <<EOF
|
||||||
|
{:paths ["src"]
|
||||||
|
:jolt/native [{:name "greet" :static {:archive "$work/libgreet.a"}}]}
|
||||||
|
EOF
|
||||||
|
echo "static-native smoke: building (default: static link)"
|
||||||
|
if ! JOLT_PWD="$app" bin/joltc build -m app.core -o "$out" >"$work/build.log" 2>&1; then
|
||||||
|
echo " FAIL: jolt build (static) exited non-zero"; cat "$work/build.log"; exit 1
|
||||||
|
fi
|
||||||
|
[ -x "$out" ] || { echo " FAIL: no executable produced"; exit 1; }
|
||||||
|
# A static lib emits a process-symbol load (its archive is in-process), not a
|
||||||
|
# dlopen of the shared object.
|
||||||
|
if ! grep -q "jolt-build-load-native '() #f #t" "$out.build/flat.ss"; then
|
||||||
|
echo " FAIL: static native did not emit a process-symbol load"; exit 1
|
||||||
|
fi
|
||||||
|
if grep -q "libgreet.$soext" "$out.build/flat.ss"; then
|
||||||
|
echo " FAIL: static native baked a runtime shared-object load"; exit 1
|
||||||
|
fi
|
||||||
|
# Remove BOTH libs: a static-linked symbol lives in the binary, nothing to load.
|
||||||
|
rm -f "$work/libgreet.a" "$work/libgreet.$soext" "$work/greet.o"
|
||||||
|
got="$(cd / && "$out" 2>&1)"
|
||||||
|
if [ "$got" != "answer: 42" ]; then
|
||||||
|
echo " FAIL: static-linked binary output mismatch"
|
||||||
|
echo "--- want ---"; echo "answer: 42"; echo "--- got ----"; echo "$got"; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- --dynamic: runtime load ------------------------------------------------
|
||||||
|
# Rebuild the shared object (static phase deleted it) and give the spec a runtime
|
||||||
|
# candidate; --dynamic loads it at startup instead of linking the archive.
|
||||||
|
cc $shared "$work/greet.c" -o "$work/libgreet.$soext"
|
||||||
|
cat > "$app/deps.edn" <<EOF
|
||||||
|
{:paths ["src"]
|
||||||
|
:jolt/native [{:name "greet"
|
||||||
|
:static {:archive "$work/libgreet.a"}
|
||||||
|
$plat ["$work/libgreet.$soext"]}]}
|
||||||
|
EOF
|
||||||
|
echo "static-native smoke: building (--dynamic: runtime load)"
|
||||||
|
if ! JOLT_PWD="$app" bin/joltc build -m app.core -o "$out" --dynamic >"$work/build.log" 2>&1; then
|
||||||
|
echo " FAIL: jolt build --dynamic exited non-zero"; cat "$work/build.log"; exit 1
|
||||||
|
fi
|
||||||
|
# --dynamic loads the shared object at runtime.
|
||||||
|
if ! grep -q "libgreet.$soext" "$out.build/flat.ss"; then
|
||||||
|
echo " FAIL: --dynamic did not emit a runtime shared-object load"; exit 1
|
||||||
|
fi
|
||||||
|
got="$(cd / && "$out" 2>&1)"
|
||||||
|
if [ "$got" != "answer: 42" ]; then
|
||||||
|
echo " FAIL: --dynamic binary output mismatch (shared object present)"
|
||||||
|
echo "--- got ----"; echo "$got"; exit 1
|
||||||
|
fi
|
||||||
|
# With the shared object gone, a --dynamic binary must FAIL — proving the symbol
|
||||||
|
# was loaded at runtime, not baked in.
|
||||||
|
rm -f "$work/libgreet.$soext"
|
||||||
|
rc=0; { (cd / && exec "$out"); } >/dev/null 2>&1 || rc=$?
|
||||||
|
if [ "$rc" -eq 0 ]; then
|
||||||
|
echo " FAIL: --dynamic binary still ran with its shared object removed"; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "static-native smoke: passed (static default + --dynamic runtime load)"
|
||||||
|
|
@ -26,6 +26,12 @@ static int self_path(char *buf, uint32_t size) {
|
||||||
/* _NSGetExecutablePath fills buf and reports the needed size on overflow. */
|
/* _NSGetExecutablePath fills buf and reports the needed size on overflow. */
|
||||||
return _NSGetExecutablePath(buf, &size);
|
return _NSGetExecutablePath(buf, &size);
|
||||||
}
|
}
|
||||||
|
#elif defined(_WIN32)
|
||||||
|
#include <windows.h>
|
||||||
|
static int self_path(char *buf, uint32_t size) {
|
||||||
|
DWORD n = GetModuleFileNameA(NULL, buf, size);
|
||||||
|
return (n == 0 || n >= size) ? -1 : 0;
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
static int self_path(char *buf, uint32_t size) {
|
static int self_path(char *buf, uint32_t size) {
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,16 @@
|
||||||
(let ((ht (make-hashtable key-hash jolt=2)))
|
(let ((ht (make-hashtable key-hash jolt=2)))
|
||||||
(pset-fold coll (lambda (e acc) (hashtable-set! ht e #t) acc) 0)
|
(pset-fold coll (lambda (e acc) (hashtable-set! ht e #t) acc) 0)
|
||||||
(make-jolt-transient 'set ht 0 #t #f)))
|
(make-jolt-transient 'set ht 0 #t #f)))
|
||||||
(else (make-jolt-transient 'cow coll 0 #t #f))))
|
;; RFC 0003: any COLLECTION transients (the sorted/list/seq superset rides
|
||||||
|
;; the copy-on-write fallback); a non-collection is the JVM's cast failure.
|
||||||
|
((or (cseq? coll) (empty-list-t? coll) (jolt-lazyseq? coll)
|
||||||
|
(htable? coll) (jrec? coll))
|
||||||
|
(make-jolt-transient 'cow coll 0 #t #f))
|
||||||
|
(else
|
||||||
|
(jolt-throw (jolt-host-throwable
|
||||||
|
"java.lang.ClassCastException"
|
||||||
|
(string-append "class " (guard (e (#t "?")) (jolt-class-name coll))
|
||||||
|
" cannot be cast to class clojure.lang.IEditableCollection"))))))
|
||||||
|
|
||||||
;; map put/delete that maintain the reverse insertion-order list in `ord`.
|
;; map put/delete that maintain the reverse insertion-order list in `ord`.
|
||||||
(define (tmap-put! t k v)
|
(define (tmap-put! t k v)
|
||||||
|
|
@ -77,7 +86,11 @@
|
||||||
(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))
|
||||||
|
|
|
||||||
|
|
@ -96,10 +96,16 @@
|
||||||
((and (jolt-coll? a) (jolt-coll? b)) (jolt-coll=? a b))
|
((and (jolt-coll? a) (jolt-coll? b)) (jolt-coll=? a b))
|
||||||
(else (eq? a b))))
|
(else (eq? a b))))
|
||||||
(define (jolt=2 a b)
|
(define (jolt=2 a b)
|
||||||
|
;; identity fast path, like Util.equiv's k1 == k2: the same object equals
|
||||||
|
;; itself without a structural walk — (= s s) on an infinite lazy seq must not
|
||||||
|
;; realize it. Numbers keep the exactness-aware arm (Chez may intern flonum
|
||||||
|
;; literals, and (= ##NaN ##NaN) is false like the JVM's).
|
||||||
|
(if (and (eq? a b) (not (number? a)))
|
||||||
|
#t
|
||||||
(let loop ((as jolt-eq-arms))
|
(let loop ((as jolt-eq-arms))
|
||||||
(cond ((null? as) (jolt=2-base a b))
|
(cond ((null? as) (jolt=2-base a b))
|
||||||
(((caar as) a b) ((cdar as) a b))
|
(((caar as) a b) ((cdar as) a b))
|
||||||
(else (loop (cdr as))))))
|
(else (loop (cdr as)))))))
|
||||||
(define (jolt= a . rest)
|
(define (jolt= a . rest)
|
||||||
(let loop ((a a) (rest rest))
|
(let loop ((a a) (rest rest))
|
||||||
(cond ((null? rest) #t)
|
(cond ((null? rest) #t)
|
||||||
|
|
|
||||||
161
install
Executable file
161
install
Executable file
|
|
@ -0,0 +1,161 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Installs the latest (or a specific) version of joltc, the self-contained jolt
|
||||||
|
# binary. It bundles the runtime, compiler, jolt-core + stdlib, and the Chez
|
||||||
|
# boots, so there is nothing else to install — no Chez, no cc, no JVM.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
version=""
|
||||||
|
checksum=""
|
||||||
|
default_install_dir="/usr/local/bin"
|
||||||
|
install_dir="$default_install_dir"
|
||||||
|
download_dir=""
|
||||||
|
|
||||||
|
repo="jolt-lang/jolt"
|
||||||
|
|
||||||
|
print_help() {
|
||||||
|
echo "Installs the latest (or a specific) version of joltc."
|
||||||
|
echo "Installation directory defaults to ${default_install_dir}."
|
||||||
|
echo
|
||||||
|
echo "Usage:"
|
||||||
|
echo " install [--dir <dir>] [--download-dir <dir>] [--version <version>] [--checksum <sha256>]"
|
||||||
|
echo
|
||||||
|
echo "Defaults:"
|
||||||
|
echo " * Installation directory: ${default_install_dir}"
|
||||||
|
echo " * Download directory: a temporary directory"
|
||||||
|
echo " * Version: the latest release on GitHub"
|
||||||
|
echo " * Checksum: fetched from the release and verified automatically"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
has() {
|
||||||
|
command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch() {
|
||||||
|
local url=$1
|
||||||
|
local outfile=${2:-}
|
||||||
|
if has curl; then
|
||||||
|
if [[ -n $outfile ]]; then curl -fsSL "$url" -o "$outfile"; else curl -fsSL "$url"; fi
|
||||||
|
elif has wget; then
|
||||||
|
if [[ -n $outfile ]]; then wget -qO "$outfile" "$url"; else wget -qO - "$url"; fi
|
||||||
|
else
|
||||||
|
>&2 echo "Either 'curl' or 'wget' needs to be on PATH."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dir) install_dir="$2"; shift 2 ;;
|
||||||
|
--download-dir) download_dir="$2"; shift 2 ;;
|
||||||
|
--version) version="$2"; shift 2 ;;
|
||||||
|
--checksum) checksum="$2"; shift 2 ;;
|
||||||
|
--help|-h) print_help ;;
|
||||||
|
*) print_help ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$download_dir" ]]; then
|
||||||
|
download_dir="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$download_dir"' EXIT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- resolve platform / arch to a release target -----------------------------
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Linux*) platform=linux ;;
|
||||||
|
Darwin*) platform=macos ;;
|
||||||
|
*) >&2 echo "Unsupported OS: $(uname -s). Prebuilt binaries exist for Linux and macOS."; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64|amd64) arch=x86_64 ;;
|
||||||
|
aarch64|arm64) arch=aarch64 ;;
|
||||||
|
*) >&2 echo "Unsupported architecture: $(uname -m)."; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
target="${arch}-${platform}"
|
||||||
|
case "$target" in
|
||||||
|
x86_64-linux|aarch64-macos) ;;
|
||||||
|
x86_64-macos)
|
||||||
|
>&2 echo "No prebuilt joltc for Intel macOS (GitHub retired the Intel runner)."
|
||||||
|
>&2 echo "Build from source: https://github.com/${repo} (needs Chez Scheme + cc)."
|
||||||
|
exit 1 ;;
|
||||||
|
*) >&2 echo "No prebuilt joltc for ${target}."
|
||||||
|
>&2 echo "Available: x86_64-linux, aarch64-macos."
|
||||||
|
>&2 echo "Build from source: https://github.com/${repo} (make joltc-release)."
|
||||||
|
exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# --- resolve version ---------------------------------------------------------
|
||||||
|
if [[ -z "$version" ]]; then
|
||||||
|
version="$(fetch "https://api.github.com/repos/${repo}/releases/latest" \
|
||||||
|
| grep -m1 '"tag_name"' | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/')"
|
||||||
|
if [[ -z "$version" ]]; then
|
||||||
|
>&2 echo "Could not determine the latest release. Pass --version explicitly."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
tag="v${version#v}" # accept 0.1.0 or v0.1.0; the tag/asset carry the leading v
|
||||||
|
|
||||||
|
filename="joltc-${tag}-${target}.tar.gz"
|
||||||
|
download_url="https://github.com/${repo}/releases/download/${tag}/${filename}"
|
||||||
|
|
||||||
|
if has sha256sum; then
|
||||||
|
sha256sum_cmd="sha256sum"
|
||||||
|
elif has shasum; then
|
||||||
|
sha256sum_cmd="shasum -a 256"
|
||||||
|
else
|
||||||
|
sha256sum_cmd=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$download_dir" && (
|
||||||
|
cd "$download_dir"
|
||||||
|
echo "Downloading ${download_url}"
|
||||||
|
fetch "$download_url" "$filename"
|
||||||
|
|
||||||
|
# verify: an explicit --checksum wins; otherwise fetch the release's .sha256.
|
||||||
|
if [[ -z "$checksum" ]]; then
|
||||||
|
checksum="$(fetch "${download_url}.sha256" 2>/dev/null | cut -d' ' -f1 || true)"
|
||||||
|
fi
|
||||||
|
if [[ -n "$checksum" && -n "$sha256sum_cmd" ]]; then
|
||||||
|
got="$($sha256sum_cmd "$filename" | cut -d' ' -f1)"
|
||||||
|
if [[ "$got" != "$checksum" ]]; then
|
||||||
|
>&2 echo "Checksum mismatch on ${filename}"
|
||||||
|
>&2 echo " got: ${got}"
|
||||||
|
>&2 echo " expected: ${checksum}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
elif [[ -z "$sha256sum_cmd" ]]; then
|
||||||
|
>&2 echo "Note: no sha256sum/shasum on PATH; skipping checksum verification."
|
||||||
|
fi
|
||||||
|
|
||||||
|
tar -zxf "$filename"
|
||||||
|
rm -f "$filename"
|
||||||
|
)
|
||||||
|
|
||||||
|
# the tarball unpacks to a directory holding the binary
|
||||||
|
extracted="${download_dir}/joltc-${tag}-${target}/joltc"
|
||||||
|
if [[ ! -f "$extracted" ]]; then
|
||||||
|
>&2 echo "Expected ${extracted} in the archive but it was not found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$install_dir"
|
||||||
|
if [[ -f "$install_dir/joltc" ]]; then
|
||||||
|
echo "Moving existing $install_dir/joltc to $install_dir/joltc.old"
|
||||||
|
mv -f "$install_dir/joltc" "$install_dir/joltc.old"
|
||||||
|
fi
|
||||||
|
mv -f "$extracted" "$install_dir/joltc"
|
||||||
|
chmod +x "$install_dir/joltc"
|
||||||
|
|
||||||
|
# clear the macOS quarantine flag so Gatekeeper doesn't block the fresh download
|
||||||
|
if [[ "$platform" == "macos" ]] && has xattr; then
|
||||||
|
xattr -d com.apple.quarantine "$install_dir/joltc" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Successfully installed joltc ${tag} to ${install_dir}/joltc"
|
||||||
|
if ! echo ":$PATH:" | grep -q ":${install_dir}:"; then
|
||||||
|
echo "Note: ${install_dir} is not on your PATH."
|
||||||
|
fi
|
||||||
|
|
@ -43,3 +43,10 @@
|
||||||
(defn mapv [f & colls] (vec (apply map f colls)))
|
(defn mapv [f & colls] (vec (apply map f colls)))
|
||||||
|
|
||||||
(defn update [m k f & args] (assoc m k (apply f (get m k) args)))
|
(defn update [m k f & args] (assoc m k (apply f (get m k) args)))
|
||||||
|
|
||||||
|
;; set: realize a seqable and dedup through the set constructor; nil -> #{}. The
|
||||||
|
;; compiler uses it off the emit path (backend bare-native-names, type inference),
|
||||||
|
;; so unlike boolean it can live here — compiling this tier never calls set, and by
|
||||||
|
;; the time those callers run the tier is bound. Pure composition of hash-set/seq/
|
||||||
|
;; apply, so it lowers to the same code the native shim did.
|
||||||
|
(defn set [coll] (if (nil? coll) #{} (apply hash-set (seq coll))))
|
||||||
|
|
|
||||||
|
|
@ -188,9 +188,23 @@
|
||||||
[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)))
|
||||||
|
|
@ -231,30 +245,45 @@
|
||||||
;; 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]
|
(fn* group [a names kind dnsp checked?]
|
||||||
(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 s]
|
(fn* [aa pair]
|
||||||
(let* [local (name s)
|
(let* [s (nth pair 0)
|
||||||
|
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)
|
||||||
(conj (conj aa (symbol local))
|
lookup (cond
|
||||||
(if (nth fo 0)
|
checked? `(req! ~gm ~keyform)
|
||||||
`(get ~gm ~keyform ~(nth fo 1))
|
(nth fo 0) `(get ~gm ~keyform ~(nth fo 1))
|
||||||
`(get ~gm ~keyform)))))
|
:else `(get ~gm ~keyform))]
|
||||||
a names)
|
(cond
|
||||||
|
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)
|
g1 (group base (get pat :keys) :kw nil false)
|
||||||
g2 (group g1 (get pat :strs) :str nil)
|
g2 (group g1 (get pat :strs) :str nil false)
|
||||||
g3 (group g2 (get pat :syms) :sym nil)]
|
g3 (group g2 (get pat :syms) :sym nil false)
|
||||||
|
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.
|
||||||
|
|
@ -262,9 +291,12 @@
|
||||||
(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)
|
(and kns (= kn "keys")) (group a (get pat k) :kw kns false)
|
||||||
(and kns (= kn "strs")) (group a (get pat k) :str kns)
|
(and kns (= kn "strs")) (group a (get pat k) :str kns false)
|
||||||
(and kns (= kn "syms")) (group a (get pat k) :sym kns)
|
(and kns (= kn "syms")) (group a (get pat k) :sym kns false)
|
||||||
|
(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.
|
||||||
|
|
@ -273,7 +305,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))))
|
||||||
g3 (keys pat)))
|
g6 (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]
|
||||||
|
|
@ -514,7 +546,9 @@
|
||||||
sub (wrap-mods (rest mods) inner)]
|
sub (wrap-mods (rest mods) inner)]
|
||||||
(if (= (first m) :when)
|
(if (= (first m) :when)
|
||||||
`(if ~(nth m 1) ~sub [])
|
`(if ~(nth m 1) ~sub [])
|
||||||
`(let* ~(nth m 1) ~sub)))))
|
;; `let` (not let*) so a :let binding may itself
|
||||||
|
;; destructure — (for [x xs :let [{:keys [y]} x]] …).
|
||||||
|
`(let ~(nth m 1) ~sub)))))
|
||||||
build (fn build [idx groups]
|
build (fn build [idx groups]
|
||||||
(let [g (nth groups idx)
|
(let [g (nth groups idx)
|
||||||
my-bind (nth g 0)
|
my-bind (nth g 0)
|
||||||
|
|
@ -543,6 +577,8 @@
|
||||||
;; name binds only in the taken branch (temp# tests the value); via `let` so the
|
;; name binds only in the taken branch (temp# tests the value); via `let` so the
|
||||||
;; binding form may itself destructure, matching Clojure.
|
;; binding form may itself destructure, matching Clojure.
|
||||||
(defmacro when-let [bindings & body]
|
(defmacro when-let [bindings & body]
|
||||||
|
(when (not= 2 (count bindings))
|
||||||
|
(throw (new IllegalArgumentException "when-let requires exactly 2 forms in binding vector")))
|
||||||
(let [form (bindings 0) tst (bindings 1)]
|
(let [form (bindings 0) tst (bindings 1)]
|
||||||
`(let [temp# ~tst]
|
`(let [temp# ~tst]
|
||||||
(if temp# (let [~form temp#] ~@body) nil))))
|
(if temp# (let [~form temp#] ~@body) nil))))
|
||||||
|
|
|
||||||
|
|
@ -155,8 +155,43 @@
|
||||||
(when-let [s (seq coll)]
|
(when-let [s (seq coll)]
|
||||||
(or (pred (first s)) (recur pred (next s)))))
|
(or (pred (first s)) (recur pred (next s)))))
|
||||||
|
|
||||||
(defn some-fn [& preds]
|
;; Reference arities: at least one predicate ((some-fn) is an arity error), and
|
||||||
(fn [& xs] (some (fn [p] (some p xs)) preds)))
|
;; the returned fn chains with or — a no-match result is the last predicate's
|
||||||
|
;; own falsy value (false stays false, not nil).
|
||||||
|
(defn some-fn
|
||||||
|
([p]
|
||||||
|
(fn sp1
|
||||||
|
([] nil)
|
||||||
|
([x] (p x))
|
||||||
|
([x y] (or (p x) (p y)))
|
||||||
|
([x y z] (or (p x) (p y) (p z)))
|
||||||
|
([x y z & args] (or (sp1 x y z)
|
||||||
|
(some p args)))))
|
||||||
|
([p1 p2]
|
||||||
|
(fn sp2
|
||||||
|
([] nil)
|
||||||
|
([x] (or (p1 x) (p2 x)))
|
||||||
|
([x y] (or (p1 x) (p1 y) (p2 x) (p2 y)))
|
||||||
|
([x y z] (or (p1 x) (p1 y) (p1 z) (p2 x) (p2 y) (p2 z)))
|
||||||
|
([x y z & args] (or (sp2 x y z)
|
||||||
|
(some (fn [q] (or (p1 q) (p2 q))) args)))))
|
||||||
|
([p1 p2 p3]
|
||||||
|
(fn sp3
|
||||||
|
([] nil)
|
||||||
|
([x] (or (p1 x) (p2 x) (p3 x)))
|
||||||
|
([x y] (or (p1 x) (p2 x) (p3 x) (p1 y) (p2 y) (p3 y)))
|
||||||
|
([x y z] (or (p1 x) (p2 x) (p3 x) (p1 y) (p2 y) (p3 y) (p1 z) (p2 z) (p3 z)))
|
||||||
|
([x y z & args] (or (sp3 x y z)
|
||||||
|
(some (fn [q] (or (p1 q) (p2 q) (p3 q))) args)))))
|
||||||
|
([p1 p2 p3 & ps]
|
||||||
|
(let [ps (cons p1 (cons p2 (cons p3 ps)))]
|
||||||
|
(fn spn
|
||||||
|
([] nil)
|
||||||
|
([x] (some (fn [p] (p x)) ps))
|
||||||
|
([x y] (or (spn x) (spn y)))
|
||||||
|
([x y z] (or (spn x) (spn y) (spn z)))
|
||||||
|
([x y z & args] (or (spn x y z)
|
||||||
|
(some (fn [p] (some p args)) ps)))))))
|
||||||
|
|
||||||
(defn not-any? [pred coll] (not (some pred coll)))
|
(defn not-any? [pred coll] (not (some pred coll)))
|
||||||
|
|
||||||
|
|
@ -177,13 +212,22 @@
|
||||||
|
|
||||||
(defn simple-ident? [x] (or (simple-symbol? x) (simple-keyword? x)))
|
(defn simple-ident? [x] (or (simple-symbol? x) (simple-keyword? x)))
|
||||||
|
|
||||||
;; Jolt has no ratio or bigdecimal types, so these are constants / reduce to int?.
|
;; Numeric-tower predicates over the Chez tower (jolt has exact ints, ratios, and
|
||||||
(defn ratio? [x] false)
|
;; flonums). ratio? = exact non-integer; rational? = exact (int or ratio). Built on
|
||||||
(defn decimal? [x] false)
|
;; the jolt.host tower tests so they lower to the same code the native shims did.
|
||||||
;; No first-class Class objects either: class names are symbols the evaluator
|
;; decimal?/integer?/float?/int?/double? stay native (bigdec-extended or on the
|
||||||
;; handles in instance?/new positions, never values — so nothing is a class.
|
;; compiler emit/inference path) — see predicates.ss.
|
||||||
|
(defn ratio? [x]
|
||||||
|
(and (number? x) (jolt.host/exact? x) (jolt.host/rational-type? x) (not (integer? x))))
|
||||||
|
(defn rational? [x]
|
||||||
|
(or (and (number? x) (jolt.host/exact? x)) (decimal? x)))
|
||||||
|
;; No first-class Class objects: class names are symbols the evaluator handles in
|
||||||
|
;; instance?/new positions, never values — so nothing is a class.
|
||||||
(defn class? [x] false)
|
(defn class? [x] false)
|
||||||
(defn rational? [x] (int? x))
|
;; list?: a list-marked cseq node or the empty list (). A lazy/vector-backed seq,
|
||||||
|
;; (rest list), (seq coll), (map …) are seqs but not lists. Not extended like
|
||||||
|
;; map?/set?/seq?, so it migrates cleanly.
|
||||||
|
(defn list? [x] (or (and (jolt.host/cseq? x) (jolt.host/cseq-list? x)) (jolt.host/empty-list? x)))
|
||||||
(defn nat-int? [x] (and (int? x) (>= x 0)))
|
(defn nat-int? [x] (and (int? x) (>= x 0)))
|
||||||
(defn neg-int? [x] (and (int? x) (neg? x)))
|
(defn neg-int? [x] (and (int? x) (neg? x)))
|
||||||
(defn pos-int? [x] (and (int? x) (pos? x)))
|
(defn pos-int? [x] (and (int? x) (pos? x)))
|
||||||
|
|
@ -231,7 +275,8 @@
|
||||||
(loop [i 0 s (seq coll)]
|
(loop [i 0 s (seq coll)]
|
||||||
(if (and s (< i n)) (recur (inc i) (next s)) i))))
|
(if (and s (< i n)) (recur (inc i) (next s)) i))))
|
||||||
|
|
||||||
(defn run! [proc coll] (reduce (fn [_ x] (proc x) nil) nil coll) nil)
|
;; the reducing fn returns proc's result, so a Reduced from proc short-circuits
|
||||||
|
(defn run! [proc coll] (reduce (fn [_ x] (proc x)) nil coll) nil)
|
||||||
|
|
||||||
(defn completing
|
(defn completing
|
||||||
([f] (completing f identity))
|
([f] (completing f identity))
|
||||||
|
|
@ -294,12 +339,24 @@
|
||||||
;; :descendants {tag #{all}}}. The 3-arity forms are PURE; the 1/2-arity forms
|
;; :descendants {tag #{all}}}. The 3-arity forms are PURE; the 1/2-arity forms
|
||||||
;; operate on the private global hierarchy atom. Multimethod dispatch
|
;; operate on the private global hierarchy atom. Multimethod dispatch
|
||||||
;; (evaluator defmulti-setup) calls isa? through the interned var.
|
;; (evaluator defmulti-setup) calls isa? through the interned var.
|
||||||
|
;;
|
||||||
|
;; Ported from clojure.core with the reference's argument assertions and throw
|
||||||
|
;; contracts intact — bad shapes throw exactly where they do there (a non-map h
|
||||||
|
;; fails on the (parent-map tag) call, invalid tags fail the asserts). The class
|
||||||
|
;; arms answer through the host class graph (jolt.host/class-* seams).
|
||||||
|
|
||||||
(defn make-hierarchy []
|
(defn make-hierarchy []
|
||||||
{:parents {} :descendants {} :ancestors {}})
|
{:parents {} :descendants {} :ancestors {}})
|
||||||
|
|
||||||
(def ^:private global-hierarchy (atom (make-hierarchy)))
|
(def ^:private global-hierarchy (atom (make-hierarchy)))
|
||||||
|
|
||||||
|
(defn- hier-assert [ok form]
|
||||||
|
(when-not ok (throw (new AssertionError (str "Assert failed: " form)))))
|
||||||
|
|
||||||
|
;; a hierarchy tag naming a class — a class value, or the name string of a class
|
||||||
|
;; the host graph models (jolt classes are their name strings).
|
||||||
|
(defn- class-tag? [tag] (if (jolt.host/class-value? tag) true false))
|
||||||
|
|
||||||
(defn isa?
|
(defn isa?
|
||||||
([child parent] (isa? (deref global-hierarchy) child parent))
|
([child parent] (isa? (deref global-hierarchy) child parent))
|
||||||
([h child parent]
|
([h child parent]
|
||||||
|
|
@ -308,6 +365,10 @@
|
||||||
;; so a class-keyed multimethod / (isa? (class x) C) dispatches like the JVM.
|
;; so a class-keyed multimethod / (isa? (class x) C) dispatches like the JVM.
|
||||||
(jolt.host/class-isa? child parent)
|
(jolt.host/class-isa? child parent)
|
||||||
(contains? (get (get h :ancestors) child #{}) parent)
|
(contains? (get (get h :ancestors) child #{}) parent)
|
||||||
|
;; a hierarchy relationship established on one of a class's supers
|
||||||
|
(and (class-tag? child)
|
||||||
|
(some (fn [s] (contains? (get (get h :ancestors) s #{}) parent))
|
||||||
|
(jolt.host/class-supers child)))
|
||||||
(and (vector? parent) (vector? child)
|
(and (vector? parent) (vector? child)
|
||||||
(= (count parent) (count child))
|
(= (count parent) (count child))
|
||||||
(loop [ret true i 0]
|
(loop [ret true i 0]
|
||||||
|
|
@ -317,24 +378,44 @@
|
||||||
|
|
||||||
(defn parents
|
(defn parents
|
||||||
([tag] (parents (deref global-hierarchy) tag))
|
([tag] (parents (deref global-hierarchy) tag))
|
||||||
([h tag] (not-empty (get (get h :parents) tag))))
|
([h tag] (not-empty
|
||||||
|
(let [tp (get (get h :parents) tag)]
|
||||||
|
(if (class-tag? tag)
|
||||||
|
(into (set (jolt.host/class-bases tag)) tp)
|
||||||
|
tp)))))
|
||||||
|
|
||||||
(defn ancestors
|
(defn ancestors
|
||||||
([tag] (ancestors (deref global-hierarchy) tag))
|
([tag] (ancestors (deref global-hierarchy) tag))
|
||||||
([h tag]
|
([h tag] (not-empty
|
||||||
;; the user hierarchy plus any modeled JVM ancestry (jolt.host/class-ancestors)
|
(let [ta (get (get h :ancestors) tag)]
|
||||||
;; so (ancestors (class x)) answers like the JVM for the common interfaces.
|
(if (class-tag? tag)
|
||||||
(let [hier (get (get h :ancestors) tag)
|
;; the class's own ancestry plus hierarchy relationships derived
|
||||||
host (jolt.host/class-ancestors tag)]
|
;; on the class or any of its supers
|
||||||
(not-empty (if host (into (or hier #{}) host) hier)))))
|
(let [superclasses (set (jolt.host/class-supers tag))]
|
||||||
|
(reduce into superclasses
|
||||||
|
(cons ta (map (fn [s] (get (get h :ancestors) s))
|
||||||
|
superclasses))))
|
||||||
|
ta)))))
|
||||||
|
|
||||||
(defn descendants
|
(defn descendants
|
||||||
([tag] (descendants (deref global-hierarchy) tag))
|
([tag] (descendants (deref global-hierarchy) tag))
|
||||||
([h tag] (not-empty (get (get h :descendants) tag))))
|
([h tag] (if (class-tag? tag)
|
||||||
|
(throw (new UnsupportedOperationException "Can't get descendants of classes"))
|
||||||
|
(not-empty (get (get h :descendants) tag)))))
|
||||||
|
|
||||||
(defn derive
|
(defn derive
|
||||||
([tag parent] (swap! global-hierarchy derive tag parent) nil)
|
([tag parent]
|
||||||
|
(hier-assert (namespace parent) "(namespace parent)")
|
||||||
|
(hier-assert (or (class-tag? tag)
|
||||||
|
(and (or (keyword? tag) (symbol? tag)) (namespace tag)))
|
||||||
|
"(or (class? tag) (and (instance? clojure.lang.Named tag) (namespace tag)))")
|
||||||
|
(swap! global-hierarchy derive tag parent) nil)
|
||||||
([h tag parent]
|
([h tag parent]
|
||||||
|
(hier-assert (not= tag parent) "(not= tag parent)")
|
||||||
|
(hier-assert (or (class-tag? tag) (keyword? tag) (symbol? tag))
|
||||||
|
"(or (class? tag) (instance? clojure.lang.Named tag))")
|
||||||
|
(hier-assert (or (keyword? parent) (symbol? parent))
|
||||||
|
"(instance? clojure.lang.Named parent)")
|
||||||
(let [tp (get h :parents)
|
(let [tp (get h :parents)
|
||||||
td (get h :descendants)
|
td (get h :descendants)
|
||||||
ta (get h :ancestors)
|
ta (get h :ancestors)
|
||||||
|
|
@ -342,14 +423,14 @@
|
||||||
(reduce (fn [ret k]
|
(reduce (fn [ret k]
|
||||||
(assoc ret k
|
(assoc ret k
|
||||||
(reduce conj (get targets k #{})
|
(reduce conj (get targets k #{})
|
||||||
(cons target (get targets target)))))
|
(cons target (targets target)))))
|
||||||
m (cons source (get sources source))))]
|
m (cons source (sources source))))]
|
||||||
(or
|
(or
|
||||||
(when-not (contains? (get tp tag #{}) parent)
|
(when-not (contains? (tp tag) parent)
|
||||||
(when (contains? (get ta tag #{}) parent)
|
(when (contains? (ta tag) parent)
|
||||||
(throw (str tag " already has " parent " as ancestor")))
|
(throw (new Exception (str tag " already has " parent " as ancestor"))))
|
||||||
(when (contains? (get ta parent #{}) tag)
|
(when (contains? (ta parent) tag)
|
||||||
(throw (str "Cyclic derivation: " parent " has " tag " as ancestor")))
|
(throw (new Exception (str "Cyclic derivation: " parent " has " tag " as ancestor"))))
|
||||||
{:parents (assoc tp tag (conj (get tp tag #{}) parent))
|
{:parents (assoc tp tag (conj (get tp tag #{}) parent))
|
||||||
:ancestors (tf ta tag td parent ta)
|
:ancestors (tf ta tag td parent ta)
|
||||||
:descendants (tf td parent ta tag td)})
|
:descendants (tf td parent ta tag td)})
|
||||||
|
|
@ -359,15 +440,15 @@
|
||||||
([tag parent] (swap! global-hierarchy underive tag parent) nil)
|
([tag parent] (swap! global-hierarchy underive tag parent) nil)
|
||||||
([h tag parent]
|
([h tag parent]
|
||||||
(let [parent-map (get h :parents)
|
(let [parent-map (get h :parents)
|
||||||
childs-parents (if (get parent-map tag)
|
childs-parents (if (parent-map tag)
|
||||||
(disj (get parent-map tag) parent)
|
(disj (parent-map tag) parent)
|
||||||
#{})
|
#{})
|
||||||
new-parents (if (not-empty childs-parents)
|
new-parents (if (not-empty childs-parents)
|
||||||
(assoc parent-map tag childs-parents)
|
(assoc parent-map tag childs-parents)
|
||||||
(dissoc parent-map tag))
|
(dissoc parent-map tag))
|
||||||
deriv-seq (mapcat (fn [e] (cons (key e) (interpose (key e) (val e))))
|
deriv-seq (mapcat (fn [e] (cons (key e) (interpose (key e) (val e))))
|
||||||
(seq new-parents))]
|
(seq new-parents))]
|
||||||
(if (contains? (get parent-map tag #{}) parent)
|
(if (contains? (parent-map tag) parent)
|
||||||
(reduce (fn [p [t pr]] (derive p t pr))
|
(reduce (fn [p [t pr]] (derive p t pr))
|
||||||
(make-hierarchy) (partition 2 deriv-seq))
|
(make-hierarchy) (partition 2 deriv-seq))
|
||||||
h))))
|
h))))
|
||||||
|
|
@ -378,7 +459,8 @@
|
||||||
(defn sequential? [x] (or (vector? x) (seq? x)))
|
(defn sequential? [x] (or (vector? x) (seq? x)))
|
||||||
(defn associative? [x] (or (map? x) (vector? x)))
|
(defn associative? [x] (or (map? x) (vector? x)))
|
||||||
(defn counted? [x]
|
(defn counted? [x]
|
||||||
(or (vector? x) (map? x) (set? x) (list? x) (string? x)))
|
;; a String is not Counted on the JVM (count works via CharSequence, not O(1))
|
||||||
|
(or (vector? x) (map? x) (set? x) (list? x)))
|
||||||
(defn indexed? [x] (vector? x))
|
(defn indexed? [x] (vector? x))
|
||||||
;; sorted? is defined by the next tier (25-sorted) — declared here so this
|
;; sorted? is defined by the next tier (25-sorted) — declared here so this
|
||||||
;; tier compiles (forward references are analysis errors).
|
;; tier compiles (forward references are analysis errors).
|
||||||
|
|
@ -386,7 +468,7 @@
|
||||||
|
|
||||||
(defn reversible? [x] (or (vector? x) (sorted? x)))
|
(defn reversible? [x] (or (vector? x) (sorted? x)))
|
||||||
(defn seqable? [x]
|
(defn seqable? [x]
|
||||||
(or (nil? x) (coll? x) (string? x)))
|
(if (or (nil? x) (coll? x) (string? x) (jolt.host/array-value? x)) true false))
|
||||||
|
|
||||||
(defn boolean? [x] (or (true? x) (false? x)))
|
(defn boolean? [x] (or (true? x) (false? x)))
|
||||||
(defn double? [x] (and (number? x) (not (integer? x))))
|
(defn double? [x] (and (number? x) (not (integer? x))))
|
||||||
|
|
@ -418,7 +500,9 @@
|
||||||
(future? x) (boolean (get x :cached))
|
(future? x) (boolean (get x :cached))
|
||||||
(= :jolt/lazy-seq (get x :jolt/type)) (boolean (get x :realized))
|
(= :jolt/lazy-seq (get x :jolt/type)) (boolean (get x :realized))
|
||||||
(atom? x) true
|
(atom? x) true
|
||||||
:else (throw (str "realized? not supported on: " x))))
|
;; name the class, never the value — an error message must not render an
|
||||||
|
;; arbitrary (possibly infinite) argument.
|
||||||
|
:else (throw (str "realized? not supported on: " (class x)))))
|
||||||
|
|
||||||
(defn force [x] (if (delay? x) (deref x) x))
|
(defn force [x] (if (delay? x) (deref x) x))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@
|
||||||
;; Clojure. Collections only — a string is seqable but not shuffleable, as on
|
;; Clojure. Collections only — a string is seqable but not shuffleable, as on
|
||||||
;; the JVM (Collections/shuffle wants a Collection).
|
;; the JVM (Collections/shuffle wants a Collection).
|
||||||
(defn shuffle [coll]
|
(defn shuffle [coll]
|
||||||
(when-not (coll? coll)
|
;; Collections/shuffle wants a java.util.Collection — a map is not one
|
||||||
|
(when (or (not (coll? coll)) (map? coll))
|
||||||
(throw (ex-info (str "shuffle requires a collection, got: " coll) {})))
|
(throw (ex-info (str "shuffle requires a collection, got: " coll) {})))
|
||||||
(loop [v (vec coll) i (dec (count v))]
|
(loop [v (vec coll) i (dec (count v))]
|
||||||
(if (pos? i)
|
(if (pos? i)
|
||||||
|
|
@ -28,6 +29,10 @@
|
||||||
(defn sort-by
|
(defn sort-by
|
||||||
([keyfn coll] (sort-by keyfn compare coll))
|
([keyfn coll] (sort-by keyfn compare coll))
|
||||||
([keyfn comp coll]
|
([keyfn comp coll]
|
||||||
|
;; a collection is never a Comparator (the JVM cast would fail); catching it
|
||||||
|
;; here beats silently "sorting" through coll-as-fn lookups
|
||||||
|
(when (coll? comp)
|
||||||
|
(throw (new ClassCastException (str (class comp) " cannot be cast to java.util.Comparator"))))
|
||||||
(sort (fn [x y] (comp (keyfn x) (keyfn y))) coll)))
|
(sort (fn [x y] (comp (keyfn x) (keyfn y))) coll)))
|
||||||
|
|
||||||
;; parse-uuid: nil unless s is a canonical 8-4-4-4-12 hex UUID string; throws
|
;; parse-uuid: nil unless s is a canonical 8-4-4-4-12 hex UUID string; throws
|
||||||
|
|
@ -61,9 +66,10 @@
|
||||||
\backspace "backspace" \space "space"})
|
\backspace "backspace" \space "space"})
|
||||||
(defn char-name-string [c] (get char-name-strings c))
|
(defn char-name-string [c] (get char-name-strings c))
|
||||||
|
|
||||||
;; Random selection over the host rand primitives.
|
;; Random selection over the host rand primitives — the reference shape:
|
||||||
|
;; nth directly (nil returns nil via RT.nth; a set throws like the JVM).
|
||||||
(defn rand-nth [coll]
|
(defn rand-nth [coll]
|
||||||
(let [v (vec coll)] (nth v (rand-int (count v)))))
|
(nth coll (rand-int (count coll))))
|
||||||
|
|
||||||
(defn random-sample
|
(defn random-sample
|
||||||
([prob] (filter (fn [_] (< (rand) prob))))
|
([prob] (filter (fn [_] (< (rand) prob))))
|
||||||
|
|
@ -133,8 +139,8 @@
|
||||||
(concat (map first ss)
|
(concat (map first ss)
|
||||||
(apply interleave (map rest ss))))))))
|
(apply interleave (map rest ss))))))))
|
||||||
|
|
||||||
;; No ratio type on Jolt, so rationalize is identity.
|
;; rationalize is host-native (java/bigdec.ss): a double routes through its
|
||||||
(defn rationalize [x] x)
|
;; shortest decimal print like BigDecimal.valueOf, so (rationalize 1.1) is 11/10.
|
||||||
|
|
||||||
;; 0-arg: a stateful transducer (tracks [seen? prev] in a volatile, so no sentinel
|
;; 0-arg: a stateful transducer (tracks [seen? prev] in a volatile, so no sentinel
|
||||||
;; value is needed). 1-arg: eager dedupe of consecutive equal elements.
|
;; value is needed). 1-arg: eager dedupe of consecutive equal elements.
|
||||||
|
|
@ -348,8 +354,8 @@
|
||||||
(defn clojure-version [] "1.11.0-jolt")
|
(defn clojure-version [] "1.11.0-jolt")
|
||||||
|
|
||||||
;; bigdec is a host fn (host/chez/java/bigdec.ss) — a real BigDecimal value type.
|
;; bigdec is a host fn (host/chez/java/bigdec.ss) — a real BigDecimal value type.
|
||||||
(defn numerator [x] (throw (ex-info "numerator requires a ratio (Jolt has no ratios)" {})))
|
;; numerator/denominator are host natives (converters.ss) over Chez's exact
|
||||||
(defn denominator [x] (throw (ex-info "denominator requires a ratio (Jolt has no ratios)" {})))
|
;; rationals; a non-ratio is the Ratio cast failure.
|
||||||
|
|
||||||
;; jolt has no reflection, but a few common JVM interfaces carry a modeled
|
;; jolt has no reflection, but a few common JVM interfaces carry a modeled
|
||||||
;; ancestry (jolt.host/class-supers) so reflective checks like
|
;; ancestry (jolt.host/class-supers) so reflective checks like
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,21 @@
|
||||||
(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))]
|
||||||
|
|
@ -136,6 +151,9 @@
|
||||||
;; a deftype/record with its own empty (IPersistentCollection) — e.g.
|
;; a deftype/record with its own empty (IPersistentCollection) — e.g.
|
||||||
;; data.priority-map — uses it, before the generic map/set/vector arms.
|
;; data.priority-map — uses it, before the generic map/set/vector arms.
|
||||||
(jolt.host/jrec-method? coll "empty") (.empty coll)
|
(jolt.host/jrec-method? coll "empty") (.empty coll)
|
||||||
|
;; a defrecord without its own empty can't have one (RT: UnsupportedOperation)
|
||||||
|
(record? coll) (throw (new UnsupportedOperationException
|
||||||
|
(str "Can't create empty: " (.getName (class coll)))))
|
||||||
(sorted? coll) ((get (jolt.host/ref-get coll :ops) :empty) coll)
|
(sorted? coll) ((get (jolt.host/ref-get coll :ops) :empty) coll)
|
||||||
(map? coll) (with-meta {} (meta coll))
|
(map? coll) (with-meta {} (meta coll))
|
||||||
(set? coll) (with-meta #{} (meta coll))
|
(set? coll) (with-meta #{} (meta coll))
|
||||||
|
|
@ -183,10 +201,16 @@
|
||||||
([x y z & args] (f (apply g x y z args)))))
|
([x y z & args] (f (apply g x y z args)))))
|
||||||
([f g & fs] (reduce comp (comp f g) fs)))
|
([f g & fs] (reduce comp (comp f g) fs)))
|
||||||
|
|
||||||
;; Canonical IFn set: fns, keywords, symbols, maps (sorted incl.),
|
;; Canonical IFn set: fns, keywords, symbols, maps (sorted incl.), sets,
|
||||||
;; sets, vectors, and vars — NOT lists ((ifn? '(1 2)) is false in Clojure).
|
;; vectors, vars — NOT lists ((ifn? '(1 2)) is false in Clojure) — plus the
|
||||||
|
;; host callables (multimethods, promises) and a deftype/record implementing
|
||||||
|
;; clojure.lang.IFn's invoke.
|
||||||
(defn ifn? [x]
|
(defn ifn? [x]
|
||||||
(or (fn? x) (keyword? x) (symbol? x) (map? x) (set? x) (vector? x) (var? x)))
|
(if (or (fn? x) (keyword? x) (symbol? x) (map? x) (set? x) (vector? x) (var? x)
|
||||||
|
(jolt.host/callable-host? x)
|
||||||
|
(jolt.host/jrec-method? x "invoke"))
|
||||||
|
true
|
||||||
|
false))
|
||||||
|
|
||||||
;; Auto-promoting (') and unchecked arithmetic. Jolt numbers don't overflow,
|
;; Auto-promoting (') and unchecked arithmetic. Jolt numbers don't overflow,
|
||||||
;; so all of these are the checked ops; fixed arities mirror Clojure's
|
;; so all of these are the checked ops; fixed arities mirror Clojure's
|
||||||
|
|
@ -198,10 +222,9 @@
|
||||||
(def inc' inc)
|
(def inc' inc)
|
||||||
(def dec' dec)
|
(def dec' dec)
|
||||||
;; unchecked-add / -subtract / -multiply / -negate / -inc / -dec (+ the -int
|
;; unchecked-add / -subtract / -multiply / -negate / -inc / -dec (+ the -int
|
||||||
;; variants) and -divide-int / -remainder-int are host-defined (host/chez/seq.ss):
|
;; variants), -divide-int / -remainder-int, and the unchecked-long/-int casts are
|
||||||
;; they WRAP to signed 64 bits like the JVM, which a plain (+ x y) overlay can't do.
|
;; host-defined (host/chez/seq.ss, converters.ss): they WRAP like the JVM
|
||||||
(defn unchecked-int [x] (int x))
|
;; primitive conversions, which a plain overlay over checked casts can't do.
|
||||||
(def unchecked-long unchecked-int)
|
|
||||||
|
|
||||||
;; int? is integer? on jolt: one number type, so fixed-precision and
|
;; int? is integer? on jolt: one number type, so fixed-precision and
|
||||||
;; arbitrary-precision integers coincide.
|
;; arbitrary-precision integers coincide.
|
||||||
|
|
@ -263,12 +286,14 @@
|
||||||
|
|
||||||
(defn to-array-2d [coll] (to-array (map to-array coll)))
|
(defn to-array-2d [coll] (to-array (map to-array coll)))
|
||||||
|
|
||||||
;; Masking integer coercions (not aliases): byte/short wrap to their width.
|
;; Wrapping (unchecked) coercions: truncate to the width and sign-fold like the
|
||||||
;; unchecked-byte/short truncate to a number; unchecked-char returns a char (as on
|
;; JVM primitive conversions ((unchecked-byte 200) is -56); unchecked-char wraps
|
||||||
;; the JVM). int handles chars, so (unchecked-byte \a) works.
|
;; into char range. unchecked-long/int are host natives (converters.ss).
|
||||||
(defn unchecked-byte [x] (bit-and (int x) 0xff))
|
(defn unchecked-byte [x]
|
||||||
(defn unchecked-short [x] (bit-and (int x) 0xffff))
|
(let [b (bit-and (unchecked-long x) 0xff)] (if (< b 128) b (- b 256))))
|
||||||
(defn unchecked-char [x] (char (bit-and (int x) 0xffff)))
|
(defn unchecked-short [x]
|
||||||
|
(let [s (bit-and (unchecked-long x) 0xffff)] (if (< s 32768) s (- s 65536))))
|
||||||
|
(defn unchecked-char [x] (char (bit-and (unchecked-long x) 0xffff)))
|
||||||
(defn unchecked-float [x] (double x))
|
(defn unchecked-float [x] (double x))
|
||||||
(defn unchecked-double [x] (double x))
|
(defn unchecked-double [x] (double x))
|
||||||
|
|
||||||
|
|
@ -300,7 +325,14 @@
|
||||||
|
|
||||||
;; --- JVM-shape stubs and trivial shells --------------------------------------
|
;; --- JVM-shape stubs and trivial shells --------------------------------------
|
||||||
;; Pure compositions or documented jolt stubs; the host keeps nothing.
|
;; Pure compositions or documented jolt stubs; the host keeps nothing.
|
||||||
(defn enumeration-seq [e] (seq e))
|
;; enumeration-seq drives a java.util.Enumeration (StringTokenizer, etc.) through
|
||||||
|
;; hasMoreElements/nextElement, like the JVM; an already-seqable arg (a jolt seq —
|
||||||
|
;; some host code passes a list) just seqs.
|
||||||
|
(defn enumeration-seq [e]
|
||||||
|
(if (or (nil? e) (seq? e) (sequential? e))
|
||||||
|
(seq e)
|
||||||
|
(lazy-seq (when (.hasMoreElements e)
|
||||||
|
(cons (.nextElement e) (enumeration-seq e))))))
|
||||||
(defn iterator-seq [i] (seq i))
|
(defn iterator-seq [i] (seq i))
|
||||||
|
|
||||||
;; jolt is single-threaded: a promise is an atom, deref never blocks
|
;; jolt is single-threaded: a promise is an atom, deref never blocks
|
||||||
|
|
@ -316,7 +348,8 @@
|
||||||
;; stays an unevaluated reader form on jolt and contains? can't see into it.
|
;; stays an unevaluated reader form on jolt and contains? can't see into it.
|
||||||
(def ^:private special-syms
|
(def ^:private special-syms
|
||||||
#{'if 'do 'let* 'fn* 'quote 'var 'def 'loop* 'recur 'throw 'try 'catch
|
#{'if 'do 'let* 'fn* 'quote 'var 'def 'loop* 'recur 'throw 'try 'catch
|
||||||
'finally 'new 'set! '. 'monitor-enter 'monitor-exit})
|
'finally 'new 'set! '. 'monitor-enter 'monitor-exit
|
||||||
|
'& 'case* 'deftype* 'letfn* 'reify*})
|
||||||
|
|
||||||
(defn special-symbol? [s] (contains? special-syms s))
|
(defn special-symbol? [s] (contains? special-syms s))
|
||||||
|
|
||||||
|
|
@ -331,3 +364,14 @@
|
||||||
(defn proxy-super [& args] (throw "proxy-super: JVM proxies are not supported in Jolt"))
|
(defn proxy-super [& args] (throw "proxy-super: JVM proxies are not supported in Jolt"))
|
||||||
(defn construct-proxy [c & args] (throw "construct-proxy: not supported in Jolt"))
|
(defn construct-proxy [c & args] (throw "construct-proxy: not supported in Jolt"))
|
||||||
(defn get-proxy-class [& interfaces] (throw "get-proxy-class: not supported in Jolt"))
|
(defn get-proxy-class [& interfaces] (throw "get-proxy-class: not supported in Jolt"))
|
||||||
|
|
||||||
|
;; resolve, requiring the symbol's namespace first when it isn't loaded yet —
|
||||||
|
;; the dynamic-require pattern (tooling, plugin registries). The require and
|
||||||
|
;; resolve are the runtime fns, so this works identically under joltc run and
|
||||||
|
;; in an AOT binary (which compiles the namespace from the source roots).
|
||||||
|
(defn requiring-resolve [sym]
|
||||||
|
(if (qualified-symbol? sym)
|
||||||
|
(or (resolve sym)
|
||||||
|
(do (require (symbol (namespace sym)))
|
||||||
|
(resolve sym)))
|
||||||
|
(throw (new IllegalArgumentException (str "Not a qualified symbol: " sym)))))
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,18 @@
|
||||||
(let [args (if (string? (first args)) (rest args) args)
|
(let [args (if (string? (first args)) (rest args) args)
|
||||||
args (if (and (map? (first args)) (not (symbol? (first args)))) (rest args) args)
|
args (if (and (map? (first args)) (not (symbol? (first args)))) (rest args) args)
|
||||||
dispatch (first args)
|
dispatch (first args)
|
||||||
opts (rest args)]
|
opts (rest args)
|
||||||
`(defmulti-setup (quote ~name) ~dispatch ~@opts)))
|
;; qualify with the EXPANSION ns: a defmulti deferred inside a fn (a
|
||||||
|
;; deftest body) must still define in the ns it was written in.
|
||||||
|
qname (symbol (str (clojure.core/ns-name clojure.core/*ns*))
|
||||||
|
(clojure.core/name name))]
|
||||||
|
`(defmulti-setup (quote ~qname) ~dispatch ~@opts)))
|
||||||
|
|
||||||
(defmacro defmethod [mm dispatch-val & fn-tail]
|
(defmacro defmethod [mm dispatch-val & fn-tail]
|
||||||
`(defmethod-setup (quote ~mm) ~dispatch-val (fn ~@fn-tail)))
|
;; the expansion ns rides along so a deferred defmethod resolves its multifn
|
||||||
|
;; against the ns it was written in (aliases and refers included).
|
||||||
|
`(defmethod-setup (quote ~mm) ~dispatch-val (fn ~@fn-tail)
|
||||||
|
~(str (clojure.core/ns-name clojure.core/*ns*))))
|
||||||
|
|
||||||
;; Multimethod table ops: a multimethod's method table lives on its
|
;; Multimethod table ops: a multimethod's method table lives on its
|
||||||
;; VAR (the value is just the dispatch closure), so these pass the name quoted
|
;; VAR (the value is just the dispatch closure), so these pass the name quoted
|
||||||
|
|
@ -109,11 +116,15 @@
|
||||||
(with-open ~(vec (drop 2 bindings)) ~@body)
|
(with-open ~(vec (drop 2 bindings)) ~@body)
|
||||||
(finally (__close ~(first bindings)))))))
|
(finally (__close ~(first bindings)))))))
|
||||||
|
|
||||||
;; jolt numbers are doubles — there is no BigDecimal math context, so the
|
;; Binds *math-context*; BigDecimal arithmetic in the dynamic scope rounds its
|
||||||
;; precision (and optional :rounding mode) is accepted and ignored.
|
;; results to the precision with the rounding mode (default HALF_UP, like
|
||||||
|
;; java.math.MathContext).
|
||||||
(defmacro with-precision [precision & exprs]
|
(defmacro with-precision [precision & exprs]
|
||||||
(let [body (if (= :rounding (first exprs)) (drop 2 exprs) exprs)]
|
(let [[rounding body] (if (= :rounding (first exprs))
|
||||||
`(do ~@body)))
|
[(second exprs) (drop 2 exprs)]
|
||||||
|
['HALF_UP exprs])]
|
||||||
|
`(binding [clojure.core/*math-context* {:precision ~precision :rounding '~rounding}]
|
||||||
|
~@body)))
|
||||||
|
|
||||||
(defmacro with-bindings [binding-map & body]
|
(defmacro with-bindings [binding-map & body]
|
||||||
`(with-bindings* ~binding-map (fn [] ~@body)))
|
`(with-bindings* ~binding-map (fn [] ~@body)))
|
||||||
|
|
@ -385,11 +396,18 @@
|
||||||
;; field binds / live-read instance (see defrecord's mk-clause).
|
;; field binds / live-read instance (see defrecord's mk-clause).
|
||||||
(let [argv (mapv (fn [p] (if (= p (quote _)) (gensym "_p") p)) (nth spec 1))
|
(let [argv (mapv (fn [p] (if (= p (quote _)) (gensym "_p") p)) (nth spec 1))
|
||||||
inst (first argv)
|
inst (first argv)
|
||||||
|
;; A method param shadows a same-named field (Clojure
|
||||||
|
;; semantics): don't let-bind a field the param already
|
||||||
|
;; provides, and treat those params as shadowing so a
|
||||||
|
;; mutable field's live-read rewrite doesn't override them.
|
||||||
|
pnames (set (map name argv))
|
||||||
;; let-bind only immutable fields; mutable ones are read live
|
;; let-bind only immutable fields; mutable ones are read live
|
||||||
;; via rewrite-body so a set! within the method is observed.
|
;; via rewrite-body so a set! within the method is observed.
|
||||||
binds (vec (mapcat (fn [f] [f `(get ~inst ~(keyword (name f)))])
|
binds (vec (mapcat (fn [f] [f `(get ~inst ~(keyword (name f)))])
|
||||||
(filter (fn [f] (not (mutable? f))) fields)))
|
(filter (fn [f] (and (not (mutable? f))
|
||||||
mbody (map (fn [bf] (rewrite-body inst #{} bf)) (drop 2 spec))]
|
(not (contains? pnames (name f)))))
|
||||||
|
fields)))
|
||||||
|
mbody (map (fn [bf] (rewrite-body inst (set argv) bf)) (drop 2 spec))]
|
||||||
(list argv (list* 'let binds mbody))))
|
(list argv (list* 'let binds mbody))))
|
||||||
groups (group-by-head body)
|
groups (group-by-head body)
|
||||||
;; merge clauses by method NAME across ALL protocols into one multi-arity
|
;; merge clauses by method NAME across ALL protocols into one multi-arity
|
||||||
|
|
@ -616,7 +634,11 @@
|
||||||
(let [argv (mapv (fn [p] (if (= p (quote _)) (gensym "_p") p)) (nth spec 1))
|
(let [argv (mapv (fn [p] (if (= p (quote _)) (gensym "_p") p)) (nth spec 1))
|
||||||
inst (first argv)
|
inst (first argv)
|
||||||
hinted (assoc argv 0 (vary-meta inst assoc :tag (name name-sym)))
|
hinted (assoc argv 0 (vary-meta inst assoc :tag (name name-sym)))
|
||||||
binds (vec (mapcat (fn [f] [f `(get ~inst ~(keyword (name f)))]) fields))]
|
;; a method param shadows a same-named field (Clojure
|
||||||
|
;; semantics), so don't rebind a field the param provides.
|
||||||
|
pnames (set (map name argv))
|
||||||
|
binds (vec (mapcat (fn [f] [f `(get ~inst ~(keyword (name f)))])
|
||||||
|
(remove (fn [f] (contains? pnames (name f))) fields)))]
|
||||||
(list hinted (list* 'let binds (drop 2 spec)))))
|
(list hinted (list* 'let binds (drop 2 spec)))))
|
||||||
groups (group-by-head body)
|
groups (group-by-head body)
|
||||||
;; merge clauses by name across protocols into one multi-arity fn (see
|
;; merge clauses by name across protocols into one multi-arity fn (see
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
unchecked-math?
|
unchecked-math?
|
||||||
form-macro? form-expand-1 resolve-global
|
form-macro? form-expand-1 resolve-global
|
||||||
form-sym-meta form-coll-meta host-intern! form-syntax-quote-lower
|
form-sym-meta form-coll-meta host-intern! form-syntax-quote-lower
|
||||||
record-type? record-ctor-key form-position late-bind?
|
record-type? record-ctor-key deftype-ctor-class form-position late-bind?
|
||||||
resolve-class-hint]]))
|
resolve-class-hint]]))
|
||||||
|
|
||||||
(declare analyze)
|
(declare analyze)
|
||||||
|
|
@ -258,6 +258,7 @@
|
||||||
(let [n {:op :try :body (analyze-seq ctx @body env)}
|
(let [n {:op :try :body (analyze-seq ctx @body env)}
|
||||||
n (if (seq @catches)
|
n (if (seq @catches)
|
||||||
(let [evar-name (gen-name "catch")
|
(let [evar-name (gen-name "catch")
|
||||||
|
raw-name (gen-name "catch-raw")
|
||||||
evar (symbol evar-name)
|
evar (symbol evar-name)
|
||||||
dispatch
|
dispatch
|
||||||
(reduce
|
(reduce
|
||||||
|
|
@ -278,6 +279,7 @@
|
||||||
(list 'throw evar)
|
(list 'throw evar)
|
||||||
(reverse @catches))]
|
(reverse @catches))]
|
||||||
(assoc n :catch-sym evar-name
|
(assoc n :catch-sym evar-name
|
||||||
|
:catch-raw-sym raw-name
|
||||||
:catch-body (analyze-seq ctx (list dispatch)
|
:catch-body (analyze-seq ctx (list dispatch)
|
||||||
(add-locals env [evar-name]))))
|
(add-locals env [evar-name]))))
|
||||||
n)
|
n)
|
||||||
|
|
@ -392,7 +394,18 @@
|
||||||
|
|
||||||
(defn- analyze-special [ctx op items env]
|
(defn- analyze-special [ctx op items env]
|
||||||
(case op
|
(case op
|
||||||
"quote" (quote-node (second items))
|
;; A quoted collection keeps its USER metadata (rewrite-clj coerces
|
||||||
|
;; '^:x (4 5 6) and expects the meta back), but not the reader's location keys
|
||||||
|
;; (:line/:column/:file) — like Clojure, which strips those from a quoted
|
||||||
|
;; constant. The kept metadata is itself part of the literal, so quote it.
|
||||||
|
"quote" (let [qf (second items)
|
||||||
|
m (form-coll-meta qf)
|
||||||
|
m (when (map? m)
|
||||||
|
(let [u (dissoc m :line :column :end-line :end-column :file)]
|
||||||
|
(when (seq u) u)))]
|
||||||
|
(if (nil? m)
|
||||||
|
(quote-node qf)
|
||||||
|
(invoke (var-ref "clojure.core" "with-meta") [(quote-node qf) (quote-node m)])))
|
||||||
"if" (do
|
"if" (do
|
||||||
;; 2 or 3 argument forms only (spec 03-special-forms X1)
|
;; 2 or 3 argument forms only (spec 03-special-forms X1)
|
||||||
(when (or (< (count items) 3) (> (count items) 4))
|
(when (or (< (count items) 3) (> (count items) 4))
|
||||||
|
|
@ -480,7 +493,12 @@
|
||||||
;; token and the analyzed args. The Chez back end lowers it to a runtime
|
;; token and the analyzed args. The Chez back end lowers it to a runtime
|
||||||
;; constructor dispatch.
|
;; constructor dispatch.
|
||||||
(defn- analyze-ctor [ctx class args env]
|
(defn- analyze-ctor [ctx class args env]
|
||||||
(host-new class (mapv #(analyze ctx % env) args)))
|
;; Qualify a bare (Name. …) to its deftype's FQN when THIS ns defined the deftype,
|
||||||
|
;; so a deftype named like a built-in host class (tools.reader's PushbackReader)
|
||||||
|
;; resolves to the deftype here while an unrelated ns's bare (PushbackReader. …)
|
||||||
|
;; still reaches java.io.PushbackReader.
|
||||||
|
(host-new (or (deftype-ctor-class ctx class) class)
|
||||||
|
(mapv #(analyze ctx % env) args)))
|
||||||
|
|
||||||
;; jolt.ffi/__cfn: the low-level foreign-function form a jolt library
|
;; jolt.ffi/__cfn: the low-level foreign-function form a jolt library
|
||||||
;; uses (via the jolt.ffi/foreign-fn macro) to bind native code. Shape:
|
;; uses (via the jolt.ffi/foreign-fn macro) to bind native code. Shape:
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,17 @@
|
||||||
|
|
||||||
;; Hot clojure.core primitives lowered to native Scheme.
|
;; Hot clojure.core primitives lowered to native Scheme.
|
||||||
;; `=` is the exactness-aware jolt= from values.ss; inc/dec/
|
;; `=` is the exactness-aware jolt= from values.ss; inc/dec/
|
||||||
;; not are rt shims; mod/rem/quot map to Scheme's (Scheme has all three).
|
;; not are rt shims. Arithmetic and comparisons lower to the jolt-n* checked
|
||||||
|
;; macros (host/chez/seq.ss): the both-Chez-numbers fast path is open-coded and
|
||||||
|
;; anything else (BigDecimal, a non-number) takes the Numbers.ops-style category
|
||||||
|
;; dispatch, with JVM contagion (a double operand wins; an exact zero divisor is
|
||||||
|
;; ArithmeticException; a double zero divisor is ##Inf/##NaN).
|
||||||
(def ^:private native-ops
|
(def ^:private native-ops
|
||||||
{"+" "+" "-" "-" "*" "*" "/" "/"
|
{"+" "jolt-n+" "-" "jolt-n-" "*" "jolt-n*" "/" "jolt-n-div"
|
||||||
"<" "<" ">" ">" "<=" "<=" ">=" ">="
|
"<" "jolt-n<" ">" "jolt-n>" "<=" "jolt-n<=" ">=" "jolt-n>="
|
||||||
"=" "jolt=" "inc" "jolt-inc" "dec" "jolt-dec" "not" "jolt-not"
|
"=" "jolt=" "inc" "jolt-inc" "dec" "jolt-dec" "not" "jolt-not"
|
||||||
"min" "min" "max" "max"
|
"min" "jolt-n-min" "max" "jolt-n-max"
|
||||||
"mod" "modulo" "rem" "remainder" "quot" "quotient"
|
"mod" "jolt-mod" "rem" "jolt-rem" "quot" "jolt-quot"
|
||||||
"vector" "jolt-vector" "hash-map" "jolt-hash-map-fn" "hash-set" "jolt-hash-set"
|
"vector" "jolt-vector" "hash-map" "jolt-hash-map-fn" "hash-set" "jolt-hash-set"
|
||||||
"conj" "jolt-conj" "get" "jolt-get" "nth" "jolt-nth" "count" "jolt-count"
|
"conj" "jolt-conj" "get" "jolt-get" "nth" "jolt-nth" "count" "jolt-count"
|
||||||
"assoc" "jolt-assoc" "dissoc" "jolt-dissoc" "contains?" "jolt-contains?"
|
"assoc" "jolt-assoc" "dissoc" "jolt-dissoc" "contains?" "jolt-contains?"
|
||||||
|
|
@ -53,12 +57,12 @@
|
||||||
"protocol-dispatch3" "protocol-dispatch3"})
|
"protocol-dispatch3" "protocol-dispatch3"})
|
||||||
|
|
||||||
;; Value-position resolution for a clojure.core ref passed AS A VALUE (to map /
|
;; Value-position resolution for a clojure.core ref passed AS A VALUE (to map /
|
||||||
;; filter / reduce / apply). Arithmetic is the exception — Scheme's +/-/*// return
|
;; filter / reduce / apply). The jolt-n* call-position forms are macros, so value
|
||||||
;; EXACT results for exact/zero-arg inputs, breaking the all-double model in
|
;; position substitutes the variadic procedures over the same binary dispatch.
|
||||||
;; higher-order use, so value-position arithmetic routes to the flonum wrappers.
|
|
||||||
(def ^:private core-value-procs
|
(def ^:private core-value-procs
|
||||||
(merge native-ops {"+" "jolt-add" "-" "jolt-sub" "*" "jolt-mul" "/" "jolt-div"
|
(merge native-ops {"+" "jolt-add" "-" "jolt-sub" "*" "jolt-mul" "/" "jolt-div"
|
||||||
"min" "jolt-min" "max" "jolt-max"}))
|
"min" "jolt-min" "max" "jolt-max"
|
||||||
|
"<" "jolt-lt" ">" "jolt-gt" "<=" "jolt-le" ">=" "jolt-ge"}))
|
||||||
|
|
||||||
;; Per-op arity gate: only lower when the Scheme prim and the jolt fn agree at
|
;; Per-op arity gate: only lower when the Scheme prim and the jolt fn agree at
|
||||||
;; this arity. Ops absent from the table are variadic (legal at any arity).
|
;; this arity. Ops absent from the table are variadic (legal at any arity).
|
||||||
|
|
@ -83,7 +87,7 @@
|
||||||
|
|
||||||
;; jolt's comparison ops are vacuously true at arity 1 and DON'T inspect the arg,
|
;; jolt's comparison ops are vacuously true at arity 1 and DON'T inspect the arg,
|
||||||
;; but Scheme's < demands a number even there — special-case.
|
;; but Scheme's < demands a number even there — special-case.
|
||||||
(def ^:private cmp1-ops #{"<" ">" "<=" ">="})
|
(def ^:private cmp1-ops #{"jolt-n<" "jolt-n>" "jolt-n<=" "jolt-n>="})
|
||||||
|
|
||||||
;; Host interop methods with a Chez RT shim (rt.ss jolt-host-call). A `.method`
|
;; Host interop methods with a Chez RT shim (rt.ss jolt-host-call). A `.method`
|
||||||
;; call on any other method routes to record-method-dispatch (a reify/record
|
;; call on any other method routes to record-method-dispatch (a reify/record
|
||||||
|
|
@ -93,7 +97,7 @@
|
||||||
;; Native-op Scheme procedures that return a genuine Scheme boolean (#t/#f), so an
|
;; Native-op Scheme procedures that return a genuine Scheme boolean (#t/#f), so an
|
||||||
;; :if test built from them needs no jolt-truthy? wrapper.
|
;; :if test built from them needs no jolt-truthy? wrapper.
|
||||||
(def ^:private bool-returning-ops
|
(def ^:private bool-returning-ops
|
||||||
#{"<" "<=" ">" ">=" "jolt=" "jolt-not"
|
#{"jolt-n<" "jolt-n<=" "jolt-n>" "jolt-n>=" "jolt=" "jolt-not"
|
||||||
"jolt-even?" "jolt-odd?" "jolt-pos?" "jolt-neg?"
|
"jolt-even?" "jolt-odd?" "jolt-pos?" "jolt-neg?"
|
||||||
"jolt-zero?" "jolt-empty?" "jolt-contains?" "jolt-nil?" "jolt-some?"})
|
"jolt-zero?" "jolt-empty?" "jolt-contains?" "jolt-nil?" "jolt-some?"})
|
||||||
|
|
||||||
|
|
@ -168,6 +172,14 @@
|
||||||
(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.
|
||||||
|
|
@ -187,6 +199,13 @@
|
||||||
;; 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)))
|
||||||
|
|
@ -249,6 +268,17 @@
|
||||||
(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
|
||||||
|
|
@ -377,6 +407,14 @@
|
||||||
;; value, not the raw tagged form). Same emit as the :inst / :uuid IR leaves.
|
;; value, not the raw tagged form). Same emit as the :inst / :uuid IR leaves.
|
||||||
(form-inst? form) (str "(jolt-inst-from-string " (chez-str-lit (form-inst-source form)) ")")
|
(form-inst? form) (str "(jolt-inst-from-string " (chez-str-lit (form-inst-source form)) ")")
|
||||||
(form-uuid? form) (str "(jolt-uuid-from-string " (chez-str-lit (form-uuid-source form)) ")")
|
(form-uuid? form) (str "(jolt-uuid-from-string " (chez-str-lit (form-uuid-source form)) ")")
|
||||||
|
;; a quoted custom #tag with no registered reader -> a tagged-literal value
|
||||||
|
;; (Clojure's reader builds a TaggedLiteral), not the raw reader map. The tag is
|
||||||
|
;; stored as a :#name keyword; strip the leading # to the bare symbol.
|
||||||
|
(and (map? form) (= :jolt/tagged (get form :jolt/type)))
|
||||||
|
(let [nm (name (get form :tag))
|
||||||
|
tsym (if (= \# (first nm)) (subs nm 1) nm)]
|
||||||
|
(str "(jolt-tagged-literal (jolt-symbol #f " (chez-str-lit tsym) ") "
|
||||||
|
(emit-quoted (get form :form)) ")"))
|
||||||
;; plain jolt VALUES (metadata maps and anything nested in them)
|
;; plain jolt VALUES (metadata maps and anything nested in them)
|
||||||
(map? form) (emit-quoted-map-value form)
|
(map? form) (emit-quoted-map-value form)
|
||||||
(vector? form) (str "(jolt-vector " (str/join " " (map emit-quoted form)) ")")
|
(vector? form) (str "(jolt-vector " (str/join " " (map emit-quoted form)) ")")
|
||||||
|
|
@ -401,9 +439,10 @@
|
||||||
;; 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*")
|
||||||
(str "(" kw " (" (str/join " " (map emit-binding (:bindings node))) ") "
|
;; bindings are non-tail; the body inherits the let's tail position
|
||||||
(emit (:body node)) ")")))
|
binds (binding [*tail?* false] (str/join " " (mapv emit-binding (:bindings 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")
|
||||||
|
|
@ -411,9 +450,10 @@
|
||||||
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 (map #(emit (nth % 1)) pairs)
|
inits (binding [*tail?* false] (mapv #(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 "))")))
|
||||||
|
|
||||||
|
|
@ -474,7 +514,11 @@
|
||||||
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")
|
||||||
body (binding [*recur-target* label] (emit (:body a)))
|
ret (:ret-nhint 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 ")")
|
||||||
|
|
@ -499,6 +543,16 @@
|
||||||
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 "
|
||||||
|
|
@ -561,7 +615,30 @@
|
||||||
(= (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)
|
||||||
|
|
@ -574,8 +651,7 @@
|
||||||
;; 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 [[f & as]]
|
(fn [operands] (emit-call tail? "jolt-invoke" operands))))]
|
||||||
(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
|
||||||
|
|
@ -624,11 +700,16 @@
|
||||||
(if idx
|
(if idx
|
||||||
(order-args (fn [as] (str "(jrec-field-at " (first as) " " idx " " (emit fnode) ")")))
|
(order-args (fn [as] (str "(jrec-field-at " (first as) " " idx " " (emit fnode) ")")))
|
||||||
(order-args (fn [as] (str "(jolt-get " (first as) " " (emit fnode) (defstr as) ")")))))
|
(order-args (fn [as] (str "(jolt-get " (first as) " " (emit fnode) (defstr as) ")")))))
|
||||||
;; (coll k [default]) -> (jolt-get coll k [default]) — coll (fnode) is the
|
;; (coll k [default]) -> lookup — coll (fnode) is the callee, evaluated
|
||||||
;; callee, evaluated before the key/default args.
|
;; before the key/default args. A VECTOR literal invokes as nth (a bad
|
||||||
|
;; index throws, IPersistentVector.invoke); maps/sets invoke as get.
|
||||||
(= kind :coll)
|
(= kind :coll)
|
||||||
(ordered-call (cons fnode arg-nodes) (cons (emit fnode) args)
|
(ordered-call (cons fnode arg-nodes) (cons (emit fnode) args)
|
||||||
(fn [[c & as]] (str "(jolt-get " c " " (str/join " " as) ")")))
|
(fn [[c & as]]
|
||||||
|
(str (if (and (= :vector (:op fnode)) (= 1 (count as)))
|
||||||
|
"(jolt-nth "
|
||||||
|
"(jolt-get ")
|
||||||
|
c " " (str/join " " as) ")")))
|
||||||
(and (stdlib-var? fnode) (not (deref prelude-mode?)))
|
(and (stdlib-var? fnode) (not (deref prelude-mode?)))
|
||||||
(throw (ex-info (str "emit: unsupported stdlib fn `" (:ns fnode) "/" (:name fnode)
|
(throw (ex-info (str "emit: unsupported stdlib fn `" (:ns fnode) "/" (:name fnode)
|
||||||
"` (no core on Chez yet)") {}))
|
"` (no core on Chez yet)") {}))
|
||||||
|
|
@ -645,8 +726,7 @@
|
||||||
;; 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] (str "(" (munge-name (:name fnode))
|
(order-args (fn [as] (emit-call tail? (munge-name (:name fnode)) as)))
|
||||||
(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.
|
||||||
|
|
@ -655,8 +735,7 @@
|
||||||
;; 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] (str "(" (dl-name (:ns fnode) (:name fnode))
|
(order-args (fn [as] (emit-call tail? (dl-name (:ns fnode) (:name fnode)) as)))
|
||||||
(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).
|
||||||
|
|
@ -664,16 +743,20 @@
|
||||||
(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 the jolt value RAW (jolt-throw =
|
;; try/catch/finally. throw raises a Chez condition wrapping the jolt value
|
||||||
;; Scheme `raise`); catch lowers to `guard` with an `else` clause (the IR drops
|
;; (jolt-throw = Scheme `raise` of a &jolt-throw condition); catch lowers to
|
||||||
;; the class), finally to `dynamic-wind`'s after-thunk (runs on success, catch and
|
;; `guard`, whose raw binding is unwrapped via jolt-unwrap-throw so the catch var
|
||||||
;; escape — Clojure finally semantics). Both keys optional on the node.
|
;; receives the jolt value (preserving ex-data/ex-message and the backtrace
|
||||||
|
;; identity tag). finally lowers to `dynamic-wind`'s after-thunk (runs on
|
||||||
|
;; success, catch and escape — Clojure finally semantics). Both keys optional.
|
||||||
(defn- emit-try [node]
|
(defn- emit-try [node]
|
||||||
(let [core (if-let [cs (:catch-sym node)]
|
(let [core (if-let [cs (:catch-sym node)]
|
||||||
(str "(guard (" (munge-name cs) " (else " (emit (:catch-body node)) ")) "
|
(let [raw (munge-name (:catch-raw-sym node))]
|
||||||
(emit (:body node)) ")")
|
(str "(guard (" raw " (else (let ((" (munge-name cs) " (jolt-unwrap-throw " raw "))) "
|
||||||
|
(emit (:catch-body node)) "))) "
|
||||||
|
(emit (:body node)) ")"))
|
||||||
(emit (:body node)))]
|
(emit (:body node)))]
|
||||||
(if-let [fin (:finally node)]
|
(if-let [fin (:finally node)]
|
||||||
(str "(dynamic-wind (lambda () #f) (lambda () " core ") (lambda () " (emit fin) "))")
|
(str "(dynamic-wind (lambda () #f) (lambda () " core ") (lambda () " (emit fin) "))")
|
||||||
|
|
@ -707,7 +790,22 @@
|
||||||
(returns-scheme-bool? (:body node) bools'))
|
(returns-scheme-bool? (:body node) bools'))
|
||||||
:else false)))
|
:else false)))
|
||||||
|
|
||||||
(defn emit [node]
|
;; In trace mode, a fn def also registers its source so the tail-frame history maps
|
||||||
|
;; 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))
|
||||||
|
|
@ -755,11 +853,14 @@
|
||||||
: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 (if (returns-scheme-bool? test) (emit test)
|
t (binding [*tail?* false]
|
||||||
(str "(jolt-truthy? " (emit test) ")"))]
|
(if (returns-scheme-bool? test) (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)) ")"))
|
||||||
:do (str "(begin " (str/join " " (map emit (:statements node)))
|
;; non-last statements are non-tail; the ret inherits the do's tail position
|
||||||
|
: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
|
||||||
|
|
@ -803,7 +904,8 @@
|
||||||
: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 (cond
|
:def (let [reg (trace-source-reg node)
|
||||||
|
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))
|
||||||
|
|
@ -811,7 +913,8 @@
|
||||||
(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,
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,10 @@
|
||||||
(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)))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@
|
||||||
|
|
||||||
(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
|
||||||
|
|
@ -28,7 +30,11 @@
|
||||||
(let [c (get spec plat)
|
(let [c (get spec plat)
|
||||||
cands (if (string? c) [c] (vec c))
|
cands (if (string? c) [c] (vec c))
|
||||||
hit (some #(when (jolt.ffi/loaded? %) %) cands)]
|
hit (some #(when (jolt.ffi/loaded? %) %) cands)]
|
||||||
(when (and (nil? hit) (not (:optional spec)))
|
;; A :static spec has no runtime shared object (it's linked into a
|
||||||
|
;; built binary), so an interpreted `run`/`repl` has nothing to load —
|
||||||
|
;; skip it rather than fail. Its foreign calls only resolve in a static
|
||||||
|
;; build; document a dynamic candidate too to use it under `run`.
|
||||||
|
(when (and (nil? hit) (not (:optional spec)) (not (:static spec)))
|
||||||
(throw (ex-info (str "required native library "
|
(throw (ex-info (str "required native library "
|
||||||
(or (:name spec) (first cands) "?")
|
(or (:name spec) (first cands) "?")
|
||||||
" not found — tried " (pr-str cands) " for " (name plat))
|
" not found — tried " (pr-str cands) " for " (name plat))
|
||||||
|
|
@ -88,22 +94,79 @@
|
||||||
(let [{:keys [roots]} (deps/resolve-project (project-dir))]
|
(let [{:keys [roots]} (deps/resolve-project (project-dir))]
|
||||||
(println (str/join ":" roots))))
|
(println (str/join ":" roots))))
|
||||||
|
|
||||||
|
(defn- repl-form-complete?
|
||||||
|
"True when `s` has balanced ()/[]/{}, no open string/char/regex, and at most
|
||||||
|
a trailing comment past the last form. Drives the REPL's read-until-complete
|
||||||
|
decision so a form split across lines is accumulated, not evaluated half-read."
|
||||||
|
[s]
|
||||||
|
(let [n (count s)]
|
||||||
|
(loop [i 0 depth 0 state :code] ; state: :code :string :regex :comment
|
||||||
|
(if (>= i n)
|
||||||
|
(and (<= depth 0) (#{:code :comment} state))
|
||||||
|
(let [c (get s i)]
|
||||||
|
(case state
|
||||||
|
:code (cond
|
||||||
|
(= c \;) (recur (inc i) depth :comment)
|
||||||
|
(= c \\) (recur (+ i 2) depth :code) ; char literal: \(
|
||||||
|
(= c \") (recur (inc i) depth :string)
|
||||||
|
(= c \#) (if (= (get s (inc i)) \")
|
||||||
|
(recur (+ i 2) depth :regex) ; consume the #" together
|
||||||
|
(recur (inc i) depth :code))
|
||||||
|
(#{\( \[ \{} c) (recur (inc i) (inc depth) :code)
|
||||||
|
(#{\) \] \}} c) (recur (inc i) (dec depth) :code)
|
||||||
|
:else (recur (inc i) depth :code))
|
||||||
|
:string (cond
|
||||||
|
(= c \\) (recur (+ i 2) depth :string) ; escaped char
|
||||||
|
(= c \") (recur (inc i) depth :code)
|
||||||
|
:else (recur (inc i) depth :string))
|
||||||
|
:regex (cond
|
||||||
|
(= c \\) (recur (+ i 2) depth :regex)
|
||||||
|
(= c \") (recur (inc i) depth :code)
|
||||||
|
:else (recur (inc i) depth :regex))
|
||||||
|
:comment (recur (inc i) depth
|
||||||
|
(if (#{\newline \return} c) :code :comment))))))))
|
||||||
|
|
||||||
|
(defn- repl-read-form []
|
||||||
|
;; Read lines — printing a secondary prompt for continuations — until the
|
||||||
|
;; accumulated buffer is a complete form. Returns the (possibly multi-line)
|
||||||
|
;; buffer, or nil on EOF at the primary prompt.
|
||||||
|
(loop [buf nil]
|
||||||
|
(print (if buf "... " "user=> ")) (flush)
|
||||||
|
(let [line (read-line)]
|
||||||
|
(cond
|
||||||
|
(nil? line) buf ; EOF: nil at primary, partial mid-form
|
||||||
|
(nil? buf) (cond
|
||||||
|
(str/blank? line) (recur nil) ; skip a blank first line
|
||||||
|
(repl-form-complete? line) line
|
||||||
|
:else (recur line))
|
||||||
|
:else (let [nb (str buf "\n" line)]
|
||||||
|
(if (repl-form-complete? nb) nb (recur nb)))))))
|
||||||
|
|
||||||
(defn- repl []
|
(defn- repl []
|
||||||
;; resolve the project so deps (git libs) are on the roots and native libs are
|
;; resolve the project so deps (git libs) are on the roots and native libs are
|
||||||
;; 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))
|
||||||
(println ";; jolt repl — ^D to exit")
|
;; REPL-driven development: trace by default so an uncaught error in evaluated
|
||||||
|
;; 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 []
|
||||||
(print "user=> ") (flush)
|
(let [form (repl-read-form)]
|
||||||
(let [line (read-line)]
|
(when form
|
||||||
(when line
|
;; :repl/quit / :exit exit the loop — a reliable gesture that works in any
|
||||||
(try (println (pr-str (load-string line)))
|
;; terminal, unlike ^D (some terminals/editors don't deliver it as EOF).
|
||||||
|
(if (#{:repl/quit :exit} (try (read-string form) (catch :default _ nil)))
|
||||||
|
nil
|
||||||
|
(do
|
||||||
|
(try (println (pr-str (load-string form)))
|
||||||
(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)))
|
||||||
(recur)))))
|
(when-let [bt (jolt.host/backtrace-string)]
|
||||||
|
(print bt))))
|
||||||
|
(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 …}.
|
||||||
(defn- run-task [name more]
|
(defn- run-task [name more]
|
||||||
|
|
@ -122,18 +185,38 @@
|
||||||
;; --direct-link (or deps.edn :jolt/build {:direct-link true}) opts into closed-world
|
;; --direct-link (or deps.edn :jolt/build {:direct-link true}) opts into closed-world
|
||||||
;; direct-linking: app->app calls bind directly, giving up runtime redefinition of
|
;; direct-linking: app->app calls bind directly, giving up runtime redefinition of
|
||||||
;; those vars and eval/load-string. Off by default — release stays dynamically linked.
|
;; those vars and eval/load-string. Off by default — release stays dynamically linked.
|
||||||
|
;; The static-link description of a :jolt/native spec for this platform, or nil.
|
||||||
|
;; :static may be flat ({:archive "…"} / {:lib "z" :libdir "…"}) or per-platform
|
||||||
|
;; ({:darwin {…} :linux {…}}). Returns a vector build.ss reads and wraps in the
|
||||||
|
;; platform's force-load flags: ["archive" abspath] or ["lib" name libdir].
|
||||||
|
(defn- static-link-spec [spec plat]
|
||||||
|
(when-let [s (:static spec)]
|
||||||
|
(let [p (get s plat)
|
||||||
|
s (if (map? p) p s)]
|
||||||
|
(cond
|
||||||
|
(:archive s) ["archive" (:archive s)]
|
||||||
|
(:lib s) ["lib" (:lib s) (or (:libdir s) "")]
|
||||||
|
:else nil))))
|
||||||
|
|
||||||
;; Encode a deps.edn :jolt/native spec for the build launcher, resolving the
|
;; Encode a deps.edn :jolt/native spec for the build launcher, resolving the
|
||||||
;; current platform's candidate list now (the binary runs on this OS). Each entry
|
;; current platform's candidate list now (the binary runs on this OS). Each entry
|
||||||
;; becomes a vector the launcher (build.ss) reads: ["process"] for the running
|
;; becomes a vector the launcher (build.ss) reads:
|
||||||
;; binary's own symbols, else ["req"|"opt" cand…] to try in turn.
|
;; ["process"] — the running binary's own symbols (libc)
|
||||||
(defn- encode-natives [natives]
|
;; ["static" form …] — the lib's archive, cc-linked into the binary; its
|
||||||
|
;; symbols load from the process (default when :static
|
||||||
|
;; is present and --dynamic wasn't passed)
|
||||||
|
;; ["req"|"opt" cand…] — load a shared object at runtime, trying each in turn
|
||||||
|
;; dynamic? forces the runtime path for every lib (the --dynamic build flag).
|
||||||
|
(defn- encode-natives [natives dynamic?]
|
||||||
(let [plat (current-platform)]
|
(let [plat (current-platform)]
|
||||||
(vec (for [spec natives]
|
(vec (for [spec natives]
|
||||||
(if (:process spec)
|
(let [static (and (not dynamic?) (static-link-spec spec plat))]
|
||||||
["process"]
|
(cond
|
||||||
(let [c (get spec plat)
|
(:process spec) ["process"]
|
||||||
|
static (into ["static"] static)
|
||||||
|
:else (let [c (get spec plat)
|
||||||
cands (if (string? c) [c] (vec c))]
|
cands (if (string? c) [c] (vec c))]
|
||||||
(into [(if (:optional spec) "opt" "req")] cands)))))))
|
(into [(if (:optional spec) "opt" "req")] cands))))))))
|
||||||
|
|
||||||
(defn- cmd-build [more]
|
(defn- cmd-build [more]
|
||||||
(let [{:keys [project-paths embed-dirs build] :as resolved}
|
(let [{:keys [project-paths embed-dirs build] :as resolved}
|
||||||
|
|
@ -167,7 +250,11 @@
|
||||||
(nil? o) (str pdir "/target/" (if (= mode "dev") "debug" "release") "/" proj)
|
(nil? o) (str pdir "/target/" (if (= mode "dev") "debug" "release") "/" proj)
|
||||||
(str/starts-with? o "/") o
|
(str/starts-with? o "/") o
|
||||||
:else (str pdir "/" o)))
|
:else (str pdir "/" o)))
|
||||||
natives (encode-natives (:natives resolved))
|
;; :jolt/native libs with a :static archive are cc-linked into the
|
||||||
|
;; binary by default; --dynamic (or deps.edn :jolt/build {:dynamic-natives
|
||||||
|
;; true}) keeps the old behavior — load a shared object at runtime.
|
||||||
|
dynamic-natives? (boolean (or (some #{"--dynamic"} more) (:dynamic-natives build)))
|
||||||
|
natives (encode-natives (:natives resolved) dynamic-natives?)
|
||||||
;; closed-world direct-linking is opt-in: the --direct-link flag or a
|
;; closed-world direct-linking is opt-in: the --direct-link flag or a
|
||||||
;; deps.edn :jolt/build {:direct-link true}. Off otherwise.
|
;; deps.edn :jolt/build {:direct-link true}. Off otherwise.
|
||||||
direct-link? (boolean (or (some #{"--direct-link"} more) (:direct-link build)))
|
direct-link? (boolean (or (some #{"--direct-link"} more) (:direct-link build)))
|
||||||
|
|
@ -195,23 +282,38 @@
|
||||||
;; the GUI main loop: glimmer's run marshals its startup here via
|
;; the GUI main loop: glimmer's run marshals its startup here via
|
||||||
;; jolt.host/call-on-main-thread — on macOS GTK quartz, g_application_run
|
;; jolt.host/call-on-main-thread — on macOS GTK quartz, g_application_run
|
||||||
;; must run on the main thread or AppKit aborts when it sets the main menu.
|
;; must run on the main thread or AppKit aborts when it sets the main menu.
|
||||||
|
;; Block SIGINT in this (primordial) thread before starting the server so the
|
||||||
|
;; accept-loop future — and the conn-handler futures it spawns — inherit a
|
||||||
|
;; blocked SIGINT mask. Without this, ^C lands on the accept loop blocked in
|
||||||
|
;; c-accept (a foreign call), where Chez can't fire the keyboard-interrupt
|
||||||
|
;; handler, and the server hangs. park-until-interrupt unblocks SIGINT here
|
||||||
|
;; once its own ^C handler is installed, so ^C reaches this thread and the
|
||||||
|
;; shutdown hooks run cleanly.
|
||||||
|
(jolt.host/block-sigint)
|
||||||
(let [stop ((resolve 'jolt.nrepl/start) port (:nrepl-middleware resolved))]
|
(let [stop ((resolve 'jolt.nrepl/start) port (:nrepl-middleware resolved))]
|
||||||
;; park here until something calls jolt.host/stop-main-pump, then shut the
|
;; register stop so ^C (handled by park-until-interrupt) closes the socket
|
||||||
;; server down cleanly (close the socket, remove .nrepl-port) and return.
|
;; and drops .nrepl-port on the way out.
|
||||||
(jolt.host/run-main-pump)
|
(jolt.host/add-shutdown-hook stop)
|
||||||
|
;; park here until ^C (handled by park-until-interrupt's keyboard-interrupt-
|
||||||
|
;; handler, which runs the shutdown hooks and exits). The accept loop
|
||||||
|
;; inherited SIGINT-blocked above, so ^C is delivered to this thread.
|
||||||
|
(jolt.host/park-until-interrupt)
|
||||||
(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] compile a standalone binary")
|
(println " build -m NS [-o OUT] [--opt|--dev] [--direct-link] [--tree-shake] [--dynamic] compile a standalone binary")
|
||||||
(println " -M:alias [args] run the alias's :main-opts")
|
(println " -M:alias [args] run the alias's :main-opts")
|
||||||
(println " -A:alias [args] add the alias's paths/deps")
|
(println " -A:alias [args] add the alias's paths/deps")
|
||||||
(println " repl start a line REPL")
|
(println " repl start a line REPL")
|
||||||
(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]
|
||||||
|
|
@ -220,6 +322,7 @@
|
||||||
(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)
|
||||||
|
|
|
||||||
|
|
@ -21,25 +21,66 @@
|
||||||
[jolt.ffi :as ffi]))
|
[jolt.ffi :as ffi]))
|
||||||
|
|
||||||
;; --- sockets (loopback server) ---------------------------------------------
|
;; --- sockets (loopback server) ---------------------------------------------
|
||||||
;; Load libc (the running process's symbols) BEFORE the foreign-fn bindings below
|
(def ^:private os-name
|
||||||
;; — defcfn resolves the C entry point when the def is evaluated (at ns load), so
|
(str/lower-case (or (System/getProperty "os.name") "")))
|
||||||
;; the socket symbols must already be available.
|
(def ^:private macos? (str/includes? os-name "mac"))
|
||||||
(ffi/load-library)
|
(def ^:private windows? (str/includes? os-name "win"))
|
||||||
|
|
||||||
|
;; 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)
|
||||||
(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)
|
;; recv/send and the socket-close call differ by platform. Winsock's recv/send
|
||||||
(ffi/defcfn c-close "close" [:int] :int)
|
;; 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-send "send" [:int :pointer :size_t :int] :ssize_t :blocking)
|
||||||
|
(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)
|
||||||
(def ^:private macos?
|
;; SOL_SOCKET / SO_REUSEADDR: 0xffff / 4 on macOS and Windows, 1 / 2 on Linux.
|
||||||
(str/includes? (str/lower-case (or (System/getProperty "os.name") "")) "mac"))
|
(def ^:private sol-socket (if (or macos? windows?) 0xffff 1))
|
||||||
(def ^:private sol-socket (if macos? 0xffff 1))
|
(def ^:private so-reuse (if (or macos? windows?) 4 2))
|
||||||
(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)]
|
||||||
|
|
@ -53,7 +94,7 @@
|
||||||
sa))
|
sa))
|
||||||
|
|
||||||
(defn- listen-socket [port]
|
(defn- listen-socket [port]
|
||||||
(ffi/load-library) ; libc process symbols
|
(ensure-winsock!) ; no-op off Windows
|
||||||
(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))
|
||||||
|
|
@ -147,7 +188,10 @@
|
||||||
(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 (reset! err (err-msg e)))))]
|
(catch :default 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*))
|
||||||
|
|
@ -236,11 +280,17 @@
|
||||||
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 "nREPL server started on port " port " (127.0.0.1) — .nrepl-port written"))
|
(println (str "jolt " (jolt.host/jolt-version) " nREPL server started on port "
|
||||||
|
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
|
||||||
|
|
@ -256,5 +306,5 @@
|
||||||
(fn stop []
|
(fn stop []
|
||||||
(when (compare-and-set! stopped false true)
|
(when (compare-and-set! stopped false true)
|
||||||
(c-close fd)
|
(c-close fd)
|
||||||
(try (io/delete-file ".nrepl-port" true) (catch :default _ nil)))
|
(jolt.host/delete-file ".nrepl-port"))
|
||||||
nil))))
|
nil))))
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,12 @@
|
||||||
ls (lng-spec nm n)
|
ls (lng-spec nm n)
|
||||||
bs (bd-spec nm n)]
|
bs (bd-spec nm n)]
|
||||||
(cond
|
(cond
|
||||||
(and ds (ok? :double :double))
|
(and ds (ok? :double :double)
|
||||||
|
;; min/max return the ORIGINAL operand (Numbers.min: an integer
|
||||||
|
;; literal stays exact), so an int-literal operand blocks the
|
||||||
|
;; flonum lowering there — flmin would coerce it.
|
||||||
|
(or (not (contains? #{"min" "max"} nm))
|
||||||
|
(every? (fn [c] (= c :double)) cls)))
|
||||||
;; coerce integer-literal operands to flonum so fl-ops never see an exact int.
|
;; coerce integer-literal operands to flonum so fl-ops never see an exact int.
|
||||||
(let [args' (mapv (fn [nd] (if (int-lit? nd) (assoc nd :val (double (get nd :val))) nd))
|
(let [args' (mapv (fn [nd] (if (int-lit? nd) (assoc nd :val (double (get nd :val))) nd))
|
||||||
argnodes)]
|
argnodes)]
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,15 @@
|
||||||
;; Reader FORMS are detected by :jolt/type tag, never by map? — strict map?
|
;; Reader FORMS are detected by :jolt/type tag, never by map? — strict map?
|
||||||
;; (correctly) excludes tagged structs, so the old (and (map? x) ...) guard
|
;; (correctly) excludes tagged structs, so the old (and (map? x) ...) guard
|
||||||
;; would skip them.
|
;; would skip them.
|
||||||
(= :jolt/set (get x :jolt/type)) (with-meta (set (map (fn [v] (edn->value opts v)) (get x :value))) (edn->value opts (meta x)))
|
(= :jolt/set (get x :jolt/type))
|
||||||
|
(let [vs (map (fn [v] (edn->value opts v)) (get x :value))
|
||||||
|
st (set vs)]
|
||||||
|
;; duplicate literal elements are invalid edn
|
||||||
|
(when (< (count st) (count vs))
|
||||||
|
(throw (new IllegalArgumentException
|
||||||
|
(str "Duplicate key: " (pr-str (some (fn [[k n]] (when (< 1 n) k))
|
||||||
|
(frequencies vs)))))))
|
||||||
|
(with-meta st (edn->value opts (meta x))))
|
||||||
;; Tagged elements: a reader from the :readers opt wins, then the built-in
|
;; Tagged elements: a reader from the :readers opt wins, then the built-in
|
||||||
;; data readers (#uuid/#inst + registered); an unknown tag falls to the
|
;; data readers (#uuid/#inst + registered); an unknown tag falls to the
|
||||||
;; :default opt fn (called with tag and value, as in Clojure) or throws.
|
;; :default opt fn (called with tag and value, as in Clojure) or throws.
|
||||||
|
|
@ -30,6 +38,9 @@
|
||||||
custom (get (get opts :readers) tag-sym)]
|
custom (get (get opts :readers) tag-sym)]
|
||||||
(cond
|
(cond
|
||||||
custom (custom v)
|
custom (custom v)
|
||||||
|
;; the built-in edn tags win over :default (a :readers entry can
|
||||||
|
;; override them; an unknown-tag :default never sees #inst/#uuid)
|
||||||
|
(contains? #{'inst 'uuid 'bigdec} tag-sym) (__read-tagged tag v)
|
||||||
;; Clojure calls :default with the tag as a SYMBOL and the value.
|
;; Clojure calls :default with the tag as a SYMBOL and the value.
|
||||||
(get opts :default) ((get opts :default) tag-sym v)
|
(get opts :default) ((get opts :default) tag-sym v)
|
||||||
:else (__read-tagged tag v)))
|
:else (__read-tagged tag v)))
|
||||||
|
|
@ -39,25 +50,30 @@
|
||||||
;; a constructed set: recurse into its elements too, so a tagged literal
|
;; a constructed set: recurse into its elements too, so a tagged literal
|
||||||
;; inside #{…} gets the :readers/:default treatment (aero's #ref in a set).
|
;; inside #{…} gets the :readers/:default treatment (aero's #ref in a set).
|
||||||
(set? x) (with-meta (set (map (fn [v] (edn->value opts v)) x)) (edn->value opts (meta x)))
|
(set? x) (with-meta (set (map (fn [v] (edn->value opts v)) x)) (edn->value opts (meta x)))
|
||||||
(seq? x) (with-meta (map (fn [v] (edn->value opts v)) x) (edn->value opts (meta x)))
|
;; edn lists are lists (list? holds), not lazy seqs
|
||||||
|
(seq? x) (with-meta (apply list (map (fn [v] (edn->value opts v)) x)) (edn->value opts (meta x)))
|
||||||
:else x))
|
:else x))
|
||||||
|
|
||||||
;; Private helper, NOT named read-string: an unqualified (read-string …) call
|
;; Private helper, NOT named read-string: an unqualified (read-string …) call
|
||||||
;; dispatches the core read-string SPECIAL FORM (by name, regardless of ns), so
|
;; dispatches the core read-string SPECIAL FORM (by name, regardless of ns), so
|
||||||
;; the 1-arity can't delegate to the 2-arity through that name.
|
;; the 1-arity can't delegate to the 2-arity through that name.
|
||||||
(defn- read-edn [opts s]
|
(defn- read-edn [opts s]
|
||||||
(if (or (nil? s) (cstr/blank? s))
|
;; the strict edn seam: no auto-resolved keywords, invalid tokens throw, and
|
||||||
(get opts :eof nil)
|
;; each #_ discard is validated through the same :readers/:default pipeline.
|
||||||
;; read the RAW form (tagged/set literals stay forms) so edn->value applies
|
;; EOF (blank/comment-only/nil input) honors :eof; an opts map WITHOUT :eof
|
||||||
;; every #tag through :readers/:default — read-string would build the built-in
|
;; makes end-of-input an error, like the reference.
|
||||||
;; #inst/#uuid eagerly, ignoring an override and failing on a non-string form.
|
(let [v (__read-form-edn s (fn [form] (edn->value opts form) nil))]
|
||||||
(edn->value opts (__read-form-raw s))))
|
(if (= v :jolt/reader-eof)
|
||||||
|
(if (contains? opts :eof)
|
||||||
|
(get opts :eof)
|
||||||
|
(throw (ex-info "EOF while reading" {})))
|
||||||
|
(edn->value opts v))))
|
||||||
|
|
||||||
(defn read-string
|
(defn read-string
|
||||||
"Reads one object from the string s. Returns the :eof option value (default
|
"Reads one object from the string s. The no-opts arity returns nil at end of
|
||||||
nil) for nil or blank input. opts is an options map; :eof sets the value
|
input; with an opts map, :eof sets the value returned at end of input and its
|
||||||
returned at end of input."
|
absence makes end-of-input an error."
|
||||||
([s] (read-edn {} s))
|
([s] (read-edn {:eof nil} s))
|
||||||
([opts s] (read-edn opts s)))
|
([opts s] (read-edn opts s)))
|
||||||
|
|
||||||
(defn- drain-reader
|
(defn- drain-reader
|
||||||
|
|
|
||||||
|
|
@ -6,28 +6,32 @@
|
||||||
(if (nil? s) true
|
(if (nil? s) true
|
||||||
(= 0 (count (str-trim s)))))
|
(= 0 (count (str-trim s)))))
|
||||||
|
|
||||||
|
;; The case fns and the searches take any Object s through its toString, like
|
||||||
|
;; the reference ((upper-case :kw) is ":KW", (capitalize 1) is "1"); nil throws
|
||||||
|
;; like calling a method on null.
|
||||||
|
(defn- to-str [s]
|
||||||
|
(if (nil? s)
|
||||||
|
(throw (new NullPointerException "s"))
|
||||||
|
(.toString s)))
|
||||||
(defn capitalize
|
(defn capitalize
|
||||||
|
|
||||||
[s]
|
[s]
|
||||||
|
(let [s (to-str s)]
|
||||||
(if (< 1 (count s))
|
(if (< 1 (count s))
|
||||||
(str (str-upper (subs s 0 1))
|
(str (str-upper (subs s 0 1))
|
||||||
(str-lower (subs s 1)))
|
(str-lower (subs s 1)))
|
||||||
(str-upper s)))
|
(str-upper s))))
|
||||||
|
|
||||||
(defn lower-case
|
(defn lower-case
|
||||||
|
|
||||||
[s]
|
[s]
|
||||||
(str-lower s))
|
(str-lower (to-str s)))
|
||||||
|
|
||||||
(defn upper-case
|
(defn upper-case
|
||||||
|
|
||||||
[s]
|
[s]
|
||||||
(str-upper s))
|
(str-upper (to-str s)))
|
||||||
|
|
||||||
(defn includes?
|
(defn includes?
|
||||||
|
|
||||||
[s substr]
|
[s substr]
|
||||||
(not (nil? (str-find substr s))))
|
(not (nil? (str-find substr (to-str s)))))
|
||||||
|
|
||||||
(defn join
|
(defn join
|
||||||
|
|
||||||
|
|
@ -36,11 +40,11 @@
|
||||||
|
|
||||||
(defn replace
|
(defn replace
|
||||||
[s match replacement]
|
[s match replacement]
|
||||||
(str-replace-all match replacement s))
|
(str-replace-all match replacement (to-str s)))
|
||||||
|
|
||||||
(defn replace-first
|
(defn replace-first
|
||||||
[s match replacement]
|
[s match replacement]
|
||||||
(str-replace match replacement s))
|
(str-replace match replacement (to-str s)))
|
||||||
|
|
||||||
(defn reverse
|
(defn reverse
|
||||||
[s]
|
[s]
|
||||||
|
|
@ -68,16 +72,18 @@
|
||||||
(vec (str-split #"\r?\n" s)))
|
(vec (str-split #"\r?\n" s)))
|
||||||
|
|
||||||
(defn starts-with?
|
(defn starts-with?
|
||||||
|
|
||||||
[s substr]
|
[s substr]
|
||||||
(let [slen (count s) slen2 (count substr)]
|
(when (nil? substr) (throw (new NullPointerException "substr")))
|
||||||
|
(let [s (to-str s)
|
||||||
|
slen (count s) slen2 (count substr)]
|
||||||
(and (>= slen slen2)
|
(and (>= slen slen2)
|
||||||
(= (subs s 0 slen2) substr))))
|
(= (subs s 0 slen2) substr))))
|
||||||
|
|
||||||
(defn ends-with?
|
(defn ends-with?
|
||||||
|
|
||||||
[s substr]
|
[s substr]
|
||||||
(let [slen (count s) slen2 (count substr)]
|
(when (nil? substr) (throw (new NullPointerException "substr")))
|
||||||
|
(let [s (to-str s)
|
||||||
|
slen (count s) slen2 (count substr)]
|
||||||
(and (>= slen slen2)
|
(and (>= slen slen2)
|
||||||
(= (subs s (- slen slen2)) substr))))
|
(= (subs s (- slen slen2)) substr))))
|
||||||
|
|
||||||
|
|
@ -97,8 +103,8 @@
|
||||||
(str-trimr s))
|
(str-trimr s))
|
||||||
|
|
||||||
(defn escape
|
(defn escape
|
||||||
|
|
||||||
[s cmap]
|
[s cmap]
|
||||||
|
(when (nil? s) (throw (new NullPointerException "s")))
|
||||||
(apply str
|
(apply str
|
||||||
(map (fn [ch]
|
(map (fn [ch]
|
||||||
(if-let [rep (cmap ch)] rep (str ch)))
|
(if-let [rep (cmap ch)] rep (str ch)))
|
||||||
|
|
@ -107,9 +113,9 @@
|
||||||
(defn index-of
|
(defn index-of
|
||||||
"0-based index of the first occurrence of value in s, or nil."
|
"0-based index of the first occurrence of value in s, or nil."
|
||||||
([s value]
|
([s value]
|
||||||
(str-find value s))
|
(str-find value (to-str s)))
|
||||||
([s value from]
|
([s value from]
|
||||||
(let [idx (str-find value (subs s from))]
|
(let [idx (str-find value (subs (to-str s) from))]
|
||||||
(when idx (+ from idx)))))
|
(when idx (+ from idx)))))
|
||||||
|
|
||||||
(defn last-index-of
|
(defn last-index-of
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@
|
||||||
; is a drop-in superset.
|
; is a drop-in superset.
|
||||||
|
|
||||||
(ns clojure.test
|
(ns clojure.test
|
||||||
(:require [clojure.string :as str]))
|
(:require [clojure.string :as str]
|
||||||
|
[clojure.template :as temp]))
|
||||||
|
|
||||||
;; --- state -----------------------------------------------------------------
|
;; --- state -----------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -19,8 +20,8 @@
|
||||||
(def jolt-report counters) ;; alias used by the suite harness
|
(def jolt-report counters) ;; alias used by the suite harness
|
||||||
(def ctx-stack (atom []))
|
(def ctx-stack (atom []))
|
||||||
(def registry (atom [])) ;; [{:name sym :fn thunk}]
|
(def registry (atom [])) ;; [{:name sym :fn thunk}]
|
||||||
(def once-fixtures (atom []))
|
(def once-fixtures (atom {})) ;; ns-sym -> [fixture-fns]
|
||||||
(def each-fixtures (atom []))
|
(def each-fixtures (atom {})) ;; ns-sym -> [fixture-fns]
|
||||||
|
|
||||||
;; clojure.test/*testing-vars* — the stack of vars under test. Real clojure.test
|
;; clojure.test/*testing-vars* — the stack of vars under test. Real clojure.test
|
||||||
;; binds it around each test var; test.check's default reporter reads it, so a
|
;; binds it around each test var; test.check's default reporter reads it, so a
|
||||||
|
|
@ -32,8 +33,8 @@
|
||||||
(reset! counters {:test 0 :pass 0 :fail 0 :error 0 :fails []})
|
(reset! counters {:test 0 :pass 0 :fail 0 :error 0 :fails []})
|
||||||
(reset! ctx-stack [])
|
(reset! ctx-stack [])
|
||||||
(reset! registry [])
|
(reset! registry [])
|
||||||
(reset! once-fixtures [])
|
(reset! once-fixtures {})
|
||||||
(reset! each-fixtures []))
|
(reset! each-fixtures {}))
|
||||||
|
|
||||||
(defn- ctx-str [] (str/join " " @ctx-stack))
|
(defn- ctx-str [] (str/join " " @ctx-stack))
|
||||||
|
|
||||||
|
|
@ -102,6 +103,12 @@
|
||||||
(clojure.test/do-report {:type :error :message ~msg :form '~form
|
(clojure.test/do-report {:type :error :message ~msg :form '~form
|
||||||
:actual (clojure.test/err-text e#)}))))
|
:actual (clojure.test/err-text e#)}))))
|
||||||
|
|
||||||
|
;; The common pure predicates whose args `is` evaluates so a failure shows the
|
||||||
|
;; actual values — (is (= expected got)) prints `got`, not just the form. A macro
|
||||||
|
;; head (not in this set) keeps the plain form-only path.
|
||||||
|
(def ^:private reported-preds
|
||||||
|
'#{= not= == < > <= >= identical? contains? instance? nil? some? empty? even? odd? pos? neg? zero?})
|
||||||
|
|
||||||
;; --- class matching for thrown? --------------------------------------------
|
;; --- class matching for thrown? --------------------------------------------
|
||||||
|
|
||||||
(defn- last-seg [s]
|
(defn- last-seg [s]
|
||||||
|
|
@ -129,6 +136,15 @@
|
||||||
([form] `(is ~form nil))
|
([form] `(is ~form nil))
|
||||||
([form msg]
|
([form msg]
|
||||||
(cond
|
(cond
|
||||||
|
;; a library-registered custom assertion (the assert-expr extension point)
|
||||||
|
;; wins over every inline path, like clojure.test, where each `is` dispatches
|
||||||
|
;; assert-expr on the exact head symbol and the built-ins are just
|
||||||
|
;; pre-registered methods. In particular a registered alias-qualified
|
||||||
|
;; `p/thrown?` must not be captured by the by-name thrown? path below.
|
||||||
|
(and (seq? form) (symbol? (first form))
|
||||||
|
(contains? (methods clojure.test/assert-expr) (first form)))
|
||||||
|
(clojure.test/assert-expr msg form)
|
||||||
|
|
||||||
;; (is (thrown? Class body...))
|
;; (is (thrown? Class body...))
|
||||||
(thrown-form? form "thrown?")
|
(thrown-form? form "thrown?")
|
||||||
(let [klass-sym (second form)
|
(let [klass-sym (second form)
|
||||||
|
|
@ -166,12 +182,17 @@
|
||||||
(clojure.test/inc-pass!)
|
(clojure.test/inc-pass!)
|
||||||
(clojure.test/fail! (str "expected throw of " ~klass " matching " ~re " but got " (clojure.core/class e#) ": " m#)))))))
|
(clojure.test/fail! (str "expected throw of " ~klass " matching " ~re " but got " (clojure.core/class e#) ": " m#)))))))
|
||||||
|
|
||||||
;; a library-registered custom assertion (the assert-expr extension point);
|
;; a predicate call — (= a b), (< x y), (pred? v): evaluate the args so a
|
||||||
;; only fires for a symbol with an explicitly registered method, so built-in
|
;; failure shows the actual values, like clojure.test's assert-predicate.
|
||||||
;; predicate forms keep the inline path below.
|
(and (seq? form) (contains? clojure.test/reported-preds (first form)))
|
||||||
(and (seq? form) (symbol? (first form))
|
`(try
|
||||||
(contains? (methods clojure.test/assert-expr) (first form)))
|
(let [vs# (list ~@(rest form))]
|
||||||
(clojure.test/assert-expr msg form)
|
(if (apply ~(first form) vs#)
|
||||||
|
(clojure.test/inc-pass!)
|
||||||
|
(clojure.test/fail! (str (pr-str (list '~'not (cons '~(first form) vs#)))
|
||||||
|
(when ~msg (str " — " ~msg))))))
|
||||||
|
(catch Throwable e#
|
||||||
|
(clojure.test/err! (str (pr-str '~form) " threw: " (clojure.test/err-text e#)))))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
`(try
|
`(try
|
||||||
|
|
@ -191,23 +212,33 @@
|
||||||
(defmacro deftest [name & body]
|
(defmacro deftest [name & body]
|
||||||
`(do
|
`(do
|
||||||
(defn ~name [] ~@body)
|
(defn ~name [] ~@body)
|
||||||
(swap! clojure.test/registry conj {:name '~name :fn ~name})
|
(swap! clojure.test/registry conj {:name '~name
|
||||||
|
:ns (clojure.core/ns-name clojure.core/*ns*)
|
||||||
|
:fn ~name})
|
||||||
(var ~name)))
|
(var ~name)))
|
||||||
|
|
||||||
(defmacro are [argv expr & data]
|
;; Template substitution (not let-binding), so argv symbols substitute inside
|
||||||
(let [n (count argv)
|
;; quote and nested forms: (are [x] (special-symbol? 'x) if def) tests 'if.
|
||||||
rows (partition n data)]
|
(defmacro are [argv expr & args]
|
||||||
`(do ~@(map (fn [row]
|
(if (or (and (empty? argv) (empty? args))
|
||||||
`(let [~@(interleave argv row)]
|
(and (pos? (count argv))
|
||||||
(clojure.test/is ~expr)))
|
(pos? (count args))
|
||||||
rows))))
|
(zero? (mod (count args) (count argv)))))
|
||||||
|
`(clojure.template/do-template ~argv (clojure.test/is ~expr) ~@args)
|
||||||
|
(throw (IllegalArgumentException.
|
||||||
|
"The number of args doesn't match are's argv or neither are empty"))))
|
||||||
|
|
||||||
;; --- fixtures + run --------------------------------------------------------
|
;; --- fixtures + run --------------------------------------------------------
|
||||||
|
|
||||||
|
;; Fixtures are per-namespace, like clojure.test (which stores them in ns
|
||||||
|
;; metadata): use-fixtures records them under the calling ns, and only that
|
||||||
|
;; ns's tests run through them — a suite loading many test namespaces into one
|
||||||
|
;; process doesn't cross-apply or clobber another ns's fixtures.
|
||||||
(defn use-fixtures [kind & fns]
|
(defn use-fixtures [kind & fns]
|
||||||
|
(let [n (ns-name *ns*)]
|
||||||
(cond
|
(cond
|
||||||
(= kind :once) (reset! once-fixtures (vec fns))
|
(= kind :once) (swap! once-fixtures assoc n (vec fns))
|
||||||
(= kind :each) (reset! each-fixtures (vec fns))))
|
(= kind :each) (swap! each-fixtures assoc n (vec fns)))))
|
||||||
|
|
||||||
(defn- wrap-fixtures [fixtures body-fn]
|
(defn- wrap-fixtures [fixtures body-fn]
|
||||||
(if (empty? fixtures)
|
(if (empty? fixtures)
|
||||||
|
|
@ -216,25 +247,46 @@
|
||||||
|
|
||||||
(defn- run-one [t]
|
(defn- run-one [t]
|
||||||
(swap! counters update :test inc)
|
(swap! counters update :test inc)
|
||||||
(wrap-fixtures @each-fixtures
|
(wrap-fixtures (get @each-fixtures (:ns t) [])
|
||||||
(fn []
|
(fn []
|
||||||
(try
|
(try
|
||||||
((:fn t))
|
((:fn t))
|
||||||
(catch Throwable e
|
(catch Throwable e
|
||||||
(err! (str (:name t) " crashed: " (err-text e))))))))
|
(err! (str (:name t) " crashed: " (err-text e))))))))
|
||||||
|
|
||||||
(defn run-registered []
|
;; Run the registered tests grouped by namespace (registration order preserved
|
||||||
(doseq [t @registry] (run-one t))
|
;; within each ns), each group wrapped in its ns's :once fixtures. ns-set nil
|
||||||
|
;; means all.
|
||||||
|
(defn- run-selected [ns-set]
|
||||||
|
(let [ts (if ns-set (filter (fn [t] (contains? ns-set (:ns t))) @registry) @registry)]
|
||||||
|
(doseq [n (distinct (map :ns ts))]
|
||||||
|
(wrap-fixtures (get @once-fixtures n [])
|
||||||
|
(fn [] (doseq [t ts :when (= n (:ns t))] (run-one t))))))
|
||||||
nil)
|
nil)
|
||||||
|
|
||||||
(defn run-tests [& _nses]
|
(defn run-registered [] (run-selected nil))
|
||||||
(wrap-fixtures @once-fixtures (fn [] (run-registered)))
|
|
||||||
(let [r @counters]
|
;; (run-tests 'ns1 'ns2 …) runs only those namespaces' tests, like clojure.test.
|
||||||
|
;; With no args it runs everything registered (a deliberate superset of the
|
||||||
|
;; JVM's current-ns default — jolt's harnesses load then run whole suites).
|
||||||
|
;; Prints and returns THIS call's summary; the global counters stay cumulative
|
||||||
|
;; for the n-pass/n-fail harness API.
|
||||||
|
(defn run-tests [& nses]
|
||||||
|
(let [before @counters
|
||||||
|
ns-set (when (seq nses)
|
||||||
|
(set (map (fn [n] (if (symbol? n) n (ns-name n))) nses)))]
|
||||||
|
(run-selected ns-set)
|
||||||
|
(let [r @counters
|
||||||
|
d {:type :summary
|
||||||
|
:test (- (:test r) (:test before))
|
||||||
|
:pass (- (:pass r) (:pass before))
|
||||||
|
:fail (- (:fail r) (:fail before))
|
||||||
|
:error (- (:error r) (:error before))}]
|
||||||
(println)
|
(println)
|
||||||
(println (str "Ran " (:test r) " tests. "
|
(println (str "Ran " (:test d) " tests. "
|
||||||
(:pass r) " assertions passed, "
|
(:pass d) " assertions passed, "
|
||||||
(:fail r) " failures, " (:error r) " errors."))
|
(:fail d) " failures, " (:error d) " errors."))
|
||||||
r))
|
d)))
|
||||||
|
|
||||||
(defn run-test [& _] nil)
|
(defn run-test [& _] nil)
|
||||||
(defn test-var [& _] nil)
|
(defn test-var [& _] nil)
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,10 @@
|
||||||
; concat/lazy-seq) walk too — without this, postwalk-replace silently no-op'd
|
; concat/lazy-seq) walk too — without this, postwalk-replace silently no-op'd
|
||||||
; a quoted list, breaking clojure.template/apply-template
|
; a quoted list, breaking clojure.template/apply-template
|
||||||
(list? form) (outer (with-meta (apply list (map inner form)) (meta form)))
|
(list? form) (outer (with-meta (apply list (map inner form)) (meta form)))
|
||||||
(seq? form) (outer (with-meta (map inner form) (meta form)))
|
; doall like Clojure: walk must be eager so an `inner` with side effects
|
||||||
|
; (rewrite-clj's #() reader bumps an arg-count atom during the walk, read right
|
||||||
|
; after) runs now, not lazily when the result is later realized.
|
||||||
|
(seq? form) (outer (with-meta (doall (map inner form)) (meta form)))
|
||||||
:else (outer form)))
|
:else (outer form)))
|
||||||
|
|
||||||
(defn postwalk
|
(defn postwalk
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ test` from the repo root.
|
||||||
|
|
||||||
## The spec corpus
|
## The spec corpus
|
||||||
|
|
||||||
`corpus.edn` is the contract: ~2920 rows `{:suite :label :expected :actual}`, with
|
`corpus.edn` is the contract: ~3570 rows `{:suite :label :expected :actual :portability}`, with
|
||||||
`:expected` sourced from reference JVM Clojure by `test/conformance/regen-corpus.clj`.
|
`:expected` sourced from reference JVM Clojure by `test/conformance/regen-corpus.clj`.
|
||||||
It is frozen (the canonical source) — add or change cases here, then re-source the
|
It is frozen (the canonical source) — add or change cases here, then re-source the
|
||||||
answers with `regen-corpus.clj` and re-certify with `test/conformance/certify.clj`.
|
answers with `regen-corpus.clj` and re-certify with `test/conformance/certify.clj`.
|
||||||
|
|
@ -22,7 +22,7 @@ answers with `regen-corpus.clj` and re-certify with `test/conformance/certify.cl
|
||||||
|
|
||||||
chez --script host/chez/run-corpus.ss
|
chez --script host/chez/run-corpus.ss
|
||||||
JOLT_CORPUS_LIMIT=200 … # every-Nth stride, fast iteration
|
JOLT_CORPUS_LIMIT=200 … # every-Nth stride, fast iteration
|
||||||
JOLT_CHEZ_ZJ_FLOOR=N … # override the floor (default 2678)
|
JOLT_CHEZ_ZJ_FLOOR=N … # override the floor (see run-corpus.ss)
|
||||||
|
|
||||||
- `run-unit.ss` — host-specific unit cases (`test/chez/unit.edn`) that aren't in the
|
- `run-unit.ss` — host-specific unit cases (`test/chez/unit.edn`) that aren't in the
|
||||||
JVM-portable corpus: dot-forms, java statics, io, reader, walk, vars/namespaces,
|
JVM-portable corpus: dot-forms, java statics, io, reader, walk, vars/namespaces,
|
||||||
|
|
@ -32,6 +32,16 @@ answers with `regen-corpus.clj` and re-certify with `test/conformance/certify.cl
|
||||||
- `selfcheck.sh` — self-host fixpoint: `bootstrap.ss` rebuild byte-equals the
|
- `selfcheck.sh` — self-host fixpoint: `bootstrap.ss` rebuild byte-equals the
|
||||||
checked-in seed (`host/chez/seed/`).
|
checked-in seed (`host/chez/seed/`).
|
||||||
- `smoke.sh` — real `bin/joltc -e` CLI smoke.
|
- `smoke.sh` — real `bin/joltc -e` CLI smoke.
|
||||||
|
- `cts.sh` — the vendored [jank-lang/clojure-test-suite](https://github.com/jank-lang/clojure-test-suite)
|
||||||
|
(`vendor/clojure-test-suite`, a per-core-fn clojure.test suite shared across
|
||||||
|
Clojure dialects), run one namespace per `joltc` process (a hang or crash is
|
||||||
|
contained) through the `test/chez/cts-app` project and `cts-run` runner.
|
||||||
|
Per-namespace fail/error counts must exactly match the checked-in baseline
|
||||||
|
`test/chez/cts-known-failures.txt` — a namespace doing worse fails the gate,
|
||||||
|
and one doing better fails as stale until the baseline is updated in the same
|
||||||
|
change. `make cts`;
|
||||||
|
`JOLT_CTS_NS=ns1,ns2` runs a subset verbosely,
|
||||||
|
`JOLT_CTS_WRITE_BASELINE=1` regenerates the baseline.
|
||||||
|
|
||||||
## Other Chez tests
|
## Other Chez tests
|
||||||
|
|
||||||
|
|
|
||||||
1
test/chez/alias-leak-app/deps.edn
Normal file
1
test/chez/alias-leak-app/deps.edn
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{:paths ["src"]}
|
||||||
3
test/chez/alias-leak-app/src/fix/lib.clj
Normal file
3
test/chez/alias-leak-app/src/fix/lib.clj
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
(ns fix.lib
|
||||||
|
(:require [clojure.set :as ss]))
|
||||||
|
(defn u [] (ss/union #{1} #{2}))
|
||||||
5
test/chez/alias-leak-app/src/fix/main.clj
Normal file
5
test/chez/alias-leak-app/src/fix/main.clj
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
(ns fix.main
|
||||||
|
(:require [clojure.string :as ss]
|
||||||
|
[fix.lib :as lib]))
|
||||||
|
(defn -main [& _]
|
||||||
|
(println (ss/upper-case "hi") (lib/u)))
|
||||||
|
|
@ -11,6 +11,17 @@
|
||||||
(clojure.test/do-report {:type :pass})
|
(clojure.test/do-report {:type :pass})
|
||||||
(clojure.test/do-report {:type :fail :message ~msg :form '~form}))))
|
(clojure.test/do-report {:type :fail :message ~msg :form '~form}))))
|
||||||
|
|
||||||
|
;; an ALIAS-QUALIFIED registered assertion whose simple name collides with the
|
||||||
|
;; built-in thrown? — the registered method must win over the by-name inline
|
||||||
|
;; path (clojure-test-suite's portability/thrown? registers exactly this shape).
|
||||||
|
(defmethod t/assert-expr 'p/thrown? [msg form]
|
||||||
|
`(try
|
||||||
|
(do ~@(rest form))
|
||||||
|
(clojure.test/do-report {:type :fail :message ~msg :form '~form})
|
||||||
|
(catch Throwable e#
|
||||||
|
(clojure.test/do-report {:type :pass})
|
||||||
|
e#)))
|
||||||
|
|
||||||
;; a custom report type (how test.check surfaces trial/shrink progress)
|
;; a custom report type (how test.check surfaces trial/shrink progress)
|
||||||
(def trials (atom 0))
|
(def trials (atom 0))
|
||||||
(defmethod t/report ::trial [_m] (swap! trials inc))
|
(defmethod t/report ::trial [_m] (swap! trials inc))
|
||||||
|
|
@ -25,23 +36,34 @@
|
||||||
(are [x y] (= x y)
|
(are [x y] (= x y)
|
||||||
2 (+ 1 1)
|
2 (+ 1 1)
|
||||||
6 (* 2 3))
|
6 (* 2 3))
|
||||||
|
;; template vars substitute inside quote (are is clojure.template, not let)
|
||||||
|
(are [x] (special-symbol? 'x)
|
||||||
|
if
|
||||||
|
def)
|
||||||
(is (thrown? clojure.lang.ExceptionInfo (throw (ex-info "x" {}))))
|
(is (thrown? clojure.lang.ExceptionInfo (throw (ex-info "x" {}))))
|
||||||
(is (thrown-with-msg? Exception #"bad" (throw (ex-info "bad" {}))))
|
(is (thrown-with-msg? Exception #"bad" (throw (ex-info "bad" {}))))
|
||||||
(is (near? 1.0 1.005)))
|
(is (near? 1.0 1.005))
|
||||||
|
(is (p/thrown? (throw (ex-info "boom" {})))))
|
||||||
|
|
||||||
(deftest expected-fail
|
(deftest expected-fail
|
||||||
(is (= 1 2))
|
(is (= 1 2))
|
||||||
(is (near? 1.0 5.0)))
|
(is (near? 1.0 5.0)))
|
||||||
|
|
||||||
(run-tests)
|
;; run-tests returns THIS call's summary; with explicit nses it runs only their
|
||||||
|
;; tests (an unknown ns runs nothing).
|
||||||
|
(def r1 (run-tests))
|
||||||
|
(def r2 (run-tests 'no.such.test-ns))
|
||||||
(t/do-report {:type ::trial})
|
(t/do-report {:type ::trial})
|
||||||
(t/do-report {:type ::trial})
|
(t/do-report {:type ::trial})
|
||||||
|
|
||||||
;; 7 pass (= + vector? + 2 are rows + thrown? + thrown-with-msg? + near?),
|
;; 10 pass (= + vector? + 4 are rows + thrown? + thrown-with-msg? + near? + p/thrown?),
|
||||||
;; 2 fail (= 1 2, near? 1.0 5.0), 0 error, 2 fixture runs, 2 custom reports
|
;; 2 fail (= 1 2, near? 1.0 5.0), 0 error, 2 fixture runs, 2 custom reports
|
||||||
(let [ok (and (= (t/n-pass) 7) (= (t/n-fail) 2) (= (t/n-error) 0)
|
(let [ok (and (= (t/n-pass) 10) (= (t/n-fail) 2) (= (t/n-error) 0)
|
||||||
|
(= 2 (:test r1)) (= 10 (:pass r1)) (= 2 (:fail r1))
|
||||||
|
(= 0 (:test r2)) (= 0 (:pass r2))
|
||||||
(= @setups 2) (= @trials 2))]
|
(= @setups 2) (= @trials 2))]
|
||||||
(println (if ok
|
(println (if ok
|
||||||
"CLOJURE-TEST OK"
|
"CLOJURE-TEST OK"
|
||||||
(str "CLOJURE-TEST FAIL pass=" (t/n-pass) " fail=" (t/n-fail)
|
(str "CLOJURE-TEST FAIL pass=" (t/n-pass) " fail=" (t/n-fail)
|
||||||
" error=" (t/n-error) " setups=" @setups " trials=" @trials))))
|
" error=" (t/n-error) " r1=" (pr-str r1) " r2=" (pr-str r2)
|
||||||
|
" setups=" @setups " trials=" @trials))))
|
||||||
|
|
|
||||||
7040
test/chez/corpus.edn
7040
test/chez/corpus.edn
File diff suppressed because it is too large
Load diff
2
test/chez/cts-app/deps.edn
Normal file
2
test/chez/cts-app/deps.edn
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
{:paths ["src" "../../../vendor/clojure-test-suite/test"]
|
||||||
|
:aliases {:cts {:main-opts ["-m" "cts-run"]}}}
|
||||||
18
test/chez/cts-app/src/cts_run.clj
Normal file
18
test/chez/cts-app/src/cts_run.clj
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
(ns cts-run
|
||||||
|
"Runner for the vendored jank-lang/clojure-test-suite (vendor/clojure-test-suite):
|
||||||
|
requires each test namespace given on the command line and runs its clojure.test
|
||||||
|
tests, printing one machine-readable result line per namespace. Driven per-process
|
||||||
|
by host/chez/cts.sh so a hang or crash in one namespace can't take out the run."
|
||||||
|
(:require [clojure.test :as t]))
|
||||||
|
|
||||||
|
(defn -main [& nses]
|
||||||
|
(doseq [n nses]
|
||||||
|
(let [ns-sym (symbol n)]
|
||||||
|
(try
|
||||||
|
(require ns-sym)
|
||||||
|
(let [r (t/run-tests ns-sym)]
|
||||||
|
(println "CTS-RESULT" n (:pass r 0) (:fail r 0) (:error r 0)))
|
||||||
|
(catch Throwable e
|
||||||
|
(println "CTS-RESULT" n 0 0 1
|
||||||
|
(str "LOAD: " (.getName (class e)) ": " (.getMessage e)))))))
|
||||||
|
(System/exit 0))
|
||||||
32
test/chez/cts-known-failures.txt
Normal file
32
test/chez/cts-known-failures.txt
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# clojure-test-suite known failures: <namespace> <fail> <error>
|
||||||
|
# The gate fails on any per-namespace change, worse OR better; regenerate
|
||||||
|
# with: JOLT_CTS_WRITE_BASELINE=1 host/chez/cts.sh
|
||||||
|
clojure.core-test.abs 1 0
|
||||||
|
clojure.core-test.add-watch 0 1
|
||||||
|
clojure.core-test.bigint 6 0
|
||||||
|
clojure.core-test.bit-set 1 0
|
||||||
|
clojure.core-test.dec 1 0
|
||||||
|
clojure.core-test.double-qmark 3 0
|
||||||
|
clojure.core-test.eq 2 0
|
||||||
|
clojure.core-test.float 1 0
|
||||||
|
clojure.core-test.inc 1 0
|
||||||
|
clojure.core-test.int-qmark 3 0
|
||||||
|
clojure.core-test.lazy-seq 1 2
|
||||||
|
clojure.core-test.minus 2 0
|
||||||
|
clojure.core-test.mod 18 0
|
||||||
|
clojure.core-test.neg-int-qmark 1 0
|
||||||
|
clojure.core-test.not-eq 3 0
|
||||||
|
clojure.core-test.num 2 0
|
||||||
|
clojure.core-test.parse-uuid 3 0
|
||||||
|
clojure.core-test.peek 1 0
|
||||||
|
clojure.core-test.plus 11 0
|
||||||
|
clojure.core-test.plus-squote 11 0
|
||||||
|
clojure.core-test.pos-int-qmark 1 0
|
||||||
|
clojure.core-test.quot 25 0
|
||||||
|
clojure.core-test.realized-qmark 1 0
|
||||||
|
clojure.core-test.rem 16 0
|
||||||
|
clojure.core-test.remove-watch 0 1
|
||||||
|
clojure.core-test.star 13 0
|
||||||
|
clojure.core-test.star-squote 13 0
|
||||||
|
clojure.core-test.transient 4 0
|
||||||
|
clojure.core-test.vec 1 0
|
||||||
1
test/chez/datareader-app/deps.edn
Normal file
1
test/chez/datareader-app/deps.edn
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{:paths ["src"]}
|
||||||
1
test/chez/datareader-app/src/data_readers.clj
Normal file
1
test/chez/datareader-app/src/data_readers.clj
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{code drtest.reader/code-reader}
|
||||||
4
test/chez/datareader-app/src/drtest/main.clj
Normal file
4
test/chez/datareader-app/src/drtest/main.clj
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
(ns drtest.main
|
||||||
|
(:require drtest.reader))
|
||||||
|
(defn -main [& _]
|
||||||
|
(println #code [:ignored]))
|
||||||
2
test/chez/datareader-app/src/drtest/reader.clj
Normal file
2
test/chez/datareader-app/src/drtest/reader.clj
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
(ns drtest.reader)
|
||||||
|
(defn code-reader [_form] (list '+ 40 2))
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
(ev "(def add1 (fn* ([x] (+ x 1))))")
|
(ev "(def add1 (fn* ([x] (+ x 1))))")
|
||||||
(let ((e (emitf "u" "(fn* ([y] (add1 y)))")))
|
(let ((e (emitf "u" "(fn* ([y] (add1 y)))")))
|
||||||
(ok "plain fn is inlined (call to add1 gone)" (not (has? e "add1")))
|
(ok "plain fn is inlined (call to add1 gone)" (not (has? e "add1")))
|
||||||
(ok "inlined body present (+ ... 1)" (has? e "(+")))
|
(ok "inlined body present (jolt-n+ ... 1)" (has? e "(jolt-n+")))
|
||||||
(ok "inlined plain fn runtime: (add1 41) = 42" (= 42 (jnum->exact (ev "((fn* ([y] (add1 y))) 41)"))))
|
(ok "inlined plain fn runtime: (add1 41) = 42" (= 42 (jnum->exact (ev "((fn* ([y] (add1 y))) 41)"))))
|
||||||
|
|
||||||
;; a ^double fn: body fl-ops fire after inlining, and the call is gone.
|
;; a ^double fn: body fl-ops fire after inlining, and the call is gone.
|
||||||
|
|
|
||||||
27
test/chez/repl-reader-test.clj
Normal file
27
test/chez/repl-reader-test.clj
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
;; Self-checking regression for the REPL's read-until-complete predicate
|
||||||
|
;; (jolt.main/repl-form-complete?), which decides whether a line buffer is a whole
|
||||||
|
;; form or the REPL should keep reading continuation lines. Runs via `bin/joltc run`
|
||||||
|
;; (jolt.main is loaded, so the private var resolves); prints a sentinel the smoke
|
||||||
|
;; gate greps. The regex cases are the ones that regressed: a #"..." literal opens a
|
||||||
|
;; regex whose body — parens, quotes and all — must not be miscounted as delimiters.
|
||||||
|
(require 'jolt.main)
|
||||||
|
|
||||||
|
(def complete? jolt.main/repl-form-complete?)
|
||||||
|
|
||||||
|
;; [input expected-complete?]
|
||||||
|
(def cases
|
||||||
|
[["(+ 1 2)" true] ; balanced
|
||||||
|
["(+ 1" false] ; open paren -> keep reading
|
||||||
|
["(defn g [] (foo (bar" false] ; deeply unbalanced
|
||||||
|
["[1 2 {:a 3}]" true] ; mixed bracket types balance
|
||||||
|
["(str \")\")" true] ; a close-paren inside a string doesn't count
|
||||||
|
["\\(" true] ; a paren char literal doesn't count
|
||||||
|
["(+ 1 2) ; ) ) trailing" true] ; parens in a line comment don't count
|
||||||
|
["(re-find #\"(a)(b)\" \"ab\")" true] ; groups inside a regex must not count as depth
|
||||||
|
["#\"[0-9]+\"" true] ; a bare regex literal is a complete form
|
||||||
|
["#\"a(b" false]]) ; an unterminated regex is incomplete
|
||||||
|
|
||||||
|
(let [bad (remove (fn [[in exp]] (= exp (boolean (complete? in)))) cases)]
|
||||||
|
(println (if (empty? bad)
|
||||||
|
"REPL-READER OK"
|
||||||
|
(str "REPL-READER FAIL " (pr-str (map first bad))))))
|
||||||
|
|
@ -226,6 +226,7 @@
|
||||||
{:suite "ioreader" :expr "(let [log (atom [])] (try (with-open [c {:close (fn [] (swap! log conj :closed))}] (throw (ex-info \"boom\" {}))) (catch Exception e nil)) (deref log))" :expected "[:closed]"}
|
{:suite "ioreader" :expr "(let [log (atom [])] (try (with-open [c {:close (fn [] (swap! log conj :closed))}] (throw (ex-info \"boom\" {}))) (catch Exception e nil)) (deref log))" :expected "[:closed]"}
|
||||||
{:suite "ioreader" :expr "(let [log (atom [])] (with-open [a {:close (fn [] (swap! log conj :outer))} b {:close (fn [] (swap! log conj :inner))}] :r) (deref log))" :expected "[:inner :outer]"}
|
{:suite "ioreader" :expr "(let [log (atom [])] (with-open [a {:close (fn [] (swap! log conj :outer))} b {:close (fn [] (swap! log conj :inner))}] :r) (deref log))" :expected "[:inner :outer]"}
|
||||||
{:suite "ioreader" :expr "(with-open [c {:close (fn [] nil) :v 5}] (:v c))" :expected "5"}
|
{:suite "ioreader" :expr "(with-open [c {:close (fn [] nil) :v 5}] (:v c))" :expected "5"}
|
||||||
|
{:suite "ioreader" :expr "(do (defrecord RC [log] java.io.Closeable (close [_] (swap! log conj :closed))) (let [log (atom [])] (with-open [r (->RC log)] :body) @log))" :expected "[:closed]"}
|
||||||
{:suite "javastatic" :expr "(Math/sqrt 16)" :expected "4.0"}
|
{:suite "javastatic" :expr "(Math/sqrt 16)" :expected "4.0"}
|
||||||
{:suite "javastatic" :expr "(Math/abs -3)" :expected "3"}
|
{:suite "javastatic" :expr "(Math/abs -3)" :expected "3"}
|
||||||
{:suite "javastatic" :expr "(Math/max 2 7)" :expected "7"}
|
{:suite "javastatic" :expr "(Math/max 2 7)" :expected "7"}
|
||||||
|
|
@ -311,6 +312,8 @@
|
||||||
{:suite "reader" :expr "(nil? (read-string \"nil\"))" :expected "true"}
|
{:suite "reader" :expr "(nil? (read-string \"nil\"))" :expected "true"}
|
||||||
{:suite "reader" :expr "(true? (read-string \"true\"))" :expected "true"}
|
{:suite "reader" :expr "(true? (read-string \"true\"))" :expected "true"}
|
||||||
{:suite "reader" :expr "(false? (read-string \"false\"))" :expected "true"}
|
{:suite "reader" :expr "(false? (read-string \"false\"))" :expected "true"}
|
||||||
|
{:suite "reader" :expr "(let [r (read-string \"#foo bar\")] (and (tagged-literal? r) (= (quote foo) (:tag r)) (= (quote bar) (:form r)) (= \"#foo bar\" (pr-str r))))" :expected "true"}
|
||||||
|
{:suite "reader" :expr "(let [t (first (quote [#foo bar]))] (and (tagged-literal? t) (= (quote foo) (:tag t)) (= \"#foo bar\" (pr-str t))))" :expected "true"}
|
||||||
{:suite "reader" :expr "(= \\a (read-string \"\\\\a\"))" :expected "true"}
|
{:suite "reader" :expr "(= \\a (read-string \"\\\\a\"))" :expected "true"}
|
||||||
{:suite "reader" :expr "(= \\newline (read-string \"\\\\newline\"))" :expected "true"}
|
{:suite "reader" :expr "(= \\newline (read-string \"\\\\newline\"))" :expected "true"}
|
||||||
{:suite "reader" :expr "(= \\space (read-string \"\\\\space\"))" :expected "true"}
|
{:suite "reader" :expr "(= \\space (read-string \"\\\\space\"))" :expected "true"}
|
||||||
|
|
@ -573,4 +576,26 @@
|
||||||
{: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"}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ read one data file, run each case, compare, report.
|
||||||
|
|
||||||
| File | Role | Generated by |
|
| File | Role | Generated by |
|
||||||
|------|------|--------------|
|
|------|------|--------------|
|
||||||
| `test/chez/corpus.edn` | **The spec.** ~2900 cases of `{:suite :label :expected :actual}`, `:expected` **sourced from reference JVM Clojure**. | `test/conformance/regen-corpus.clj` |
|
| `test/chez/corpus.edn` | **The spec.** ~3570 cases of `{:suite :label :expected :actual :portability}`, `:expected` **sourced from reference JVM Clojure**. | `test/conformance/regen-corpus.clj` |
|
||||||
| `test/conformance/profile.edn` | Per-case **feature classification** — which non-portable cases need which host capability. | `certify.clj --profile` |
|
| `test/conformance/profile.edn` | Per-case **feature classification** — which non-portable cases need which host capability. | `certify.clj --profile` |
|
||||||
| `test/conformance/known-divergences.edn` | The few rows whose JVM value is an opaque host object that can't round-trip to readable source (Java arrays/transients/atoms/beans/proxies print as `#object[..@addr]`), so the corpus keeps jolt's value. | `regen-corpus.clj` leftovers, hand-checked |
|
| `test/conformance/known-divergences.edn` | The few rows whose JVM value is an opaque host object that can't round-trip to readable source (Java arrays/transients/atoms/beans/proxies print as `#object[..@addr]`), so the corpus keeps jolt's value. | `regen-corpus.clj` leftovers, hand-checked |
|
||||||
| `test/conformance/regen-corpus.clj` | Sources every `:expected` from reference **JVM Clojure** in one process. | — |
|
| `test/conformance/regen-corpus.clj` | Sources every `:expected` from reference **JVM Clojure** in one process. | — |
|
||||||
|
|
@ -27,8 +27,14 @@ the canonical, frozen contract**: it is what every runtime consumes, what
|
||||||
{:suite "numbers / arithmetic" ; grouping; "<suite> :: <label>" is the case id
|
{:suite "numbers / arithmetic" ; grouping; "<suite> :: <label>" is the case id
|
||||||
:label "integer add" ; unique within a suite
|
:label "integer add" ; unique within a suite
|
||||||
:actual "(+ 1 2)" ; Clojure source to evaluate
|
:actual "(+ 1 2)" ; Clojure source to evaluate
|
||||||
:expected "3"} ; Clojure source whose value it must equal,
|
:expected "3" ; Clojure source whose value it must equal,
|
||||||
; or the keyword :throws
|
; or the keyword :throws
|
||||||
|
:portability :common} ; :common = portable Clojure any dialect
|
||||||
|
; must satisfy; :jvm = exercises host
|
||||||
|
; interop (java.*/clojure.lang.* classes,
|
||||||
|
; dot forms, ctors, statics, arrays,
|
||||||
|
; proxy/bean) — skip on a non-JVM-shaped
|
||||||
|
; dialect
|
||||||
```
|
```
|
||||||
|
|
||||||
- `[:suite :label]` is the **canonical, unique case id** (the generator
|
- `[:suite :label]` is the **canonical, unique case id** (the generator
|
||||||
|
|
@ -48,8 +54,8 @@ the canonical, frozen contract**: it is what every runtime consumes, what
|
||||||
Historically every `:expected` was hand-written. `certify.clj` removes that
|
Historically every `:expected` was hand-written. `certify.clj` removes that
|
||||||
weakness: it evaluates every `:actual` (and `:expected`) on **JVM Clojure** in a
|
weakness: it evaluates every `:actual` (and `:expected`) on **JVM Clojure** in a
|
||||||
fresh `user` namespace and checks jolt's `:expected` against what real Clojure
|
fresh `user` namespace and checks jolt's `:expected` against what real Clojure
|
||||||
produces. Of ~2740 vanilla-certifiable rows, **>2730 match reference Clojure
|
produces. Of ~3400 vanilla-certifiable rows, **all but ~26 allowlisted rows
|
||||||
exactly**. The rest are classified (see below) — none are silently wrong.
|
match reference Clojure exactly**. The rest are classified (see below) — none are silently wrong.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
clojure -M test/conformance/certify.clj # gate
|
clojure -M test/conformance/certify.clj # gate
|
||||||
|
|
@ -127,10 +133,49 @@ arithmetic, `=`, and `hash` behave exactly as the JVM — but report `Long`, not
|
||||||
`Byte`/`Short`/`Integer`, so `(class (byte 5))` and `(instance? Byte (byte 5))`
|
`Byte`/`Short`/`Integer`, so `(class (byte 5))` and `(instance? Byte (byte 5))`
|
||||||
diverge. This is substrate-inherent: a Chez fixnum is an immediate `identical?`
|
diverge. This is substrate-inherent: a Chez fixnum is an immediate `identical?`
|
||||||
to the plain integer (nothing to tag, and numbers carry no metadata), so the only
|
to the plain integer (nothing to tag, and numbers carry no metadata), so the only
|
||||||
faithful representation is a boxed type — which would crash raw compiled `(+ …)`
|
faithful representation is a boxed type — which would crash the compiled
|
||||||
(arithmetic emits a bare Chez `+`) or force every `+`/`-`/`*` through an
|
arithmetic fast path (both operands Chez numbers → the raw Chez op) or force
|
||||||
unwrapping dispatcher, de-optimizing all arithmetic. Same shape as the accepted
|
every `+`/`-`/`*` through an unwrapping dispatcher, de-optimizing all
|
||||||
BigInt-vs-Long unification.
|
arithmetic. Same shape as the accepted BigInt-vs-Long unification.
|
||||||
|
|
||||||
|
The cast RANGE contract is full parity (corpus `casts / *`): `byte`/`short`/
|
||||||
|
`int`/`long`/`char` range-check like `RT.byteCast` — an out-of-range value is
|
||||||
|
IllegalArgumentException "Value out of range for byte: 128". A double operand
|
||||||
|
range-checks ITSELF before truncating (`(byte 1.1)` is `1`, `(byte 127.000001)`
|
||||||
|
throws), NaN casts to 0, ratios and bigdecs truncate, a non-number is
|
||||||
|
ClassCastException. `float` range-checks against Float/MAX_VALUE. The
|
||||||
|
`unchecked-*` casts wrap and sign-fold like the JVM primitive conversions
|
||||||
|
(`(unchecked-byte 200)` is `-56`; a double saturates instead of wrapping).
|
||||||
|
What jolt does NOT model is a distinct single-float type: `(float x)` keeps
|
||||||
|
the double VALUE, so a double below Float/MIN_VALUE stays nonzero and float
|
||||||
|
rounding does not occur (the accepted no-single-float residue, baselined with
|
||||||
|
`:integer-box-model`'s class residue).
|
||||||
|
|
||||||
|
## Number operations
|
||||||
|
|
||||||
|
Binary arithmetic and comparisons follow the JVM's `Numbers.ops(x, y)` category
|
||||||
|
dispatch. Every position (call, value, higher-order) funnels a binary op through
|
||||||
|
one seam (`host/chez/seq.ss`): operands inside Chez's tower take the native op
|
||||||
|
with the JVM contagion rules patched in; an operand outside it (BigDecimal)
|
||||||
|
falls to a slow hook the numeric shim extends (`host/chez/java/bigdec.ss`); a
|
||||||
|
non-numeric operand throws `ClassCastException`. The rules the corpus pins
|
||||||
|
(`numbers / ops dispatch`, `numbers / with-precision`, `numbers / rationalize`):
|
||||||
|
|
||||||
|
- A double operand wins: `(* 1.0 0)` is `0.0` (Chez's exact-zero shortcut must
|
||||||
|
not leak), `(* ##Inf 0)` is `##NaN`, `(+ 1.5M 2.0)` is `3.5`.
|
||||||
|
- Division: an exact zero divisor throws `ArithmeticException`; a double zero
|
||||||
|
divisor yields `##Inf`/`##-Inf`/`##NaN`. `(/ 1M 3M)` with no bound
|
||||||
|
`*math-context*` throws (non-terminating); under `with-precision` it rounds.
|
||||||
|
- `quot`/`rem`/`mod` cover the full tower (ratios truncate; doubles keep double;
|
||||||
|
`mod` takes the divisor's sign; zero divisor throws in both worlds).
|
||||||
|
- `min`/`max` return the *original* operand (`(min 1 2.0)` is `1`, exact); a
|
||||||
|
`##NaN` operand wins.
|
||||||
|
- `with-precision` binds `*math-context*`; BigDecimal results round to the
|
||||||
|
precision with the `java.math.RoundingMode` semantics (default `HALF_UP`,
|
||||||
|
`UNNECESSARY` throws).
|
||||||
|
- `rationalize` routes a double through its shortest decimal print
|
||||||
|
(`BigDecimal.valueOf`), so `(rationalize 1.1)` is `11/10`, not the exact
|
||||||
|
binary expansion.
|
||||||
|
|
||||||
## Hosting jolt on a new runtime
|
## Hosting jolt on a new runtime
|
||||||
|
|
||||||
|
|
@ -166,3 +211,39 @@ corpus`.
|
||||||
deliberate delta (classify it in `known-divergences.edn`).
|
deliberate delta (classify it in `known-divergences.edn`).
|
||||||
- **Refresh the profile**: re-run with `--profile test/conformance/profile.edn`.
|
- **Refresh the profile**: re-run with `--profile test/conformance/profile.edn`.
|
||||||
- **Re-floor the runtime gate** when parity rises (`host/chez/run-corpus.ss`).
|
- **Re-floor the runtime gate** when parity rises (`host/chez/run-corpus.ss`).
|
||||||
|
|
||||||
|
## clojure-test-suite baseline traceability
|
||||||
|
|
||||||
|
Every residual entry in `test/chez/cts-known-failures.txt` traces to one
|
||||||
|
documented model divergence — nothing in the baseline is an unexplained bug:
|
||||||
|
|
||||||
|
- `:integer-box-model` (this file, above): every `big-int?`/`instance?
|
||||||
|
Byte…BigInt` class check, the overflow-throw rows (`(+ max-long 1)` is a
|
||||||
|
bignum, not ArithmeticException — abs/inc/dec/minus/plus/star/quot/rem/mod/
|
||||||
|
bit-set and the `+'`/`*'` promotion-identity namespaces), boxed-identity
|
||||||
|
rows (`(identical? (Boolean. "true") true)`, `(= x x)` on a boxed NaN — jolt
|
||||||
|
numbers are immediates, there is no box to distinguish), and `num`'s
|
||||||
|
primitive-overload reflection rows.
|
||||||
|
- **no single float** (Narrow integer types, above): `(float Double/MIN_VALUE)`
|
||||||
|
keeps the double value instead of rounding to 0.0f; `(double? (float x))`
|
||||||
|
is true.
|
||||||
|
- **RFC 0003 transients**: `(transient sorted/list/lazy-seq)` succeeds through
|
||||||
|
the copy-on-write fallback (a deliberate superset; non-collections now throw
|
||||||
|
like the JVM), and double-transient is idempotent rather than throwing.
|
||||||
|
- `:seq-type-model`/`:chunking-model` (Seq semantics, above): `realized?` on
|
||||||
|
the rest of a realized chain (a plain seq cell on jolt, a cached LazySeq on
|
||||||
|
the JVM), `p/lazy-seq?` on forced rest chains, and chunk-granularity
|
||||||
|
realization counts (lazy-seq namespace).
|
||||||
|
- **stm-refs** (`coverage.md`): the `(ref …)`/`dosync` sections of the watch
|
||||||
|
namespaces (add-watch/remove-watch) — refs are out of scope pending the
|
||||||
|
concurrency design note.
|
||||||
|
- **parse-uuid strictness** (spec §9, parse-uuid S3): jolt is deliberately
|
||||||
|
strict where the reference's java.util.UUID accepts non-canonical forms
|
||||||
|
like `"0-0-0-0-0"`.
|
||||||
|
- **vec of an array copies**: the reference ADOPTS an Object array (mutating
|
||||||
|
the array mutates the vector); jolt copies — immutable semantics win over
|
||||||
|
the implementation leak (`vec` namespace, one row).
|
||||||
|
|
||||||
|
|
||||||
|
A future change that makes any of these rows pass will fail the cts gate as
|
||||||
|
STALE, forcing this section and the baseline to be updated together.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"Rows of test/chez/corpus.edn whose :expected differs from reference JVM Clojure. The corpus is JVM-sourced (regen-corpus.clj), so this list is only the rows whose JVM value is an opaque host object that cannot round-trip to readable source — Java arrays, transients, atoms, beans, proxies, and chunks print as #object[..@addr] with a per-run identity — plus the (fn* foo) strictness case. For that the corpus keeps jolt's value. certify.clj gates on NEW (unlisted) divergences and STALE entries. Keyed by [suite label].",
|
"Rows of test/chez/corpus.edn whose :expected differs from reference JVM Clojure. The corpus is JVM-sourced (regen-corpus.clj), so this list is only the rows whose JVM value is an opaque host object that cannot round-trip to readable source — Java arrays, transients, atoms, beans, proxies, and chunks print as #object[..@addr] with a per-run identity — plus the (fn* foo) strictness case. For that the corpus keeps jolt's value. certify.clj gates on NEW (unlisted) divergences and STALE entries. Keyed by [suite label].",
|
||||||
:legend
|
:legend
|
||||||
{:numeric-model
|
{:numeric-model
|
||||||
"jolt has a numeric tower (exact integer / Ratio / double); no BigDecimal",
|
"jolt has the full numeric tower (exact integer / Ratio / double / BigDecimal); binary ops dispatch by operand category with JVM contagion rules",
|
||||||
:host-model
|
:host-model
|
||||||
"no JVM host: classes->name strings, type->symbol, *in* is a map, inline-impl extenders, duck-typed with-open close",
|
"no JVM host: classes->name strings, type->symbol, *in* is a map, inline-impl extenders, duck-typed with-open close",
|
||||||
:reader-model
|
:reader-model
|
||||||
|
|
|
||||||
|
|
@ -99,11 +99,12 @@
|
||||||
[row :kept])))
|
[row :kept])))
|
||||||
|
|
||||||
;; --- corpus writer (preserves the one-row-per-line layout) -------------------
|
;; --- corpus writer (preserves the one-row-per-line layout) -------------------
|
||||||
(defn row-str [{:keys [suite label expected actual]}]
|
(defn row-str [{:keys [suite label expected actual portability]}]
|
||||||
(str " {:suite " (pr-str suite)
|
(str " {:suite " (pr-str suite)
|
||||||
" :label " (pr-str label)
|
" :label " (pr-str label)
|
||||||
" :expected " (if (= expected :throws) ":throws" (pr-str expected))
|
" :expected " (if (= expected :throws) ":throws" (pr-str expected))
|
||||||
" :actual " (pr-str actual) "}"))
|
" :actual " (pr-str actual)
|
||||||
|
(when portability (str " :portability " portability)) "}"))
|
||||||
|
|
||||||
(defn -main [& _]
|
(defn -main [& _]
|
||||||
(let [corpus (edn/read-string (slurp corpus-path))
|
(let [corpus (edn/read-string (slurp corpus-path))
|
||||||
|
|
|
||||||
1
vendor/clojure-test-suite
vendored
Submodule
1
vendor/clojure-test-suite
vendored
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 489b6743e8421687ef96cec557830acf258d1886
|
||||||
Loading…
Add table
Add a link
Reference in a new issue