Compare commits
45 commits
numeric/op
...
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 |
61 changed files with 6817 additions and 4837 deletions
115
.github/workflows/release.yml
vendored
115
.github/workflows/release.yml
vendored
|
|
@ -12,6 +12,7 @@ on:
|
|||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch: {} # dry-run the build matrix without tagging
|
||||
|
||||
permissions:
|
||||
contents: write # create/update the GitHub Release and upload assets
|
||||
|
|
@ -26,10 +27,18 @@ jobs:
|
|||
include:
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-linux
|
||||
shell: bash
|
||||
# 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
|
||||
target: aarch64-macos
|
||||
shell: bash
|
||||
- os: windows-latest
|
||||
target: x86_64-windows
|
||||
shell: msys2 {0}
|
||||
defaults:
|
||||
run:
|
||||
shell: ${{ matrix.shell }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
|
|
@ -78,6 +87,65 @@ jobs:
|
|||
if: runner.os == 'macOS'
|
||||
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
|
||||
run: chez --version
|
||||
|
||||
|
|
@ -86,6 +154,19 @@ jobs:
|
|||
# dev-machine check — see jolt-8479). `make joltc-release`, not `make joltc`.
|
||||
- name: Build 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.
|
||||
- name: Smoke the binary
|
||||
|
|
@ -109,21 +190,47 @@ jobs:
|
|||
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
|
||||
run: |
|
||||
ver="${GITHUB_REF_NAME}"
|
||||
name="joltc-${ver}-${{ matrix.target }}"
|
||||
mkdir -p "dist/${name}"
|
||||
cp target/release/joltc "dist/${name}/joltc"
|
||||
cp README.md LICENSE "dist/${name}/"
|
||||
tar -C dist -czf "dist/${name}.tar.gz" "${name}"
|
||||
( cd dist && shasum -a 256 "${name}.tar.gz" > "${name}.tar.gz.sha256" )
|
||||
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}"
|
||||
( cd dist && shasum -a 256 "${name}.tar.gz" > "${name}.tar.gz.sha256" )
|
||||
fi
|
||||
ls -la dist
|
||||
|
||||
- name: Upload to the GitHub Release
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
dist/*.tar.gz
|
||||
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)
|
||||
run: |
|
||||
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
|
||||
clojure --version
|
||||
|
||||
|
|
|
|||
11
Makefile
11
Makefile
|
|
@ -4,18 +4,23 @@
|
|||
# build step. `make test` is the full gate. `make remint` rebuilds the seed after a
|
||||
# source change.
|
||||
|
||||
.PHONY: test ci values corpus unit smoke buildsmoke staticnativesmoke selfhost sci cts certify ffi transient infer wp devirt fieldread numwp fieldnum protoret narrow directlink numeric inline shakesmoke remint joltc joltc-release joltc-debug joltcsmoke
|
||||
.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
|
||||
# on the same Chez that minted the seed.
|
||||
test: selfhost ci
|
||||
test: submodules selfhost ci
|
||||
@echo "OK: all gates passed"
|
||||
|
||||
# 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
|
||||
# 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).
|
||||
ci: values corpus unit smoke buildsmoke staticnativesmoke sci cts 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"
|
||||
|
||||
# Self-host fixpoint: bootstrap.ss rebuild == checked-in seed.
|
||||
|
|
|
|||
20
README.md
20
README.md
|
|
@ -9,8 +9,12 @@ runs it on Chez. The compiler is self-hosted: it is written in Clojure
|
|||
|
||||
## Install
|
||||
|
||||
Grab the self-contained `joltc` binary (Linux/macOS) — it bundles the runtime,
|
||||
compiler, and standard library, so there is nothing else to 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:
|
||||
|
||||
|
|
@ -45,6 +49,18 @@ cd jolt
|
|||
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
|
||||
analyzer/IR/backend (`jolt-core/jolt/*.clj`), or the `clojure.core` overlay
|
||||
(`jolt-core/clojure/core/*.clj`) — re-mint the seed:
|
||||
|
|
|
|||
25
bin/joltc
25
bin/joltc
|
|
@ -14,5 +14,28 @@
|
|||
# JOLT_PWD.
|
||||
root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
export JOLT_PWD="${JOLT_PWD:-$PWD}"
|
||||
|
||||
# Identify the Chez Scheme executable
|
||||
while read -r CHEZ
|
||||
do
|
||||
if [ `which ${CHEZ}` ]
|
||||
then
|
||||
break;
|
||||
fi
|
||||
done <<EOF
|
||||
chez
|
||||
chezscheme
|
||||
EOF
|
||||
|
||||
# If we failed to find one, whinge and exit.
|
||||
if [ ! `which ${CHEZ}` ]
|
||||
then
|
||||
echo "No valid Chez Scheme executable found: please install Chez Scheme."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Version for --version / banners: git describe of this checkout, else "dev".
|
||||
export JOLT_VERSION="${JOLT_VERSION:-$(git -C "$root" describe --tags --always --dirty 2>/dev/null || echo dev)}"
|
||||
cd "$root" || exit 1
|
||||
exec chez --script host/chez/cli.ss "$@"
|
||||
exec ${CHEZ} --script host/chez/cli.ss "$@"
|
||||
|
||||
|
|
|
|||
|
|
@ -258,6 +258,14 @@ transitivity is computed):
|
|||
;; (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,
|
||||
say) means editing the relevant `host/chez/*.ss` file and running `make remint`
|
||||
— see [building-and-deps.md](building-and-deps.md).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
`v` followed by evaluation yields a value equal to `v` for the
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -232,6 +232,128 @@ clojure-test-suite `core_test/special_symbol_qmark.cljc` and every
|
|||
|
||||
---
|
||||
|
||||
### 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
|
||||
|
||||
- Source examples from the ClojureDocs export (`clojuredocs-export.edn`,
|
||||
|
|
|
|||
|
|
@ -182,6 +182,30 @@ a root, transitively.
|
|||
- 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).
|
||||
|
||||
## 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
|
||||
|
||||
The known-working libraries (see [libraries.md](libraries.md)) and the
|
||||
|
|
|
|||
|
|
@ -23,21 +23,38 @@
|
|||
(fields (mutable val) (mutable watches) (mutable validator) lock)
|
||||
(nongenerative jolt-atom-v3))
|
||||
|
||||
;; (atom init) / (atom init :validator f :meta m): scan the trailing keyword opts
|
||||
;; for :validator (the only one with runtime behaviour; :meta is accepted/ignored).
|
||||
;; a rejected reference value is IllegalStateException, like ARef.validate.
|
||||
(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)
|
||||
(let loop ((o opts) (validator jolt-nil))
|
||||
(let loop ((o opts) (validator jolt-nil) (m #f))
|
||||
(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"))
|
||||
(loop (cddr o) (cadr o)))
|
||||
(else (loop (cddr o) validator)))))
|
||||
(loop (cddr o) (cadr o) m))
|
||||
((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.
|
||||
(define (jolt-atom-validate a v)
|
||||
(let ((vf (jolt-atom-validator a)))
|
||||
(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,
|
||||
;; so walk it reversed to match add order).
|
||||
|
|
@ -106,27 +123,87 @@
|
|||
(jolt-atom-notify a 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);
|
||||
;; 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
|
||||
;; 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)
|
||||
(jolt-atom-watches-set! a
|
||||
(cons (cons key f)
|
||||
(remp (lambda (kv) (jolt=2 (car kv) key)) (jolt-atom-watches a))))
|
||||
a)
|
||||
(cond
|
||||
((jolt-atom? a)
|
||||
(jolt-atom-watches-set! a (jolt-watch-add (jolt-atom-watches a) key f))
|
||||
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)
|
||||
(jolt-atom-watches-set! a
|
||||
(remp (lambda (kv) (jolt=2 (car kv) key)) (jolt-atom-watches a)))
|
||||
a)
|
||||
(cond
|
||||
((jolt-atom? a)
|
||||
(jolt-atom-watches-set! a
|
||||
(remp (lambda (kv) (jolt=2 (car kv) key)) (jolt-atom-watches 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)
|
||||
(let ((vf (if (jolt-nil? f) jolt-nil f)))
|
||||
(when (and (not (jolt-nil? vf)) (jolt-not (jolt-invoke vf (jolt-atom-val a))))
|
||||
(error #f "Invalid reference state"))
|
||||
(jolt-atom-validator-set! a vf)
|
||||
(cond
|
||||
((jolt-atom? a)
|
||||
(when (and (not (jolt-nil? vf)) (jolt-not (jolt-invoke vf (jolt-atom-val a))))
|
||||
(jolt-iref-state-throw))
|
||||
(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))
|
||||
(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" "deref" jolt-deref)
|
||||
|
|
|
|||
|
|
@ -46,6 +46,15 @@
|
|||
(unless (or jb-release? (string=? jb-profile "debug"))
|
||||
(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"))
|
||||
(bld-check-toolchain)
|
||||
(bld-system (string-append "mkdir -p '" (path-parent jb-out) "' '" jb-build "'"))
|
||||
|
|
@ -136,11 +145,10 @@
|
|||
(scheme-start
|
||||
(lambda args
|
||||
(set-source-roots! (list \"jolt-core\" \"stdlib\"))
|
||||
(guard (v (#t (jolt-report-throwable v (current-error-port))
|
||||
(let ((bt (jolt-backtrace-string v)))
|
||||
(when bt (display \" trace:\\n\" (current-error-port))
|
||||
(display bt (current-error-port))))
|
||||
(exit 1)))
|
||||
;; JOLT_TRACE at RUNTIME (the env is unset at heap-build), before any app ns
|
||||
;; compiles, so a `-M:run` traces the app's own code.
|
||||
(jolt-trace-init-from-env!)
|
||||
(guard (v (#t (jolt-report-throwable v (current-error-port)) (exit 1)))
|
||||
(cond
|
||||
((and (= (length args) 2) (string=? (car args) \"-e\"))
|
||||
(let ((result (jolt-final-str
|
||||
|
|
@ -164,6 +172,10 @@
|
|||
(jb-emit-runtime-embeds out)
|
||||
(put-string out "\n;; === embedded jolt-core + stdlib source ===\n")
|
||||
(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")
|
||||
(jb-emit-launcher out)
|
||||
(close-port out))
|
||||
|
|
@ -245,6 +257,8 @@
|
|||
;; 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
|
||||
"cc -O2 -rdynamic -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)))
|
||||
(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
|
||||
fi
|
||||
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,
|
||||
# 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
|
||||
|
|
@ -129,4 +151,20 @@ if [ "$got_dr" != "42" ]; then
|
|||
echo " FAIL: built #code data reader — want 42, got \`$got_dr\`"; exit 1
|
||||
fi
|
||||
|
||||
echo "build smoke: passed (release + optimized + direct-link + tree-shake + compiler+core shake + data-reader)"
|
||||
# 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 ----------------------------------------------------------
|
||||
;; Run a command, return its stdout as one trimmed string ("" on no output).
|
||||
(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 ((l (get-line in)))
|
||||
(if (eof-object? l)
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
(loop (cons l acc)))))))
|
||||
|
||||
(define (bld-system cmd)
|
||||
(let ((rc (system cmd)))
|
||||
(let ((rc (system (bld-sh-wrap cmd))))
|
||||
(unless (zero? rc)
|
||||
(error 'jolt-build (string-append "command failed (" (number->string rc) "): " cmd)))))
|
||||
|
||||
|
|
@ -57,6 +57,24 @@
|
|||
;; --- toolchain discovery ----------------------------------------------------
|
||||
(define bld-machine (symbol->string (machine-type)))
|
||||
(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).
|
||||
(define bld-chez
|
||||
|
|
@ -94,14 +112,21 @@
|
|||
|
||||
;; Link flags. macOS Homebrew layout for the kernel's lz4/zlib/ncurses deps.
|
||||
(define (bld-link-libs)
|
||||
(if bld-osx?
|
||||
(let ((lz4 (bld-sh-capture "brew --prefix lz4 2>/dev/null")))
|
||||
(string-append
|
||||
(if (> (string-length lz4) 0) (string-append "-L" lz4 "/lib ") "")
|
||||
"-llz4 -lz -lncurses -framework Foundation -liconv -lm"))
|
||||
;; Linux: the Chez kernel pulls in compression (lz4/z), the expression
|
||||
;; editor (ncurses + terminfo), threads, dlopen, libuuid, and clock_gettime.
|
||||
"-llz4 -lz -lncurses -ltinfo -ldl -lm -lpthread -luuid -lrt"))
|
||||
(cond
|
||||
(bld-osx?
|
||||
(let ((lz4 (bld-sh-capture "brew --prefix lz4 2>/dev/null")))
|
||||
(string-append
|
||||
(if (> (string-length lz4) 0) (string-append "-L" lz4 "/lib ") "")
|
||||
"-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
|
||||
;; editor (ncurses + terminfo), threads, dlopen, libuuid, and clock_gettime.
|
||||
(else "-llz4 -lz -lncurses -ltinfo -ldl -lm -lpthread -luuid -lrt")))
|
||||
|
||||
;; --- 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
|
||||
|
|
@ -411,7 +436,15 @@
|
|||
;; direct-link?: opt-in closed-world direct-linking (app->app calls bind directly,
|
||||
;; no runtime redefinition). Off by default in every mode — release stays
|
||||
;; 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?)
|
||||
;; 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
|
||||
;; kernel files, no Chez, no cc — only the legacy cc path does.
|
||||
(unless (jolt-embedded-bytes "stub/launcher") (bld-check-toolchain))
|
||||
|
|
@ -538,11 +571,23 @@
|
|||
"))\n"
|
||||
" (list \"jolt-core\" \"stdlib\"))))\n"))
|
||||
(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
|
||||
;; of Chez's opaque dump, then exit non-zero.
|
||||
" (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"))
|
||||
(close-port out))
|
||||
;; 4. compile -> boot -> link. Two paths, chosen by whether this process
|
||||
|
|
@ -558,24 +603,70 @@
|
|||
(build-self-contained entry-ns out-path mode builddir flat-ss flat-so boot
|
||||
(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))))))))
|
||||
(bld-native-link-flags natives)))))))))
|
||||
|
||||
;; --- self-contained link (in-process compile + append the boot to the stub) ---
|
||||
;; compile-file runs with a FRESH scheme-environment as the interaction
|
||||
;; environment: the loaded jolt runtime redefines `error` (regex.ss), and flat.ss
|
||||
;; inlines that same runtime, so an early reference to `error` (before its define
|
||||
;; runs) must bind to the kernel primitive — a clean env gives exactly that, the
|
||||
;; same as the legacy path compiling in a fresh Chez process. Without it the boot
|
||||
;; dies at startup with "variable error is not bound".
|
||||
;; compile-file runs against the DEFAULT interaction environment, so the boot's
|
||||
;; top-level defines land in the real symbol cells — the runtime compiler's
|
||||
;; eval'd code must resolve them (var-deref, jolt-invoke, the jolt-n* macros)
|
||||
;; when the built binary dynamically requires a namespace. Compiling in a clean
|
||||
;; copy-environment instead orphans every define in locations eval can't see,
|
||||
;; and the binary dies with "variable var-deref is not bound" the moment a
|
||||
;; 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"))
|
||||
(scheme (string-append builddir "/scheme.boot")))
|
||||
(jolt-spill-embedded! "csv/petite.boot" petite)
|
||||
(jolt-spill-embedded! "csv/scheme.boot" scheme)
|
||||
(display (string-append "jolt build: compiling " entry-ns " (" mode " mode, self-contained)\n"))
|
||||
(parameterize ((interaction-environment (copy-environment (scheme-environment))))
|
||||
(compile-file flat-ss flat-so)
|
||||
(make-boot-file boot '() petite scheme flat-so))
|
||||
(bld-prepend-prologue! flat-ss)
|
||||
(compile-file flat-ss 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
|
||||
|
|
|
|||
|
|
@ -11,6 +11,26 @@
|
|||
|
||||
(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")
|
||||
(set-chez-ns! "clojure.core")
|
||||
(load "host/chez/seed/prelude.ss")
|
||||
|
|
@ -46,6 +66,9 @@
|
|||
(when bt (display " trace:\n" port) (display bt port)))
|
||||
(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)))
|
||||
(cond
|
||||
;; -e EXPR — evaluate one expression and print it (blank for nil). Wrapped in
|
||||
|
|
|
|||
|
|
@ -298,8 +298,21 @@
|
|||
(define empty-pmap-hash (make-pmap empty-hnode 0 #f)) ; hash-order backing (sets)
|
||||
(define pmap-absent (list 'absent)) ; unique missing-key sentinel
|
||||
;; 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-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 (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))
|
||||
(cnt (pmap-cnt m)) (ord (pmap-order m)))
|
||||
(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) #f))
|
||||
(make-pmap r cnt ord))))
|
||||
|
|
@ -352,10 +365,14 @@
|
|||
(let loop ((ks ord) (a acc))
|
||||
(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))))
|
||||
;; 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)
|
||||
(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"))
|
||||
(else (loop (pmap-put-ordered m (car kvs) (cadr kvs)) (cddr kvs))))))
|
||||
;; array-map ctor: insertion-ordered regardless of size (createAsIfByAssoc).
|
||||
|
|
@ -404,9 +421,11 @@
|
|||
(if (null? args)
|
||||
(jolt-vector)
|
||||
(let ((coll (car args)) (xs (cdr args)))
|
||||
(if (jolt-nil? coll)
|
||||
(fold-left jolt-conj1 jolt-empty-list xs)
|
||||
(meta-carry coll (fold-left jolt-conj1 coll xs))))))
|
||||
(cond
|
||||
;; 1-arity returns the coll untouched — (conj nil) is nil
|
||||
((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) ->
|
||||
;; value) instead of set!-wrapping jolt-get — disjoint coll types, checked before the
|
||||
|
|
@ -443,11 +462,16 @@
|
|||
(define (rec-coll-method 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
|
||||
(case-lambda
|
||||
((coll i)
|
||||
(jolt-nth-nil-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)
|
||||
(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)
|
||||
|
|
@ -456,8 +480,10 @@
|
|||
((rec-coll-method coll "nth") => (lambda (m) (jolt-invoke m coll i)))
|
||||
(else (error 'nth "unsupported collection")))))
|
||||
((coll i d)
|
||||
(jolt-nth-nil-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))
|
||||
((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)))
|
||||
|
|
@ -502,6 +528,21 @@
|
|||
((pset? coll) (pset-contains? coll k))
|
||||
((pvec? coll) (let ((k (->idx k))) (and (fixnum? k) (fx>=? k 0) (fx<? k (pvec-count coll)))))
|
||||
((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)))
|
||||
|
||||
(define (jolt-empty? coll)
|
||||
|
|
@ -514,15 +555,25 @@
|
|||
((cseq? coll) #f) ; a cseq is non-empty by construction
|
||||
(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)
|
||||
(cond ((pvec? coll) (pvec-peek coll))
|
||||
((or (cseq? coll) (empty-list-t? coll)) (jolt-first coll)) ; list peek = first
|
||||
((jolt-nil? coll) jolt-nil) (else (error 'peek "unsupported collection"))))
|
||||
;; list peek = first; a non-list seq (range, a rest chain) is not an
|
||||
;; 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)
|
||||
(cond ((pvec? coll) (meta-carry coll (pvec-pop coll)))
|
||||
((cseq? coll) (meta-carry coll (jolt-rest coll))) ; list pop = rest
|
||||
(cond ((jolt-nil? coll) jolt-nil) ; RT.pop(nil) is nil
|
||||
((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"))
|
||||
(else (error 'pop "unsupported collection"))))
|
||||
(else (jolt-stack-throw coll))))
|
||||
|
||||
;; ============================================================================
|
||||
;; equality / hash hooks called from values.ss (jolt=2 / jolt-hash)
|
||||
|
|
|
|||
|
|
@ -110,6 +110,38 @@
|
|||
;; older seed during the first re-mint pass.
|
||||
(let ((scv (var-deref "jolt.backend-scheme" "set-var-cache!")))
|
||||
(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.
|
||||
|
|
@ -211,7 +243,13 @@
|
|||
;; 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
|
||||
;; 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)
|
||||
(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
|
||||
;; 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
|
||||
|
|
@ -229,6 +267,9 @@
|
|||
;; record this form's source location first, so a compile- or run-time error
|
||||
;; in it reports the right place.
|
||||
(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)))
|
||||
(interaction-environment)))))
|
||||
|
||||
|
|
|
|||
|
|
@ -117,20 +117,21 @@
|
|||
(let ((a (car args)))
|
||||
(cond
|
||||
((jolt-symbol? a) a)
|
||||
;; (symbol "ns/name") splits the namespace at the LAST "/" (JVM
|
||||
;; Symbol.intern), so (namespace (symbol "foo/bar")) => "foo". A lone "/"
|
||||
;; or a leading slash has no namespace. The no-ns sentinel is #f — matches
|
||||
;; emit's quoted-symbol lowering (jolt-symbol #f "x"), so (= 'x (symbol
|
||||
;; "x")) holds (jolt= compares ns with strict equal?).
|
||||
;; (symbol "ns/name") splits the namespace at the FIRST "/" (JVM
|
||||
;; Symbol.intern), so (namespace (symbol "foo/bar/baz")) => "foo" with
|
||||
;; name "bar/baz". A lone "/" or a leading slash has no namespace. The
|
||||
;; no-ns sentinel is #f — matches emit's quoted-symbol lowering
|
||||
;; (jolt-symbol #f "x"), so (= 'x (symbol "x")) holds (jolt= compares
|
||||
;; ns with strict equal?).
|
||||
((string? a)
|
||||
(let ((slen (string-length a)))
|
||||
(if (string=? a "/")
|
||||
(jolt-symbol #f "/")
|
||||
(let loop ((i (- slen 1)))
|
||||
(cond ((<= i 0) (jolt-symbol #f a))
|
||||
(let loop ((i 1))
|
||||
(cond ((>= i slen) (jolt-symbol #f a))
|
||||
((char=? (string-ref a i) #\/)
|
||||
(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)))
|
||||
;; (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)))
|
||||
|
|
@ -155,7 +156,12 @@
|
|||
;; int/long: truncate toward zero to an EXACT integer (= JVM long). char -> code
|
||||
;; point (exact). double: always a flonum (= JVM double).
|
||||
(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).
|
||||
(define (jolt-cmp3 x y) (cond ((< x y) -1) ((> x y) 1) (else 0)))
|
||||
|
|
@ -172,7 +178,11 @@
|
|||
((jolt-nil? b) 1)
|
||||
((and (number? a) (number? b)) (jolt-cmp3 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))
|
||||
(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)))
|
||||
|
|
@ -195,16 +205,84 @@
|
|||
(def-var! "clojure.core" "keyword" jolt-keyword)
|
||||
(def-var! "clojure.core" "symbol" jolt-symbol-new)
|
||||
(def-var! "clojure.core" "gensym" jolt-gensym)
|
||||
(def-var! "clojure.core" "int" jolt-int)
|
||||
;; char: coerce a code point (jolt's all-flonum number) to a Chez char; pass a
|
||||
;; char through. Inverse of int on chars. The cross-compiled emitter's
|
||||
;; chez-str-lit needs it for printable-ASCII escaping.
|
||||
(define (jolt-char x) (if (char? x) x (integer->char (exact (round x)))))
|
||||
;; --- checked narrow casts (RT.byteCast/shortCast/intCast/longCast/charCast) --
|
||||
;; One helper carries the JVM ranges: truncate toward zero, then range-check.
|
||||
;; NaN casts to 0 (Java (long)NaN); an out-of-range value (including a float
|
||||
;; infinity) is IllegalArgumentException "Value out of range for <type>: 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)
|
||||
;; long: same truncation as int in jolt's all-flonum model (seed core-long =
|
||||
;; math/trunc; char -> code point). Distinct cell so (long ...) resolves.
|
||||
(def-var! "clojure.core" "long" jolt-int)
|
||||
;; unchecked-long: truncate + wrap to 64 bits (RT.uncheckedLongCast — a float
|
||||
;; infinity saturates, NaN is 0). unchecked-int wraps and sign-folds to 32.
|
||||
(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)
|
||||
;; float: Chez has no single-float type, so float coerces to a flonum like double.
|
||||
(def-var! "clojure.core" "float" jolt-double)
|
||||
;; float: Chez has no single-float type, so the value stays a flonum — but the
|
||||
;; 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)
|
||||
|
|
|
|||
|
|
@ -77,14 +77,23 @@
|
|||
(let ((p (dyn-find-binding v)))
|
||||
(if p
|
||||
(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)))
|
||||
|
||||
;; alter-var-root: atomically apply f to the current root plus 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-defined?-set! v #t)
|
||||
(iref-notify v old new)
|
||||
new))
|
||||
|
||||
;; __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.
|
||||
(let ((scv (var-deref "jolt.backend-scheme" "set-var-cache!")))
|
||||
(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?)
|
||||
(let ((ir (jolt-ce-analyze ctx f)))
|
||||
(jolt-ce-emit-top (if optimize? (jolt-ce-run-passes ir ctx) ir))))
|
||||
|
|
|
|||
|
|
@ -334,6 +334,11 @@
|
|||
(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?)
|
||||
|
|
@ -355,6 +360,19 @@
|
|||
(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
|
||||
;; bigdec vs a plain number compares by value; bigdec vs bigdec is scale-independent.
|
||||
(define jbd-prev-compare jolt-compare)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,15 @@
|
|||
|
||||
(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)
|
||||
|
|
@ -46,6 +55,11 @@
|
|||
(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)
|
||||
|
|
@ -130,6 +144,30 @@
|
|||
(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"))
|
||||
|
|
|
|||
|
|
@ -151,16 +151,31 @@
|
|||
(mutable queue) (mutable running?) mu cv)
|
||||
(nongenerative jolt-agent-v1))
|
||||
|
||||
;; (agent state) / (agent state :validator f :error-mode m :meta x): only :validator
|
||||
;; has runtime behaviour here; other opts are accepted/ignored.
|
||||
;; (agent state :meta m :validator f :error-mode e): the ARef ctor contract like
|
||||
;; 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)
|
||||
(let loop ((o opts) (validator jolt-nil))
|
||||
(let loop ((o opts) (validator jolt-nil) (m #f))
|
||||
(cond
|
||||
((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"))
|
||||
(loop (cddr o) (cadr o)))
|
||||
(else (loop (cddr o) validator)))))
|
||||
(loop (cddr o) (cadr o) m))
|
||||
((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 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)
|
||||
(jolt-agent-err-set! a e)
|
||||
(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)))
|
||||
(when (and (not (jolt-nil? vf)) (jolt-not (jolt-invoke vf nv)))
|
||||
(error #f "Invalid reference state")))
|
||||
(jolt-agent-state-set! a nv)))
|
||||
(jolt-iref-state-throw)))
|
||||
(jolt-agent-state-set! a nv)
|
||||
(iref-notify a old nv)))
|
||||
(loop)))))
|
||||
|
||||
;; 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" "promise" jolt-promise-new)
|
||||
(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?)
|
||||
(def-var! "clojure.core" "send" jolt-agent-send)
|
||||
|
|
@ -552,9 +579,9 @@
|
|||
;; 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
|
||||
(guard (e (#t #f)) (foreign-procedure "pthread_sigmask" (int u8* u8*) int)))
|
||||
(define c-sigemptyset (guard (e (#t #f)) (foreign-procedure "sigemptyset" (u8*) int)))
|
||||
(define c-sigaddset (guard (e (#t #f)) (foreign-procedure "sigaddset" (u8* int) int)))
|
||||
(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".
|
||||
|
|
|
|||
|
|
@ -60,6 +60,12 @@
|
|||
;; branch and is mis-read as a missing :iterator key (nil). Some libraries
|
||||
;; (e.g. malli's -vmap) iterate a map this way.
|
||||
((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)))
|
||||
|
||||
;; Universal object-methods: on a
|
||||
|
|
|
|||
|
|
@ -841,7 +841,12 @@
|
|||
(define (make-class-obj name) (make-jhost "class" (vector name)))
|
||||
(define (jclass? x) (and (jhost? x) (string=? (jhost-tag x) "class")))
|
||||
(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)))
|
||||
(lambda (a b) (let ((ka (class-key a)) (kb (class-key b)))
|
||||
(and ka kb (string=? ka kb) #t))))
|
||||
|
|
@ -1047,19 +1052,62 @@
|
|||
#t 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
|
||||
;; 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"
|
||||
(lambda (x)
|
||||
(let ((name (class-key x)))
|
||||
(if name
|
||||
(let ((as (class-ancestors-list name))) ; transitive, like the JVM
|
||||
(let ((as (class-ancestors-rooted name)))
|
||||
(if (null? as) jolt-nil (list->cseq as)))
|
||||
jolt-nil))))
|
||||
(def-var! "jolt.host" "class-ancestors"
|
||||
(lambda (x)
|
||||
(let ((name (class-key x)))
|
||||
(if name
|
||||
(let ((as (class-ancestors-list name)))
|
||||
(let ((as (class-ancestors-rooted name)))
|
||||
(if (null? as) jolt-nil (list->cseq as)))
|
||||
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 ()
|
||||
(unless tried?
|
||||
(set! tried? #t)
|
||||
(set! fp (guard (e (#t #f))
|
||||
(load-shared-object #f)
|
||||
(foreign-procedure "sched_yield" () int))))
|
||||
(set! fp (jolt-foreign-proc-safe "sched_yield" '() 'int)))
|
||||
(if fp (fp) (sleep (make-time 'time-duration 0 0)))
|
||||
jolt-nil)))
|
||||
|
||||
|
|
@ -202,7 +200,8 @@
|
|||
(bitwise-ior (bitwise-arithmetic-shift-left r 1)
|
||||
(bitwise-and (bitwise-arithmetic-shift-right v i) 1)))))))
|
||||
(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 "bitCount" (lambda (n) (->num (bitwise-bit-count (bitwise-and (jnum->exact n) long-mask64)))))
|
||||
(cons "numberOfLeadingZeros" (lambda (n) (->num (long-nlz n))))
|
||||
|
|
@ -214,6 +213,8 @@
|
|||
(define (int->u32 n) (if (< n 0) (+ n 4294967296) n))
|
||||
(register-class-statics! "Integer"
|
||||
(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)
|
||||
(if (number? x) (->num x)
|
||||
(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
|
||||
;; libraries reference the JVM ranges — clojure.test.check generates over them).
|
||||
(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 "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))))))
|
||||
(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 "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))))))
|
||||
|
|
@ -247,13 +250,15 @@
|
|||
(cons "ROOT" "und") (cons "US" "en-US") (cons "ENGLISH" "en")))
|
||||
|
||||
(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)))
|
||||
|
||||
(register-class-ctor! "Double" ->double)
|
||||
(register-class-ctor! "Float" ->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 "toString" (lambda (x) (jolt-str-render-one (->double x))))
|
||||
(cons "isNaN" (lambda (x) (and (flonum? x) (nan? x))))
|
||||
|
|
@ -261,11 +266,13 @@
|
|||
(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)))
|
||||
(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).
|
||||
(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 "isDigit" (lambda (c) (let ((n (char-code c))) (and (>= n 48) (<= n 57)))))
|
||||
;; JVM Character.isWhitespace: Unicode whitespace (so U+2028 line separator
|
||||
|
|
|
|||
|
|
@ -80,13 +80,18 @@
|
|||
;; every Chez binary) — no external toolchain. Falls back to /bin/sh chmod if the
|
||||
;; symbol can't be resolved.
|
||||
(define jolt-chmod-755
|
||||
(let ((c (guard (e (#t #f))
|
||||
(load-shared-object #f)
|
||||
(foreign-procedure "chmod" (string int) int))))
|
||||
(let ((c (jolt-foreign-proc-safe "chmod" '(string int) 'int)))
|
||||
(lambda (path)
|
||||
(if c
|
||||
(c path #o755)
|
||||
(system (string-append "chmod 755 '" path "'"))))))
|
||||
(cond
|
||||
(c (c path #o755))
|
||||
;; 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
|
||||
;; the launcher cd'd to the jolt repo root — matching the JVM, where io/file is
|
||||
|
|
@ -624,6 +629,31 @@
|
|||
;; (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! "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 -----------------------------------------------------------
|
||||
;; 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) (and (jolt-array? x) (eq? (jolt-array-kind x) 'byte)))
|
||||
(define (na-identity x) x)
|
||||
(define (na-byte x)
|
||||
(let ((b (bitwise-and (exact (floor x)) #xff))) (if (>= b 128) (- b 256) b)))
|
||||
(define (na-short x)
|
||||
(let ((s (bitwise-and (exact (floor x)) #xffff))) (if (>= s #x8000) (- s #x10000) s)))
|
||||
(define (na-byte x) (jolt-byte-cast x))
|
||||
(define (na-short x) (jolt-short-cast x))
|
||||
|
||||
;; --- chunked seqs -----------------------------------------------------------
|
||||
;; 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))))
|
||||
(if (and (>= j 0) (< j (vector-length v))) (vector-ref v j) 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)
|
||||
(set! jolt-get
|
||||
(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)))))
|
||||
;; 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/
|
||||
|
|
|
|||
|
|
@ -42,6 +42,20 @@ if [ "$got_e" != "45" ]; then
|
|||
exit 1
|
||||
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
|
||||
# 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.
|
||||
|
|
|
|||
|
|
@ -384,3 +384,14 @@
|
|||
(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" "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)
|
||||
(let-values (((dk h) (parse-mm-opts opts)))
|
||||
(let ((mf (make-jolt-multifn (symbol-t-name name-sym) dispatch
|
||||
(new-mm-table) dk h (new-mm-table))))
|
||||
(def-var! (chez-current-ns) (symbol-t-name name-sym) mf)
|
||||
(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))))
|
||||
(def-var! ns (symbol-t-name name-sym) mf)
|
||||
mf)))
|
||||
|
||||
;; (defmethod-setup 'mm dispatch-val impl) — add a method. Auto-creates the multifn
|
||||
;; 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))
|
||||
(sns (symbol-t-ns mm-sym))
|
||||
(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);
|
||||
;; 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
|
||||
;; the current ns (a shadow, as before).
|
||||
;; the writing ns (a shadow, as before).
|
||||
(mns (cond
|
||||
(qns (or (chez-resolve-alias (chez-current-ns) qns) qns))
|
||||
((var-cell-lookup (chez-current-ns) nm) (chez-current-ns))
|
||||
((chez-resolve-refer (chez-current-ns) nm) => values)
|
||||
(else (chez-current-ns))))
|
||||
(qns (or (chez-resolve-alias here qns) qns))
|
||||
((var-cell-lookup here nm) here)
|
||||
((chez-resolve-refer here nm) => values)
|
||||
(else here)))
|
||||
(cur (var-deref mns nm))
|
||||
(mf (if (jolt-multifn? cur) cur
|
||||
;; auto-create: copy the dispatch fn + default from a same-named
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@
|
|||
;; so dispatch to its meta method rather than the identity side-table — which
|
||||
;; the deftype's reconstructed instances would not share.
|
||||
((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))
|
||||
(hashtable-ref meta-table x jolt-nil))
|
||||
(else jolt-nil)))
|
||||
;; everything else (collections, fns, reify, atoms/agents and any reference
|
||||
;; type) reads the identity side-table; a value with no entry is nil meta.
|
||||
(else (hashtable-ref meta-table x jolt-nil))))
|
||||
|
||||
;; 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.
|
||||
|
|
|
|||
|
|
@ -260,6 +260,9 @@
|
|||
;; intern: create/set a var ns/sym to val (or an unbound cell). Returns the var.
|
||||
(define (jolt-intern ns-desig sym . vopt)
|
||||
(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))))
|
||||
|
||||
;; alias / ns-unalias: register/drop an :as alias under the current (or given) ns.
|
||||
|
|
@ -306,13 +309,24 @@
|
|||
(loop (cddr a)))))
|
||||
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)
|
||||
(let* ((cur (or (hashtable-ref var-meta-table ref #f) (jolt-hash-map)))
|
||||
(new (apply jolt-invoke f cur args)))
|
||||
(hashtable-set! var-meta-table ref new)
|
||||
new))
|
||||
(define (jolt-reset-meta! ref m) (hashtable-set! var-meta-table ref m) m)
|
||||
(if (var-cell? ref)
|
||||
(let* ((cur (or (hashtable-ref var-meta-table ref #f) (jolt-hash-map)))
|
||||
(new (apply jolt-invoke f cur args)))
|
||||
(hashtable-set! var-meta-table ref new)
|
||||
new)
|
||||
(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 -------------------------------------
|
||||
;; Native-op primitives (+ map reduce …) are INLINED at emit, so they have no
|
||||
|
|
|
|||
|
|
@ -67,8 +67,13 @@
|
|||
;; 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.
|
||||
((cseq? x) (if (cseq-forced? x) #t #f))
|
||||
((empty-list-t? x) #t)
|
||||
;; 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))))))
|
||||
;; 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.
|
||||
|
|
|
|||
|
|
@ -48,12 +48,24 @@
|
|||
|
||||
(define (rdr-digit? c) (and (char>=? c #\0) (char<=? c #\9)))
|
||||
(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).
|
||||
(define (rdr-all-octal? s 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)))))
|
||||
|
||||
;; 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)
|
||||
(let loop ((i i))
|
||||
(cond
|
||||
|
|
@ -61,7 +73,8 @@
|
|||
((rdr-ws? (string-ref s i)) (loop (+ i 1)))
|
||||
((char=? (string-ref s i) #\;)
|
||||
(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)
|
||||
(eol (+ j 1)))))
|
||||
(else i))))
|
||||
|
|
@ -115,12 +128,17 @@
|
|||
(slash (rdr-string-index-char body #\/)))
|
||||
(cond
|
||||
;; 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
|
||||
(let ((n (string->number (substring body 0 slash)))
|
||||
(d (string->number (substring body (+ slash 1) blen))))
|
||||
(and (integer? n) (integer? d) (not (= d 0))
|
||||
(* sign (/ n d)))))
|
||||
(let ((ns (substring body 0 slash))
|
||||
(ds (substring body (+ slash 1) blen)))
|
||||
(and (rdr-all-digits? ns 0 (string-length ns))
|
||||
(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..
|
||||
((and (>= blen 2) (char=? (string-ref body 0) #\0)
|
||||
(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?).
|
||||
((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))))
|
||||
;; 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
|
||||
((and (> blen 1) (char=? (string-ref body (- blen 1)) #\N))
|
||||
(let ((n (string->number (substring body 0 (- blen 1)))))
|
||||
|
|
@ -190,7 +213,10 @@
|
|||
(let oct ((j (+ i 1)) (val 0) (cnt 0))
|
||||
(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))
|
||||
(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)
|
||||
(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,
|
||||
|
|
@ -208,7 +234,8 @@
|
|||
(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 (+ 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)))))))
|
||||
|
||||
;; backslash already consumed; read a Clojure character literal.
|
||||
|
|
@ -240,7 +267,10 @@
|
|||
((char=? (string-ref name 0) #\u)
|
||||
(integer->char (string->number (substring name 1 (string-length name)) 16)))
|
||||
((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)
|
||||
empty-pmap)))))
|
||||
|
||||
|
|
@ -258,14 +288,39 @@
|
|||
(values #f 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)
|
||||
(let ((n (rdr-try-number tok)))
|
||||
(cond
|
||||
(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 "true") #t)
|
||||
((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 ------------------------------------------------------------
|
||||
;; Read forms until the close delimiter; returns (values reversed?-no list j).
|
||||
|
|
@ -289,6 +344,14 @@
|
|||
;; sequence in a weak side-table the host contract's form-map-pairs consults.
|
||||
(define rdr-map-order (make-weak-eq-hashtable))
|
||||
(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)))
|
||||
(when (pair? es) (hashtable-set! rdr-map-order m es))
|
||||
m))
|
||||
|
|
@ -548,8 +611,12 @@
|
|||
(let-values (((src j) (rdr-read-regex s (+ i 1) end)))
|
||||
(values (jolt-re-pattern src) j)))
|
||||
((char=? c #\_) ; #_ discard the next form
|
||||
(let-values (((_ j) (rdr-read-form s (+ i 1) end)))
|
||||
(when (rdr-eof? _) (jolt-throw (jolt-ex-info "EOF after #_" empty-pmap)))
|
||||
(let-values (((d j) (rdr-read-form s (+ i 1) end)))
|
||||
(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)))
|
||||
((char=? c #\') ; #'x var-quote -> (var x)
|
||||
(let-values (((form j) (rdr-read-form s (+ i 1) end)))
|
||||
|
|
@ -601,6 +668,17 @@
|
|||
(let ((auto? (and (< i end) (char=? (string-ref s i) #\:))))
|
||||
(let ((i (if auto? (+ i 1) i)))
|
||||
(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)))
|
||||
(if auto?
|
||||
(let* ((cur (chez-current-ns))
|
||||
|
|
@ -735,15 +813,76 @@
|
|||
(let ((v (var-deref "clojure.core" "*default-data-reader-fn*")))
|
||||
(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,
|
||||
;; #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
|
||||
;; a tagged FORM (lenient — clojure.edn raises instead).
|
||||
(define (rdr-construct-tag tag inner)
|
||||
(cond
|
||||
((eq? tag (keyword #f "#inst")) (jolt-inst-from-string inner))
|
||||
((eq? tag (keyword #f "#uuid")) (jolt-uuid-from-string inner))
|
||||
((eq? tag (keyword #f "#inst"))
|
||||
(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))
|
||||
;; 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)))
|
||||
(if fn (jolt-invoke fn inner)
|
||||
(let ((dfn (rdr-default-data-reader-fn)))
|
||||
|
|
@ -765,7 +904,11 @@
|
|||
(let ((items (jolt-get x rdr-kw-value)))
|
||||
(let loop ((i 0) (s empty-pset))
|
||||
(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)
|
||||
(let-values (((items changed) (rdr-conv-each (vector->list (pvec-v x)))))
|
||||
(if changed (apply jolt-vector items) x)))
|
||||
|
|
@ -790,12 +933,35 @@
|
|||
(if (jolt-nil? m) v (jolt-with-meta v (rdr-form->data m)))))
|
||||
|
||||
;; --- 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
|
||||
;; (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.
|
||||
(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)))
|
||||
|
||||
;; 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)
|
||||
(let ((form (jolt-read-form-raw s)))
|
||||
(if (jolt-nil? form) form (rdr-form->data form))))
|
||||
|
|
@ -803,7 +969,7 @@
|
|||
;; __parse-next: [form rest-of-string] or nil when only whitespace/comments left.
|
||||
(define (jolt-parse-next 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)
|
||||
jolt-nil
|
||||
(jolt-vector (rdr-form->data form) (substring s j end))))))
|
||||
|
|
@ -812,8 +978,13 @@
|
|||
;; is the :#name keyword the reader produced; #uuid/#inst reuse the inst-time ctors.
|
||||
(define (jolt-read-tagged tag form)
|
||||
(cond
|
||||
((eq? tag (keyword #f "#uuid")) (jolt-uuid-from-string form))
|
||||
((eq? tag (keyword #f "#inst")) (jolt-inst-from-string form))
|
||||
((eq? tag (keyword #f "#uuid"))
|
||||
(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,
|
||||
;; 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
|
||||
|
|
@ -833,3 +1004,4 @@
|
|||
;; :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 […]).
|
||||
(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
|
||||
;; 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=?))
|
||||
;; 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=?))
|
||||
;; 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-
|
||||
|
|
@ -618,6 +622,11 @@
|
|||
;; 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-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
|
||||
;; ctor var "ns/->Name" the analyzer resolves a (->Name …) call to.
|
||||
(register-record-shape! (string-append (chez-current-ns) "/->" (symbol-t-name name-sym))
|
||||
|
|
@ -689,9 +698,14 @@
|
|||
type-name)))
|
||||
;; 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.
|
||||
;; 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)
|
||||
(jch-known? base) (jch-known? type-name))
|
||||
(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
|
||||
;; extender of the protocol (recorded inside type-registry so the per-case prune
|
||||
|
|
@ -731,6 +745,12 @@
|
|||
(let ((h (make-hashtable string-hash string=?))) (hashtable-set! type-registry tag h) h))))
|
||||
(unless (hashtable-ref ti proto-name #f)
|
||||
(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)
|
||||
|
||||
;; protocol-resolve: the impl procedure for obj — by record type tag, a reify's
|
||||
|
|
@ -1050,8 +1070,18 @@
|
|||
;; 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.
|
||||
(define (register-record-type! name-sym)
|
||||
(hashtable-set! chez-record-type-tbl
|
||||
(string-append (chez-current-ns) "." (symbol-t-name name-sym)) #t)
|
||||
(let ((tag (string-append (chez-current-ns) "." (symbol-t-name name-sym))))
|
||||
(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)
|
||||
(def-var! "clojure.core" "register-record-type!" register-record-type!)
|
||||
(def-var! "clojure.core" "make-protocol" make-protocol)
|
||||
|
|
|
|||
117
host/chez/rt.ss
117
host/chez/rt.ss
|
|
@ -11,6 +11,17 @@
|
|||
;; Emitted programs do `(load "host/chez/rt.ss")`; this loads values.ss in turn.
|
||||
|
||||
(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/seq.ss")
|
||||
|
||||
|
|
@ -49,6 +60,112 @@
|
|||
;; stack trace (source-registry.ss). call/cc is paid only on a throw, never per
|
||||
;; call; the captured k is walked, never invoked.
|
||||
(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))
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
;; reset between cases so there is no leakage — same isolation a fresh process gives.
|
||||
;;
|
||||
;; 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_DUMP_CRASH_LABELS=1 list crash + allowlisted labels
|
||||
(import (chezscheme))
|
||||
|
|
@ -196,7 +196,7 @@
|
|||
|
||||
;; Regression floor: fail on any NEW divergence or if pass drops below the 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))
|
||||
(when (or (> (length diverged) 0) (< pass floor))
|
||||
(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
|
|
@ -237,7 +237,12 @@
|
|||
(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) (fltruncate (fl/ n d)))))
|
||||
(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)))))
|
||||
|
|
@ -246,7 +251,11 @@
|
|||
((or (flonum? a) (flonum? b))
|
||||
(let ((n (real->flonum a)) (d (real->flonum b)))
|
||||
(if (fl= d 0.0) (jolt-div0-throw)
|
||||
(fl- n (fl* d (fltruncate (fl/ n d)))))))
|
||||
(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)))))))
|
||||
|
|
@ -436,11 +445,26 @@
|
|||
;; 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.
|
||||
;; ============================================================================
|
||||
;; (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)
|
||||
(cond
|
||||
((procedure? f) (apply f args))
|
||||
((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])
|
||||
;; 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-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
|
||||
|
|
@ -449,6 +473,9 @@
|
|||
=> (lambda (m) (apply jolt-invoke m f args)))
|
||||
((and (reified-methods f) (hashtable-ref (reified-methods f) "invoke" #f))
|
||||
=> (lambda (m) (apply jolt-invoke m f args)))
|
||||
;; host types registered as callable (promise delivers, …): consulted only
|
||||
;; after every built-in case missed, so the hot dispatch pays nothing.
|
||||
((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
|
||||
|
|
@ -634,8 +661,14 @@
|
|||
;; 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.
|
||||
(define (jolt-into to from)
|
||||
(meta-carry to
|
||||
(jolt-persistent! (reduce-seq (lambda (t x) (jolt-conj! t x)) (jolt-transient-new to) (jolt-seq 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
|
||||
(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)))))
|
||||
;; A bounded range is a real chunked-seq, like clojure.lang.LongRange: eager, with
|
||||
|
|
@ -740,8 +773,14 @@
|
|||
;; 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).
|
||||
(define (parity-int n) (if (flonum? n) (exact (floor n)) n))
|
||||
(define (jolt-even? n) (even? (parity-int n)))
|
||||
(define (jolt-odd? n) (odd? (parity-int n)))
|
||||
(define (jolt-parity-check 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-neg? n) (< n 0))
|
||||
(define (jolt-zero? n) (= n 0))
|
||||
|
|
@ -750,8 +789,18 @@
|
|||
;; ============================================================================
|
||||
;; 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)) '()))))
|
||||
(define (jolt-vals m) (if (jolt-nil? m) jolt-nil (list->cseq (pmap-fold m (lambda (k v a) (cons v a)) '()))))
|
||||
;; keys/vals of anything empty is nil (RT.keys over a nil seq); a non-empty
|
||||
;; 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);
|
||||
|
|
|
|||
|
|
@ -30,6 +30,39 @@ check_loc() {
|
|||
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 '(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'
|
||||
|
|
@ -60,6 +93,53 @@ check '(try (load-string "(+") (catch :default e (ex-message e)))' 'EOF while re
|
|||
check_loc '(throw (ex-info "boom" {}))' 'boom'
|
||||
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_out="$(bin/joltc --help 2>/dev/null)"
|
||||
if printf '%s' "$help_out" | grep -q -- '--nrepl-server'; then
|
||||
|
|
@ -159,5 +239,25 @@ else
|
|||
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"
|
||||
[ "$fails" -eq 0 ]
|
||||
|
|
|
|||
|
|
@ -57,10 +57,36 @@
|
|||
((symbol? nm) (symbol->string nm))
|
||||
(else #f)))))))
|
||||
|
||||
;; Walk a continuation, returning the registered jolt frames (innermost first) as
|
||||
;; (frame-name . record) pairs, where record is #(ns name file line) or the symbol
|
||||
;; 'ambiguous. Unmapped frames (host spine, anonymous lambdas) are skipped; raw
|
||||
;; depth is capped.
|
||||
;; Frame names that are pure Chez / jolt-runtime plumbing — the eval boundary,
|
||||
;; the var-cell trampoline, continuation/winder internals. They carry no Clojure
|
||||
;; meaning, so an unmapped frame with one of these names is dropped from the trace
|
||||
;; (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)
|
||||
;; 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.
|
||||
|
|
@ -70,39 +96,84 @@
|
|||
(if (or (not io) (fx>=? n 400))
|
||||
(reverse acc)
|
||||
(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)
|
||||
(display (string-append " [frame] " nm (if src " *MAPPED*" "") "\n")
|
||||
(display (string-append " [frame] " nm (if src " *MAPPED*"
|
||||
(if keep? "" " (skipped)")) "\n")
|
||||
(current-error-port)))
|
||||
(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))))))))
|
||||
|
||||
;; Render a list of (frame-name . record) pairs (innermost/deepest first) to a
|
||||
;; backtrace string. record is a source vector #(ns name file line) -> "ns/name
|
||||
;; (file:line)", or 'ambiguous / #f -> the bare frame name. A run of the same
|
||||
;; frame-name collapses to one "name (xN)" line (deep recursion, or a hot fn a
|
||||
;; loop re-enters), and the number of distinct lines is capped.
|
||||
(define (jolt-render-recs recs)
|
||||
(let ((port (open-output-string)))
|
||||
(let loop ((rs recs) (shown 0))
|
||||
(if (or (null? rs) (fx>=? shown 30))
|
||||
(get-output-string port)
|
||||
(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 " ")
|
||||
(if (vector? r)
|
||||
(let ((ns (vector-ref r 0)) (nm (vector-ref r 1))
|
||||
(file (vector-ref r 2)) (line (vector-ref r 3)))
|
||||
(put-string port ns) (put-string port "/") (put-string port nm)
|
||||
(when (string? file)
|
||||
(put-string port " (") (put-string port file)
|
||||
(put-string port ":") (put-string port (number->string line))
|
||||
(put-string port ")")))
|
||||
(put-string port frame-name)) ; 'ambiguous / unmapped: bare name
|
||||
(when (fx>? cnt 1)
|
||||
(put-string port " (x") (put-string port (number->string cnt)) (put-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))))
|
||||
|
||||
;; Multi-line backtrace for an uncaught value — " ns/name (file:line)" for a
|
||||
;; mapped frame, the bare frame name for an ambiguous one — or #f when no jolt
|
||||
;; frame maps (the caller then prints just the top-level location). Capped to the
|
||||
;; innermost frames.
|
||||
(define (jolt-backtrace-string v)
|
||||
(let ((k (jolt-error-continuation v)))
|
||||
(and k
|
||||
(let ((recs (jolt-frame-records k)))
|
||||
(and (pair? recs)
|
||||
(let ((port (open-output-string)))
|
||||
(let loop ((rs recs) (shown 0))
|
||||
(when (and (pair? rs) (fx<? shown 30))
|
||||
(let* ((p (car rs)) (frame-name (car p)) (r (cdr p)))
|
||||
(put-string port " ")
|
||||
(if (vector? r)
|
||||
(let ((ns (vector-ref r 0)) (nm (vector-ref r 1))
|
||||
(file (vector-ref r 2)) (line (vector-ref r 3)))
|
||||
(put-string port ns) (put-string port "/") (put-string port nm)
|
||||
(when (string? file)
|
||||
(put-string port " (") (put-string port file)
|
||||
(put-string port ":") (put-string port (number->string line))
|
||||
(put-string port ")")))
|
||||
(put-string port frame-name)) ; 'ambiguous: bare name
|
||||
(put-char port #\newline))
|
||||
(loop (cdr rs) (fx+ shown 1))))
|
||||
(get-output-string port)))))))
|
||||
(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:
|
||||
;; an ex-info shows its message + ex-data (+ a host cause); anything else is
|
||||
|
|
|
|||
|
|
@ -26,6 +26,12 @@ static int self_path(char *buf, uint32_t size) {
|
|||
/* _NSGetExecutablePath fills buf and reports the needed size on overflow. */
|
||||
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
|
||||
#include <unistd.h>
|
||||
static int self_path(char *buf, uint32_t size) {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,16 @@
|
|||
(let ((ht (make-hashtable key-hash jolt=2)))
|
||||
(pset-fold coll (lambda (e acc) (hashtable-set! ht e #t) acc) 0)
|
||||
(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`.
|
||||
(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)))
|
||||
(make-pvec out)))))))
|
||||
((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)
|
||||
;; promoted past the array capacity: hash order
|
||||
(let ((m empty-pmap-hash))
|
||||
|
|
|
|||
|
|
@ -188,9 +188,23 @@
|
|||
[false nil]
|
||||
(if or-map (keys or-map) [])))
|
||||
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
|
||||
(fn* proc [pat init acc]
|
||||
(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)
|
||||
(vector? pat)
|
||||
(let* [g (symbol (str (gensym)))
|
||||
|
|
@ -231,30 +245,45 @@
|
|||
;; group binds a :keys/:strs/:syms list. dnsp is the destructuring
|
||||
;; namespace from a qualified key like :ns/keys — it both prefixes
|
||||
;; 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
|
||||
(fn* group [a names kind dnsp]
|
||||
(fn* group [a names kind dnsp checked?]
|
||||
(if names
|
||||
(reduce
|
||||
;; s is a symbol (a b) or a keyword (:a :b); name/
|
||||
;; namespace handle both, so :keys [:major] binds
|
||||
;; `major` looking up :major (str would keep the colon).
|
||||
(fn* [aa s]
|
||||
(let* [local (name s)
|
||||
(fn* [aa pair]
|
||||
(let* [s (nth pair 0)
|
||||
bind? (nth pair 1)
|
||||
local (name s)
|
||||
nsp (or (namespace s) dnsp)
|
||||
keyform (cond
|
||||
(= kind :kw) (keyword (if nsp (str nsp "/" local) local))
|
||||
(= kind :str) local
|
||||
:else `(quote ~(symbol nsp local)))
|
||||
fo (find-or or-map local)]
|
||||
(conj (conj aa (symbol local))
|
||||
(if (nth fo 0)
|
||||
`(get ~gm ~keyform ~(nth fo 1))
|
||||
`(get ~gm ~keyform)))))
|
||||
a names)
|
||||
fo (find-or or-map local)
|
||||
lookup (cond
|
||||
checked? `(req! ~gm ~keyform)
|
||||
(nth fo 0) `(get ~gm ~keyform ~(nth fo 1))
|
||||
:else `(get ~gm ~keyform))]
|
||||
(cond
|
||||
bind? (conj (conj aa (symbol local)) lookup)
|
||||
checked? (conj (conj aa (symbol (str (gensym)))) lookup)
|
||||
:else aa)))
|
||||
a (classify names))
|
||||
a))
|
||||
g1 (group base (get pat :keys) :kw nil)
|
||||
g2 (group g1 (get pat :strs) :str nil)
|
||||
g3 (group g2 (get pat :syms) :sym nil)]
|
||||
g1 (group base (get pat :keys) :kw nil false)
|
||||
g2 (group g1 (get pat :strs) :str nil false)
|
||||
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
|
||||
;; its namespace; any other keyword is skipped; a non-keyword is a
|
||||
;; nested binding pattern.
|
||||
|
|
@ -262,9 +291,12 @@
|
|||
(if (keyword? k)
|
||||
(let* [kn (name k) kns (namespace k)]
|
||||
(cond
|
||||
(and kns (= kn "keys")) (group a (get pat k) :kw kns)
|
||||
(and kns (= kn "strs")) (group a (get pat k) :str kns)
|
||||
(and kns (= kn "syms")) (group a (get pat k) :sym kns)
|
||||
(and kns (= kn "keys")) (group a (get pat k) :kw kns false)
|
||||
(and kns (= kn "strs")) (group a (get pat k) :str kns false)
|
||||
(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))
|
||||
;; a direct binding {x :x}: apply its :or default
|
||||
;; (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)))
|
||||
a))))
|
||||
g3 (keys pat)))
|
||||
g6 (keys pat)))
|
||||
:else (throw (str "unsupported destructuring pattern: " (pr-str pat)))))
|
||||
ploop
|
||||
(fn* ploop [i acc]
|
||||
|
|
@ -545,6 +577,8 @@
|
|||
;; name binds only in the taken branch (temp# tests the value); via `let` so the
|
||||
;; binding form may itself destructure, matching Clojure.
|
||||
(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 [temp# ~tst]
|
||||
(if temp# (let [~form temp#] ~@body) nil))))
|
||||
|
|
|
|||
|
|
@ -155,8 +155,43 @@
|
|||
(when-let [s (seq coll)]
|
||||
(or (pred (first s)) (recur pred (next s)))))
|
||||
|
||||
(defn some-fn [& preds]
|
||||
(fn [& xs] (some (fn [p] (some p xs)) preds)))
|
||||
;; Reference arities: at least one predicate ((some-fn) is an arity error), and
|
||||
;; 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)))
|
||||
|
||||
|
|
@ -184,7 +219,8 @@
|
|||
;; 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] (and (number? x) (jolt.host/exact? 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)
|
||||
|
|
@ -239,7 +275,8 @@
|
|||
(loop [i 0 s (seq coll)]
|
||||
(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
|
||||
([f] (completing f identity))
|
||||
|
|
@ -298,16 +335,28 @@
|
|||
(defn val [e] (if (map-entry? e) (nth e 1) (throw (ex-info "val requires a map entry" {}))))
|
||||
|
||||
;; --- Ad-hoc hierarchies (stage 3) — Clojure's canonical pure-map port. -----
|
||||
;; A hierarchy is {:parents {tag #{parents}} :ancestors {tag #{all}}
|
||||
;; A hierarchy is {:parents {tag #{parents}} :ancestors {tag #{all}}
|
||||
;; :descendants {tag #{all}}}. The 3-arity forms are PURE; the 1/2-arity forms
|
||||
;; operate on the private global hierarchy atom. Multimethod dispatch
|
||||
;; (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 []
|
||||
{:parents {} :descendants {} :ancestors {}})
|
||||
|
||||
(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?
|
||||
([child parent] (isa? (deref global-hierarchy) child parent))
|
||||
([h child parent]
|
||||
|
|
@ -316,6 +365,10 @@
|
|||
;; so a class-keyed multimethod / (isa? (class x) C) dispatches like the JVM.
|
||||
(jolt.host/class-isa? 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)
|
||||
(= (count parent) (count child))
|
||||
(loop [ret true i 0]
|
||||
|
|
@ -325,24 +378,44 @@
|
|||
|
||||
(defn parents
|
||||
([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
|
||||
([tag] (ancestors (deref global-hierarchy) tag))
|
||||
([h tag]
|
||||
;; the user hierarchy plus any modeled JVM ancestry (jolt.host/class-ancestors)
|
||||
;; so (ancestors (class x)) answers like the JVM for the common interfaces.
|
||||
(let [hier (get (get h :ancestors) tag)
|
||||
host (jolt.host/class-ancestors tag)]
|
||||
(not-empty (if host (into (or hier #{}) host) hier)))))
|
||||
([h tag] (not-empty
|
||||
(let [ta (get (get h :ancestors) tag)]
|
||||
(if (class-tag? tag)
|
||||
;; the class's own ancestry plus hierarchy relationships derived
|
||||
;; on the class or any of its supers
|
||||
(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
|
||||
([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
|
||||
([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]
|
||||
(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)
|
||||
td (get h :descendants)
|
||||
ta (get h :ancestors)
|
||||
|
|
@ -350,14 +423,14 @@
|
|||
(reduce (fn [ret k]
|
||||
(assoc ret k
|
||||
(reduce conj (get targets k #{})
|
||||
(cons target (get targets target)))))
|
||||
m (cons source (get sources source))))]
|
||||
(cons target (targets target)))))
|
||||
m (cons source (sources source))))]
|
||||
(or
|
||||
(when-not (contains? (get tp tag #{}) parent)
|
||||
(when (contains? (get ta tag #{}) parent)
|
||||
(throw (str tag " already has " parent " as ancestor")))
|
||||
(when (contains? (get ta parent #{}) tag)
|
||||
(throw (str "Cyclic derivation: " parent " has " tag " as ancestor")))
|
||||
(when-not (contains? (tp tag) parent)
|
||||
(when (contains? (ta tag) parent)
|
||||
(throw (new Exception (str tag " already has " parent " as ancestor"))))
|
||||
(when (contains? (ta parent) tag)
|
||||
(throw (new Exception (str "Cyclic derivation: " parent " has " tag " as ancestor"))))
|
||||
{:parents (assoc tp tag (conj (get tp tag #{}) parent))
|
||||
:ancestors (tf ta tag td parent ta)
|
||||
:descendants (tf td parent ta tag td)})
|
||||
|
|
@ -367,15 +440,15 @@
|
|||
([tag parent] (swap! global-hierarchy underive tag parent) nil)
|
||||
([h tag parent]
|
||||
(let [parent-map (get h :parents)
|
||||
childs-parents (if (get parent-map tag)
|
||||
(disj (get parent-map tag) parent)
|
||||
childs-parents (if (parent-map tag)
|
||||
(disj (parent-map tag) parent)
|
||||
#{})
|
||||
new-parents (if (not-empty childs-parents)
|
||||
(assoc parent-map tag childs-parents)
|
||||
(dissoc parent-map tag))
|
||||
deriv-seq (mapcat (fn [e] (cons (key e) (interpose (key e) (val e))))
|
||||
(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))
|
||||
(make-hierarchy) (partition 2 deriv-seq))
|
||||
h))))
|
||||
|
|
@ -386,7 +459,8 @@
|
|||
(defn sequential? [x] (or (vector? x) (seq? x)))
|
||||
(defn associative? [x] (or (map? x) (vector? 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))
|
||||
;; sorted? is defined by the next tier (25-sorted) — declared here so this
|
||||
;; tier compiles (forward references are analysis errors).
|
||||
|
|
@ -394,7 +468,7 @@
|
|||
|
||||
(defn reversible? [x] (or (vector? x) (sorted? 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 double? [x] (and (number? x) (not (integer? x))))
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@
|
|||
;; Clojure. Collections only — a string is seqable but not shuffleable, as on
|
||||
;; the JVM (Collections/shuffle wants a Collection).
|
||||
(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) {})))
|
||||
(loop [v (vec coll) i (dec (count v))]
|
||||
(if (pos? i)
|
||||
|
|
@ -28,6 +29,10 @@
|
|||
(defn sort-by
|
||||
([keyfn coll] (sort-by keyfn compare 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)))
|
||||
|
||||
;; 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"})
|
||||
(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]
|
||||
(let [v (vec coll)] (nth v (rand-int (count v)))))
|
||||
(nth coll (rand-int (count coll))))
|
||||
|
||||
(defn random-sample
|
||||
([prob] (filter (fn [_] (< (rand) prob))))
|
||||
|
|
@ -348,8 +354,8 @@
|
|||
(defn clojure-version [] "1.11.0-jolt")
|
||||
|
||||
;; 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)" {})))
|
||||
(defn denominator [x] (throw (ex-info "denominator requires a ratio (Jolt has no ratios)" {})))
|
||||
;; numerator/denominator are host natives (converters.ss) over Chez's exact
|
||||
;; rationals; a non-ratio is the Ratio cast failure.
|
||||
|
||||
;; jolt has no reflection, but a few common JVM interfaces carry a modeled
|
||||
;; ancestry (jolt.host/class-supers) so reflective checks like
|
||||
|
|
|
|||
|
|
@ -89,6 +89,21 @@
|
|||
(recur nxt (next ks))))
|
||||
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.
|
||||
(defn memoize [f]
|
||||
(let [mem (atom (hash-map))]
|
||||
|
|
@ -136,6 +151,9 @@
|
|||
;; a deftype/record with its own empty (IPersistentCollection) — e.g.
|
||||
;; data.priority-map — uses it, before the generic map/set/vector arms.
|
||||
(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)
|
||||
(map? 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)))))
|
||||
([f g & fs] (reduce comp (comp f g) fs)))
|
||||
|
||||
;; Canonical IFn set: fns, keywords, symbols, maps (sorted incl.),
|
||||
;; sets, vectors, and vars — NOT lists ((ifn? '(1 2)) is false in Clojure).
|
||||
;; Canonical IFn set: fns, keywords, symbols, maps (sorted incl.), sets,
|
||||
;; 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]
|
||||
(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,
|
||||
;; so all of these are the checked ops; fixed arities mirror Clojure's
|
||||
|
|
@ -198,10 +222,9 @@
|
|||
(def inc' inc)
|
||||
(def dec' dec)
|
||||
;; unchecked-add / -subtract / -multiply / -negate / -inc / -dec (+ the -int
|
||||
;; variants) and -divide-int / -remainder-int are host-defined (host/chez/seq.ss):
|
||||
;; they WRAP to signed 64 bits like the JVM, which a plain (+ x y) overlay can't do.
|
||||
(defn unchecked-int [x] (int x))
|
||||
(def unchecked-long unchecked-int)
|
||||
;; variants), -divide-int / -remainder-int, and the unchecked-long/-int casts are
|
||||
;; host-defined (host/chez/seq.ss, converters.ss): they WRAP like the JVM
|
||||
;; primitive conversions, which a plain overlay over checked casts can't do.
|
||||
|
||||
;; int? is integer? on jolt: one number type, so fixed-precision and
|
||||
;; arbitrary-precision integers coincide.
|
||||
|
|
@ -263,12 +286,14 @@
|
|||
|
||||
(defn to-array-2d [coll] (to-array (map to-array coll)))
|
||||
|
||||
;; Masking integer coercions (not aliases): byte/short wrap to their width.
|
||||
;; unchecked-byte/short truncate to a number; unchecked-char returns a char (as on
|
||||
;; the JVM). int handles chars, so (unchecked-byte \a) works.
|
||||
(defn unchecked-byte [x] (bit-and (int x) 0xff))
|
||||
(defn unchecked-short [x] (bit-and (int x) 0xffff))
|
||||
(defn unchecked-char [x] (char (bit-and (int x) 0xffff)))
|
||||
;; Wrapping (unchecked) coercions: truncate to the width and sign-fold like the
|
||||
;; JVM primitive conversions ((unchecked-byte 200) is -56); unchecked-char wraps
|
||||
;; into char range. unchecked-long/int are host natives (converters.ss).
|
||||
(defn unchecked-byte [x]
|
||||
(let [b (bit-and (unchecked-long x) 0xff)] (if (< b 128) b (- b 256))))
|
||||
(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-double [x] (double x))
|
||||
|
||||
|
|
@ -323,7 +348,8 @@
|
|||
;; stays an unevaluated reader form on jolt and contains? can't see into it.
|
||||
(def ^:private special-syms
|
||||
#{'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))
|
||||
|
||||
|
|
@ -338,3 +364,14 @@
|
|||
(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 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)
|
||||
args (if (and (map? (first args)) (not (symbol? (first args)))) (rest args) args)
|
||||
dispatch (first args)
|
||||
opts (rest args)]
|
||||
`(defmulti-setup (quote ~name) ~dispatch ~@opts)))
|
||||
opts (rest args)
|
||||
;; 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]
|
||||
`(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
|
||||
;; VAR (the value is just the dispatch closure), so these pass the name quoted
|
||||
|
|
|
|||
|
|
@ -172,6 +172,14 @@
|
|||
(def var-cache? (atom false))
|
||||
(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
|
||||
;; jv$<ns>$<name>; chars that break a Scheme identifier or the `$` separator are
|
||||
;; escaped so distinct vars never collide.
|
||||
|
|
@ -191,6 +199,13 @@
|
|||
;; recursion auto-restores them (no manual save/restore, no throw-leak).
|
||||
(def ^:dynamic *recur-target* nil)
|
||||
(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))
|
||||
(defn- fresh-label [prefix] (str prefix (swap! gensym-counter inc)))
|
||||
|
|
@ -253,6 +268,17 @@
|
|||
(if (or (contains? scheme-reserved s) (contains? bare-native-names s)) (str "_" s) s)))
|
||||
|
||||
(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
|
||||
;; codepoint hex escape \x<cp>; ; the named escapes (\n \t \r \" \\) match what
|
||||
|
|
@ -413,9 +439,10 @@
|
|||
;; 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*.
|
||||
(defn- emit-let [node]
|
||||
(let [kw (if (:letrec node) "letrec*" "let*")]
|
||||
(str "(" kw " (" (str/join " " (map emit-binding (:bindings node))) ") "
|
||||
(emit (:body node)) ")")))
|
||||
(let [kw (if (:letrec node) "letrec*" "let*")
|
||||
;; bindings are non-tail; the body inherits the let's tail position
|
||||
binds (binding [*tail?* false] (str/join " " (mapv emit-binding (:bindings node))))]
|
||||
(str "(" kw " (" binds ") " (emit (:body node)) ")")))
|
||||
|
||||
(defn- emit-loop [node]
|
||||
(let [label (fresh-label "loop")
|
||||
|
|
@ -423,9 +450,10 @@
|
|||
names (map #(munge-name (nth % 0)) pairs)
|
||||
;; inits evaluate in the OUTER scope (recur-target unchanged) and, like
|
||||
;; 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))
|
||||
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)))]
|
||||
(str "(let* (" seq-bs ") (let " label " (" rebinds ") " body "))")))
|
||||
|
||||
|
|
@ -486,7 +514,11 @@
|
|||
params (map munge-name orig)
|
||||
restp (when-let [r (:rest a)] (munge-name r))
|
||||
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
|
||||
(and restp (empty? params)) restp
|
||||
restp (str "(" (str/join " " params) " . " restp ")")
|
||||
|
|
@ -511,6 +543,16 @@
|
|||
self (when-let [nm (:name node)] (munge-name nm))
|
||||
clauses (binding [*known-procs* (if self (conj *known-procs* self) *known-procs*)]
|
||||
(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))
|
||||
(let [c (first clauses)] (str "(lambda " (nth c 0) " " (nth c 1) ")"))
|
||||
(str "(case-lambda "
|
||||
|
|
@ -573,8 +615,31 @@
|
|||
(= (nth shape i) kw) 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]
|
||||
(let [fnode (:fn node)
|
||||
(let [tail? *tail?*] ; capture: children below emit non-tail
|
||||
(binding [*tail?* false]
|
||||
(let [fnode (:fn node)
|
||||
arg-nodes (:args node)
|
||||
args (mapv emit arg-nodes)
|
||||
nop (native-op fnode (count args))
|
||||
|
|
@ -586,8 +651,7 @@
|
|||
;; order [callee & args] together when ordering is observable.
|
||||
invoke (fn []
|
||||
(ordered-call (cons fnode arg-nodes) (cons (emit fnode) args)
|
||||
(fn [[f & as]]
|
||||
(str "(jolt-invoke " f (if (seq as) (str " " (str/join " " as)) "") ")"))))]
|
||||
(fn [operands] (emit-call tail? "jolt-invoke" operands))))]
|
||||
(cond
|
||||
;; 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
|
||||
|
|
@ -636,11 +700,16 @@
|
|||
(if idx
|
||||
(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) ")")))))
|
||||
;; (coll k [default]) -> (jolt-get coll k [default]) — coll (fnode) is the
|
||||
;; callee, evaluated before the key/default args.
|
||||
;; (coll k [default]) -> lookup — coll (fnode) is the callee, evaluated
|
||||
;; before the key/default args. A VECTOR literal invokes as nth (a bad
|
||||
;; index throws, IPersistentVector.invoke); maps/sets invoke as get.
|
||||
(= kind :coll)
|
||||
(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?)))
|
||||
(throw (ex-info (str "emit: unsupported stdlib fn `" (:ns fnode) "/" (:name fnode)
|
||||
"` (no core on Chez yet)") {}))
|
||||
|
|
@ -657,8 +726,7 @@
|
|||
;; holds an arbitrary IFn -> dynamic dispatch.
|
||||
(= :local (:op fnode))
|
||||
(if (*known-procs* (munge-name (:name fnode)))
|
||||
(order-args (fn [as] (str "(" (munge-name (:name fnode))
|
||||
(if (seq as) (str " " (str/join " " as)) "") ")")))
|
||||
(order-args (fn [as] (emit-call tail? (munge-name (:name fnode)) as)))
|
||||
(invoke))
|
||||
;; 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.
|
||||
|
|
@ -667,8 +735,7 @@
|
|||
;; below (which still uses the direct binding as the invoke target).
|
||||
(and (= :var (:op fnode)) (direct-linkable? (:ns fnode) (:name fnode))
|
||||
(direct-link-fn? (:ns fnode) (:name fnode)))
|
||||
(order-args (fn [as] (str "(" (dl-name (:ns fnode) (:name fnode))
|
||||
(if (seq as) (str " " (str/join " " as)) "") ")")))
|
||||
(order-args (fn [as] (emit-call tail? (dl-name (:ns fnode) (:name fnode)) as)))
|
||||
;; a late-bound :var call head can hold a procedure OR a non-applicable
|
||||
;; value the RT dispatches (multimethod, keyword/coll IFn) — route via
|
||||
;; jolt-invoke (transparent for a procedure).
|
||||
|
|
@ -676,7 +743,7 @@
|
|||
(invoke)
|
||||
;; a computed callee can yield ANY IFn — route through jolt-invoke.
|
||||
:else
|
||||
(invoke))))
|
||||
(invoke))))))
|
||||
|
||||
;; try/catch/finally. throw raises a Chez condition wrapping the jolt value
|
||||
;; (jolt-throw = Scheme `raise` of a &jolt-throw condition); catch lowers to
|
||||
|
|
@ -723,7 +790,22 @@
|
|||
(returns-scheme-bool? (:body node) bools'))
|
||||
: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)
|
||||
:const (emit-const (:val node))
|
||||
:local (munge-name (:name node))
|
||||
|
|
@ -771,11 +853,14 @@
|
|||
:host-new (str "(host-new " (chez-str-lit (:class node))
|
||||
(let [args (map emit (:args node))]
|
||||
(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)
|
||||
t (if (returns-scheme-bool? test) (emit test)
|
||||
(str "(jolt-truthy? " (emit test) ")"))]
|
||||
t (binding [*tail?* false]
|
||||
(if (returns-scheme-bool? test) (emit test)
|
||||
(str "(jolt-truthy? " (emit test) ")")))]
|
||||
(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)) ")")
|
||||
:invoke (emit-invoke node)
|
||||
;; collection literals -> rt constructors (collections.ss). Elements are
|
||||
|
|
@ -819,15 +904,17 @@
|
|||
:fn (emit-fn node)
|
||||
;; (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).
|
||||
:def (cond
|
||||
(:no-init node)
|
||||
(str "(declare-var! " (chez-str-lit (:ns node)) " " (chez-str-lit (:name node)) ")")
|
||||
(jmeta-nonempty? (:meta node))
|
||||
(str "(def-var-with-meta! " (chez-str-lit (:ns node)) " " (chez-str-lit (:name node)) " "
|
||||
(emit-with-cells #(emit (:init node))) " " (emit-def-meta node) ")")
|
||||
:else
|
||||
(str "(def-var! " (chez-str-lit (:ns node)) " " (chez-str-lit (:name node)) " "
|
||||
(emit-with-cells #(emit (:init node))) ")"))
|
||||
:def (let [reg (trace-source-reg node)
|
||||
d (cond
|
||||
(:no-init node)
|
||||
(str "(declare-var! " (chez-str-lit (:ns node)) " " (chez-str-lit (:name node)) ")")
|
||||
(jmeta-nonempty? (:meta node))
|
||||
(str "(def-var-with-meta! " (chez-str-lit (:ns node)) " " (chez-str-lit (:name node)) " "
|
||||
(emit-with-cells #(emit (:init node))) " " (emit-def-meta node) ")")
|
||||
:else
|
||||
(str "(def-var! " (chez-str-lit (:ns node)) " " (chez-str-lit (:name 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))) {}))))
|
||||
|
||||
;; ^: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))
|
||||
(:jolt/module spec)
|
||||
(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
|
||||
(do (warn "skipping unsupported coordinate " coord " " (pr-str spec)) nil)))
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
(defn- project-dir [] (or (jolt.host/getenv "JOLT_PWD") "."))
|
||||
|
||||
(defn- version [] (jolt.host/jolt-version))
|
||||
|
||||
(defn- current-platform []
|
||||
(let [os (str/lower-case (or (System/getProperty "os.name") ""))]
|
||||
(cond (str/includes? os "mac") :darwin
|
||||
|
|
@ -145,7 +147,10 @@
|
|||
;; loaded — same context a run gets, so (require '[some.lib]) works in the REPL.
|
||||
(try (apply-project! (deps/resolve-project (project-dir)))
|
||||
(catch :default _ nil))
|
||||
(println ";; jolt repl — :repl/quit or ^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 []
|
||||
(let [form (repl-read-form)]
|
||||
(when form
|
||||
|
|
@ -158,7 +163,9 @@
|
|||
(catch :default e
|
||||
(println "error:" (or (ex-message e)
|
||||
(try ((resolve 'jolt.host/condition-message) e) (catch :default _ nil))
|
||||
(pr-str e)))))
|
||||
(pr-str e)))
|
||||
(when-let [bt (jolt.host/backtrace-string)]
|
||||
(print bt))))
|
||||
(recur)))))))
|
||||
|
||||
;; A deps.edn :tasks entry: a string is a shell command; a map is {:main-opts …}.
|
||||
|
|
@ -294,7 +301,9 @@
|
|||
(when stop (stop))))))
|
||||
|
||||
(defn- usage []
|
||||
(println (str "jolt " (version)))
|
||||
(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 FILE load a Clojure file")
|
||||
(println " build -m NS [-o OUT] [--opt|--dev] [--direct-link] [--tree-shake] [--dynamic] compile a standalone binary")
|
||||
|
|
@ -304,6 +313,7 @@
|
|||
(println " --nrepl-server [port] start an nREPL server (default 7888) for editors")
|
||||
(println " path print the resolved source roots")
|
||||
(println " <task> run a deps.edn :tasks entry")
|
||||
(println " --version print the jolt version")
|
||||
(println " --help print this message"))
|
||||
|
||||
(defn -main [& args]
|
||||
|
|
@ -312,6 +322,7 @@
|
|||
(nil? cmd) (usage)
|
||||
(= cmd "--help") (usage)
|
||||
(= cmd "-h") (usage)
|
||||
(#{"--version" "-V"} cmd) (println (str "jolt " (version)))
|
||||
(= cmd "run") (cmd-run more)
|
||||
(= cmd "repl") (repl)
|
||||
(= cmd "--nrepl-server") (nrepl more)
|
||||
|
|
|
|||
|
|
@ -21,25 +21,66 @@
|
|||
[jolt.ffi :as ffi]))
|
||||
|
||||
;; --- sockets (loopback server) ---------------------------------------------
|
||||
;; Load libc (the running process's symbols) BEFORE the foreign-fn bindings below
|
||||
;; — defcfn resolves the C entry point when the def is evaluated (at ns load), so
|
||||
;; the socket symbols must already be available.
|
||||
(ffi/load-library)
|
||||
(def ^:private os-name
|
||||
(str/lower-case (or (System/getProperty "os.name") "")))
|
||||
(def ^:private macos? (str/includes? os-name "mac"))
|
||||
(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-bind "bind" [:int :pointer :int] :int)
|
||||
(ffi/defcfn c-listen "listen" [:int :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-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)
|
||||
(ffi/defcfn c-accept "accept" [:int :pointer :pointer] :int :blocking)
|
||||
|
||||
;; recv/send and the socket-close call differ by platform. Winsock's recv/send
|
||||
;; take an int length and return int (not ssize_t), and a socket is closed with
|
||||
;; closesocket, not close. A symbol that exists on only one OS (closesocket on
|
||||
;; Windows, close on POSIX) can only be bound there, so these live in the taken
|
||||
;; platform branch — jolt interns the vars from both branches at analysis time,
|
||||
;; so later references resolve either way.
|
||||
(if windows?
|
||||
(do
|
||||
(ffi/defcfn c-recv "recv" [:int :pointer :int :int] :int :blocking)
|
||||
(ffi/defcfn c-send "send" [:int :pointer :int :int] :int :blocking)
|
||||
(ffi/defcfn c-close "closesocket" [:int] :int)
|
||||
;; Winsock must be initialized once per process before any socket call.
|
||||
(ffi/defcfn c-wsastartup "WSAStartup" [:int :pointer] :int))
|
||||
(do
|
||||
(ffi/defcfn c-recv "recv" [:int :pointer :size_t :int] :ssize_t :blocking)
|
||||
(ffi/defcfn c-send "send" [:int :pointer :size_t :int] :ssize_t :blocking)
|
||||
(ffi/defcfn c-close "close" [:int] :int)))
|
||||
|
||||
(def ^:private AF-INET 2)
|
||||
(def ^:private SOCK-STREAM 1)
|
||||
(def ^:private macos?
|
||||
(str/includes? (str/lower-case (or (System/getProperty "os.name") "")) "mac"))
|
||||
(def ^:private sol-socket (if macos? 0xffff 1))
|
||||
(def ^:private so-reuse (if macos? 4 2))
|
||||
;; SOL_SOCKET / SO_REUSEADDR: 0xffff / 4 on macOS and Windows, 1 / 2 on Linux.
|
||||
(def ^:private sol-socket (if (or macos? windows?) 0xffff 1))
|
||||
(def ^:private so-reuse (if (or macos? windows?) 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]
|
||||
(let [sa (ffi/alloc 16)]
|
||||
|
|
@ -53,7 +94,7 @@
|
|||
sa))
|
||||
|
||||
(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)]
|
||||
(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))
|
||||
|
|
@ -147,7 +188,10 @@
|
|||
(try (when (and ns-str (not (str/blank? ns-str)) (find-ns (symbol ns-str)))
|
||||
(in-ns (symbol ns-str)))
|
||||
(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))
|
||||
:out out
|
||||
:ns (str (ns-name *ns*))
|
||||
|
|
@ -236,11 +280,17 @@
|
|||
no-op."
|
||||
([port] (start port nil))
|
||||
([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 [])))
|
||||
fd (listen-socket port) ; throws on bind/listen failure
|
||||
stopped (atom false)]
|
||||
(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))))
|
||||
(println ";; connect your editor; ^C to stop")
|
||||
(future
|
||||
|
|
|
|||
|
|
@ -16,7 +16,15 @@
|
|||
;; Reader FORMS are detected by :jolt/type tag, never by map? — strict map?
|
||||
;; (correctly) excludes tagged structs, so the old (and (map? x) ...) guard
|
||||
;; 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
|
||||
;; data readers (#uuid/#inst + registered); an unknown tag falls to the
|
||||
;; :default opt fn (called with tag and value, as in Clojure) or throws.
|
||||
|
|
@ -30,6 +38,9 @@
|
|||
custom (get (get opts :readers) tag-sym)]
|
||||
(cond
|
||||
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.
|
||||
(get opts :default) ((get opts :default) tag-sym v)
|
||||
:else (__read-tagged tag v)))
|
||||
|
|
@ -39,25 +50,30 @@
|
|||
;; a constructed set: recurse into its elements too, so a tagged literal
|
||||
;; 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)))
|
||||
(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))
|
||||
|
||||
;; Private helper, NOT named read-string: an unqualified (read-string …) call
|
||||
;; 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.
|
||||
(defn- read-edn [opts s]
|
||||
(if (or (nil? s) (cstr/blank? s))
|
||||
(get opts :eof nil)
|
||||
;; read the RAW form (tagged/set literals stay forms) so edn->value applies
|
||||
;; every #tag through :readers/:default — read-string would build the built-in
|
||||
;; #inst/#uuid eagerly, ignoring an override and failing on a non-string form.
|
||||
(edn->value opts (__read-form-raw s))))
|
||||
;; the strict edn seam: no auto-resolved keywords, invalid tokens throw, and
|
||||
;; each #_ discard is validated through the same :readers/:default pipeline.
|
||||
;; EOF (blank/comment-only/nil input) honors :eof; an opts map WITHOUT :eof
|
||||
;; makes end-of-input an error, like the reference.
|
||||
(let [v (__read-form-edn s (fn [form] (edn->value opts form) nil))]
|
||||
(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
|
||||
"Reads one object from the string s. Returns the :eof option value (default
|
||||
nil) for nil or blank input. opts is an options map; :eof sets the value
|
||||
returned at end of input."
|
||||
([s] (read-edn {} s))
|
||||
"Reads one object from the string s. The no-opts arity returns nil at end of
|
||||
input; with an opts map, :eof sets the value returned at end of input and its
|
||||
absence makes end-of-input an error."
|
||||
([s] (read-edn {:eof nil} s))
|
||||
([opts s] (read-edn opts s)))
|
||||
|
||||
(defn- drain-reader
|
||||
|
|
|
|||
|
|
@ -6,28 +6,32 @@
|
|||
(if (nil? s) true
|
||||
(= 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
|
||||
|
||||
[s]
|
||||
(if (< 1 (count s))
|
||||
(str (str-upper (subs s 0 1))
|
||||
(str-lower (subs s 1)))
|
||||
(str-upper s)))
|
||||
(let [s (to-str s)]
|
||||
(if (< 1 (count s))
|
||||
(str (str-upper (subs s 0 1))
|
||||
(str-lower (subs s 1)))
|
||||
(str-upper s))))
|
||||
|
||||
(defn lower-case
|
||||
|
||||
[s]
|
||||
(str-lower s))
|
||||
(str-lower (to-str s)))
|
||||
|
||||
(defn upper-case
|
||||
|
||||
[s]
|
||||
(str-upper s))
|
||||
(str-upper (to-str s)))
|
||||
|
||||
(defn includes?
|
||||
|
||||
[s substr]
|
||||
(not (nil? (str-find substr s))))
|
||||
(not (nil? (str-find substr (to-str s)))))
|
||||
|
||||
(defn join
|
||||
|
||||
|
|
@ -36,11 +40,11 @@
|
|||
|
||||
(defn replace
|
||||
[s match replacement]
|
||||
(str-replace-all match replacement s))
|
||||
(str-replace-all match replacement (to-str s)))
|
||||
|
||||
(defn replace-first
|
||||
[s match replacement]
|
||||
(str-replace match replacement s))
|
||||
(str-replace match replacement (to-str s)))
|
||||
|
||||
(defn reverse
|
||||
[s]
|
||||
|
|
@ -68,16 +72,18 @@
|
|||
(vec (str-split #"\r?\n" s)))
|
||||
|
||||
(defn starts-with?
|
||||
|
||||
[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)
|
||||
(= (subs s 0 slen2) substr))))
|
||||
|
||||
(defn ends-with?
|
||||
|
||||
[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)
|
||||
(= (subs s (- slen slen2)) substr))))
|
||||
|
||||
|
|
@ -97,8 +103,8 @@
|
|||
(str-trimr s))
|
||||
|
||||
(defn escape
|
||||
|
||||
[s cmap]
|
||||
(when (nil? s) (throw (new NullPointerException "s")))
|
||||
(apply str
|
||||
(map (fn [ch]
|
||||
(if-let [rep (cmap ch)] rep (str ch)))
|
||||
|
|
@ -107,9 +113,9 @@
|
|||
(defn index-of
|
||||
"0-based index of the first occurrence of value in s, or nil."
|
||||
([s value]
|
||||
(str-find value s))
|
||||
(str-find value (to-str s)))
|
||||
([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)))))
|
||||
|
||||
(defn last-index-of
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ test` from the repo root.
|
|||
|
||||
## 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`.
|
||||
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`.
|
||||
|
|
@ -22,7 +22,7 @@ answers with `regen-corpus.clj` and re-certify with `test/conformance/certify.cl
|
|||
|
||||
chez --script host/chez/run-corpus.ss
|
||||
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
|
||||
JVM-portable corpus: dot-forms, java statics, io, reader, walk, vars/namespaces,
|
||||
|
|
|
|||
7082
test/chez/corpus.edn
7082
test/chez/corpus.edn
File diff suppressed because it is too large
Load diff
|
|
@ -2,81 +2,31 @@
|
|||
# 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 3
|
||||
clojure.core-test.ancestors 9 0
|
||||
clojure.core-test.atom 14 0
|
||||
clojure.core-test.add-watch 0 1
|
||||
clojure.core-test.bigint 6 0
|
||||
clojure.core-test.bit-set 1 0
|
||||
clojure.core-test.boolean-qmark 0 3
|
||||
clojure.core-test.byte 7 5
|
||||
clojure.core-test.char 1 0
|
||||
clojure.core-test.compare 1 0
|
||||
clojure.core-test.conj 1 0
|
||||
clojure.core-test.contains-qmark 3 0
|
||||
clojure.core-test.counted-qmark 1 0
|
||||
clojure.core-test.dec 1 0
|
||||
clojure.core-test.denominator 0 3
|
||||
clojure.core-test.derive 21 0
|
||||
clojure.core-test.descendants 4 0
|
||||
clojure.core-test.double 0 4
|
||||
clojure.core-test.double-qmark 3 0
|
||||
clojure.core-test.empty 1 0
|
||||
clojure.core-test.eq 2 0
|
||||
clojure.core-test.eval 0 3
|
||||
clojure.core-test.even-qmark 1 0
|
||||
clojure.core-test.float 4 4
|
||||
clojure.core-test.get 0 1
|
||||
clojure.core-test.ifn-qmark 2 0
|
||||
clojure.core-test.float 1 0
|
||||
clojure.core-test.inc 1 0
|
||||
clojure.core-test.int 4 5
|
||||
clojure.core-test.int-qmark 3 0
|
||||
clojure.core-test.intern 2 0
|
||||
clojure.core-test.keys 0 4
|
||||
clojure.core-test.lazy-seq 3 0
|
||||
clojure.core-test.long 2 5
|
||||
clojure.core-test.lazy-seq 1 2
|
||||
clojure.core-test.minus 2 0
|
||||
clojure.core-test.mod 23 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.nth 0 1
|
||||
clojure.core-test.num 2 1
|
||||
clojure.core-test.number-qmark 3 0
|
||||
clojure.core-test.numerator 0 3
|
||||
clojure.core-test.odd-qmark 1 0
|
||||
clojure.core-test.parents 10 0
|
||||
clojure.core-test.num 2 0
|
||||
clojure.core-test.parse-uuid 3 0
|
||||
clojure.core-test.peek 2 0
|
||||
clojure.core-test.peek 1 0
|
||||
clojure.core-test.plus 11 0
|
||||
clojure.core-test.plus-squote 11 0
|
||||
clojure.core-test.pop 0 1
|
||||
clojure.core-test.pos-int-qmark 1 0
|
||||
clojure.core-test.quot 30 0
|
||||
clojure.core-test.rand-nth 0 1
|
||||
clojure.core-test.rational-qmark 3 0
|
||||
clojure.core-test.realized-qmark 3 0
|
||||
clojure.core-test.reduce 0 1
|
||||
clojure.core-test.rem 21 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.run-bang 1 0
|
||||
clojure.core-test.select-keys 2 0
|
||||
clojure.core-test.seqable-qmark 1 0
|
||||
clojure.core-test.short 7 5
|
||||
clojure.core-test.shuffle 1 0
|
||||
clojure.core-test.some-fn 3 0
|
||||
clojure.core-test.sort-by 2 0
|
||||
clojure.core-test.special-symbol-qmark 4 0
|
||||
clojure.core-test.star 13 0
|
||||
clojure.core-test.star-squote 13 0
|
||||
clojure.core-test.transient 23 0
|
||||
clojure.core-test.underive 7 0
|
||||
clojure.core-test.update 1 0
|
||||
clojure.core-test.vals 0 3
|
||||
clojure.core-test.transient 4 0
|
||||
clojure.core-test.vec 1 0
|
||||
clojure.core-test.when-let 1 0
|
||||
clojure.edn-test.read-string 46 5
|
||||
clojure.string-test.capitalize 0 4
|
||||
clojure.string-test.ends-with-qmark 1 4
|
||||
clojure.string-test.escape 1 0
|
||||
clojure.string-test.lower-case 0 4
|
||||
clojure.string-test.starts-with-qmark 1 10
|
||||
clojure.string-test.upper-case 0 4
|
||||
|
|
|
|||
|
|
@ -576,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 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]"}
|
||||
|
||||
;; 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 |
|
||||
|------|------|--------------|
|
||||
| `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/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. | — |
|
||||
|
|
@ -24,11 +24,17 @@ the canonical, frozen contract**: it is what every runtime consumes, what
|
|||
## Row schema
|
||||
|
||||
```edn
|
||||
{:suite "numbers / arithmetic" ; grouping; "<suite> :: <label>" is the case id
|
||||
:label "integer add" ; unique within a suite
|
||||
:actual "(+ 1 2)" ; Clojure source to evaluate
|
||||
:expected "3"} ; Clojure source whose value it must equal,
|
||||
{:suite "numbers / arithmetic" ; grouping; "<suite> :: <label>" is the case id
|
||||
:label "integer add" ; unique within a suite
|
||||
:actual "(+ 1 2)" ; Clojure source to evaluate
|
||||
:expected "3" ; Clojure source whose value it must equal,
|
||||
; 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
|
||||
|
|
@ -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
|
||||
weakness: it evaluates every `:actual` (and `:expected`) on **JVM Clojure** in a
|
||||
fresh `user` namespace and checks jolt's `:expected` against what real Clojure
|
||||
produces. Of ~2740 vanilla-certifiable rows, **>2730 match reference Clojure
|
||||
exactly**. The rest are classified (see below) — none are silently wrong.
|
||||
produces. Of ~3400 vanilla-certifiable rows, **all but ~26 allowlisted rows
|
||||
match reference Clojure exactly**. The rest are classified (see below) — none are silently wrong.
|
||||
|
||||
```sh
|
||||
clojure -M test/conformance/certify.clj # gate
|
||||
|
|
@ -132,6 +138,19 @@ arithmetic fast path (both operands Chez numbers → the raw Chez op) or force
|
|||
every `+`/`-`/`*` through an unwrapping dispatcher, de-optimizing all
|
||||
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
|
||||
|
|
@ -192,3 +211,39 @@ corpus`.
|
|||
deliberate delta (classify it in `known-divergences.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`).
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
|
|
@ -99,11 +99,12 @@
|
|||
[row :kept])))
|
||||
|
||||
;; --- 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)
|
||||
" :label " (pr-str label)
|
||||
" :expected " (if (= expected :throws) ":throws" (pr-str expected))
|
||||
" :actual " (pr-str actual) "}"))
|
||||
" :actual " (pr-str actual)
|
||||
(when portability (str " :portability " portability)) "}"))
|
||||
|
||||
(defn -main [& _]
|
||||
(let [corpus (edn/read-string (slurp corpus-path))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue