diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 732fbb0..6ecfd66 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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,54 @@ 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 + install: >- + git make vim unzip zip + mingw-w64-x86_64-gcc + mingw-w64-x86_64-lz4 + mingw-w64-x86_64-zlib + + - 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 --installprefix="$GITHUB_WORKSPACE/chez-install" + make -j"$(nproc)" + make install + + - name: Put chez on PATH (Windows) + if: runner.os == 'Windows' + run: | + printf '#!/bin/sh +exec "%s/chez-install/bin/scheme" "$@" +' "$GITHUB_WORKSPACE" > "$GITHUB_WORKSPACE/chez-install/bin/chez" + chmod +x "$GITHUB_WORKSPACE/chez-install/bin/chez" + echo "$GITHUB_WORKSPACE/chez-install/bin" >> "$GITHUB_PATH" + # cc is the build's compiler name; alias it to mingw gcc + printf '#!/bin/sh +exec gcc "$@" +' > /usr/local/bin/cc || printf '#!/bin/sh +exec gcc "$@" +' > "$GITHUB_WORKSPACE/chez-install/bin/cc" + chmod +x /usr/local/bin/cc 2>/dev/null || chmod +x "$GITHUB_WORKSPACE/chez-install/bin/cc" + - name: Show Chez version run: chez --version @@ -132,16 +189,24 @@ jobs: 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 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d0036df..a3eff9c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 diff --git a/host/chez/build-joltc.ss b/host/chez/build-joltc.ss index b2ab8bb..6151e22 100644 --- a/host/chez/build-joltc.ss +++ b/host/chez/build-joltc.ss @@ -245,6 +245,6 @@ ;; 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 "' '" + "cc -O2 " (if bld-nt? "" "-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")) diff --git a/host/chez/build.ss b/host/chez/build.ss index e7f9816..1ca0847 100644 --- a/host/chez/build.ss +++ b/host/chez/build.ss @@ -57,6 +57,7 @@ ;; --- 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")) ;; The Chez executable, for the isolated compile pass (see build-binary step 4). (define bld-chez @@ -94,14 +95,19 @@ ;; 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? + "-llz4 -lz -lws2_32 -lrpcrt4 -lole32 -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 +417,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)) @@ -558,7 +572,7 @@ (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 against the DEFAULT interaction environment, so the boot's diff --git a/host/chez/run-corpus.ss b/host/chez/run-corpus.ss index d581f0b..4dbad37 100644 --- a/host/chez/run-corpus.ss +++ b/host/chez/run-corpus.ss @@ -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" diff --git a/host/chez/seed/prelude.ss b/host/chez/seed/prelude.ss index 6cee1c2..144312c 100644 --- a/host/chez/seed/prelude.ss +++ b/host/chez/seed/prelude.ss @@ -361,7 +361,7 @@ (guard (e (#t #f)) (def-var! "clojure.core" "char-name-string" (letrec ((char-name-string (lambda (c) (let fnrec4381 ((c c)) (jolt-get (var-deref "clojure.core" "char-name-strings") c))))) char-name-string))) (guard (e (#t #f)) - (def-var! "clojure.core" "rand-nth" (letrec ((rand-nth (lambda (coll) (let fnrec4382 ((coll coll)) (let* ((v (jolt-invoke (var-deref "clojure.core" "vec") coll))) (jolt-nth v (jolt-invoke (var-deref "clojure.core" "rand-int") (jolt-count v)))))))) rand-nth))) + (def-var! "clojure.core" "rand-nth" (letrec ((rand-nth (lambda (coll) (let fnrec4382 ((coll coll)) (jolt-nth coll (jolt-invoke (var-deref "clojure.core" "rand-int") (jolt-count coll))))))) rand-nth))) (guard (e (#t #f)) (def-var! "clojure.core" "random-sample" (letrec ((random-sample (case-lambda ((prob) (let fnrec4383 ((prob prob)) (jolt-invoke jolt-filter (lambda (_) (let fnrec4384 ((_ _)) (jolt-n< (jolt-invoke (var-deref "clojure.core" "rand")) prob)))))) ((prob coll) (let fnrec4385 ((prob prob) (coll coll)) (jolt-filter (lambda (_) (let fnrec4386 ((_ _)) (jolt-n< (jolt-invoke (var-deref "clojure.core" "rand")) prob))) coll)))))) random-sample))) (guard (e (#t #f)) diff --git a/host/chez/stub/launcher.c b/host/chez/stub/launcher.c index 0cc7cee..29a270e 100644 --- a/host/chez/stub/launcher.c +++ b/host/chez/stub/launcher.c @@ -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 +static int self_path(char *buf, uint32_t size) { + DWORD n = GetModuleFileNameA(NULL, buf, size); + return (n == 0 || n >= size) ? -1 : 0; +} #else #include static int self_path(char *buf, uint32_t size) { diff --git a/jolt-core/clojure/core/21-coll.clj b/jolt-core/clojure/core/21-coll.clj index 5bf0caa..3a12c8e 100644 --- a/jolt-core/clojure/core/21-coll.clj +++ b/jolt-core/clojure/core/21-coll.clj @@ -66,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)))) diff --git a/test/chez/README.md b/test/chez/README.md index b0418ef..8f8b4d5 100644 --- a/test/chez/README.md +++ b/test/chez/README.md @@ -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, diff --git a/test/chez/corpus.edn b/test/chez/corpus.edn index b32f021..c4117b4 100644 --- a/test/chez/corpus.edn +++ b/test/chez/corpus.edn @@ -3583,4 +3583,5 @@ {:suite "contracts / misc" :label "vector invocation has nth semantics; run! honors reduced; eval self-evaluates values" :expected "[:ioob 1 true]" :actual "[(try ([1 2] 5) (catch IndexOutOfBoundsException _ :ioob)) (let [c (atom 0)] (run! (fn [x] (swap! c inc) (reduced x)) [1 2 3]) (deref c)) (fn? (eval +))]" :portability :common} {:suite "contracts / misc" :label "intern demands an existing ns; counted? excludes strings; seqable? includes arrays" :expected "[:t false true]" :actual "[(try (intern (quote no-such-ns-xyz) (quote v) 1) (catch Throwable _ :t)) (counted? \"s\") (seqable? (object-array 2))]" :portability :jvm} {:suite "contracts / misc" :label "nth of nil honors notFound; a nil index throws; the starred specials are special" :expected "[:default :npe [true true true true true]]" :actual "[(nth nil 5 :default) (try (nth [1] nil) (catch NullPointerException _ :npe)) [(special-symbol? (quote case*)) (special-symbol? (quote deftype*)) (special-symbol? (quote letfn*)) (special-symbol? (quote reify*)) (special-symbol? (quote &))]]" :portability :common} + {:suite "contracts / misc" :label "rand-nth of nil is nil; of a set throws" :expected "[nil :t]" :actual "[(rand-nth nil) (try (rand-nth #{1 2}) (catch Throwable _ :t))]" :portability :common} ] diff --git a/test/chez/cts-known-failures.txt b/test/chez/cts-known-failures.txt index de85d24..52c66f3 100644 --- a/test/chez/cts-known-failures.txt +++ b/test/chez/cts-known-failures.txt @@ -23,7 +23,6 @@ clojure.core-test.plus 11 0 clojure.core-test.plus-squote 11 0 clojure.core-test.pos-int-qmark 1 0 clojure.core-test.quot 25 0 -clojure.core-test.rand-nth 0 1 clojure.core-test.realized-qmark 1 0 clojure.core-test.rem 16 0 clojure.core-test.remove-watch 0 1 diff --git a/test/conformance/SPEC.md b/test/conformance/SPEC.md index 86856e9..7509686 100644 --- a/test/conformance/SPEC.md +++ b/test/conformance/SPEC.md @@ -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. | — | @@ -54,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 @@ -243,8 +243,7 @@ documented model divergence — nothing in the baseline is an unexplained bug: - **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). -- **eval of JVM shapes** (`rand-nth`/`eval` residue): rows needing JVM-only - evaluation shapes (e.g. evaluating a Java array literal). + 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.