diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..bf604b0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# The EOF reader cases need these files to end WITHOUT a newline, byte for byte +# (no CRLF or final-newline normalisation on checkout). +programs/eof-*.txt -text diff --git a/.gitignore b/.gitignore index b592c49..2ae9d15 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ # per-Lua-build files, .shen-kernel-cache..bin, as of shen-lua#55). .shen-kernel-cache*.bin .shen-kernel-cache*.bin.tmp +# shen-lua on 42.2 also caches the standard library alongside it. +.shen-kernel-cache*.bin.stlib # Generated KLambda artefacts (e.g. a stray shake/build output in programs/). *.kl diff --git a/README.md b/README.md index a28fd58..ea98396 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ go run . --heavy # also Yggdrasil stage-1 byte-identity ``` ~70 cases in `cases/*.json` + `programs/*.shen`: arithmetic, lists, strings, -closures, Prolog, trap-error, and S42 kernel behavior any port should share +closures, Prolog, trap-error, and ShenOSKernel 42.2 behavior any port should share (`integer?`, `=`, intern of `"true"`, hash agreeing with `=`, tuples/vectors, `put`/`get`, freeze captured by a lambda). diff --git a/adapters.json b/adapters.json index c9071cf..833c8a0 100644 --- a/adapters.json +++ b/adapters.json @@ -30,7 +30,13 @@ "Per-OS specifics go in an optional 'os_overrides' block per adapter,", "keyed by sys.platform ('win32'/'darwin'/'linux'); the matching block is", "shallow-merged over the adapter. See _os_overrides_example below. As", - "always, $BIFROST_ overrides the launcher path on any OS." + "always, $BIFROST_ overrides the launcher path on any OS.", + "", + "KERNEL: every production port targets the community ShenOSKernel 42.2", + "(Shen-Language/shen-sources tag shen-42.2, commit 73507e06, the kernel of", + "upstream shen-scheme 0.50), adopted wholesale in place of Tarver's S42.0", + "(pyrex41/bifrost#26). Until each port's kernel-42.2 PR merges to its default", + "branch, git-build installs it with `bifrost install --ref kernel-42.2`." ], "_os_overrides_example": { @@ -64,7 +70,7 @@ "repl": ["{bin}"], "hush_writes_file": true, "status": "production", - "kernel": "42", + "kernel": "42.2", "build": { "cwd": "../shen-cl", "argv": ["make", "build-sbcl"], @@ -76,7 +82,7 @@ "git": "https://github.com/pyrex41/shen-cl", "needs": ["git", "make", "sbcl"] }, - "_install_note": "Default method git-build (so a fork can be built). git remote / ref overridable via --git/--ref or $BIFROST_SHEN_CL_GIT / _REF; default is the pyrex41 fork. `--method brew` uses the 'shen-cl' Homebrew formula instead. The launcher itself can point at any prebuilt fork binary via $BIFROST_SHEN_CL. BOOTSTRAP: a fresh clone has no kernel/ or compiled/ tree, so before `make build-sbcl` you must run `make fetch` (or scripts/assemble-tarver-kernel.sh) and then `make precompile SHEN=` -- precompiling the kernel requires an already-working Shen. `bifrost install shen-cl` runs only the final build step, so on a first-ever install do the fetch+precompile once by hand (see the shen-cl README).", + "_install_note": "Default method git-build (so a fork can be built). git remote / ref overridable via --git/--ref or $BIFROST_SHEN_CL_GIT / _REF; default is the pyrex41 fork. `--method brew` uses a 'shen-cl' Homebrew formula instead (not the corpus-validated fork; check its kernel with `bifrost impls --versions`). The launcher itself can point at any prebuilt fork binary via $BIFROST_SHEN_CL. BOOTSTRAP: a fresh clone has no kernel/ or compiled/ tree, so before `make build-sbcl` you must run `make fetch` (scripts/fetch-kernel.sh: downloads ShenOSKernel-42.2.tar.gz from Shen-Language/shen-sources tag shen-42.2 and verifies its SHA-256) and then `make precompile SHEN=` -- precompiling the kernel requires an already-working Shen. `bifrost install shen-cl` runs only the final build step, so on a first-ever install do the fetch+precompile once by hand (see the shen-cl README).", "_repl_note": "Bare {bin} (no args) enters the REPL. Do NOT pass -q here: -q is only valid after the eval/script subcommand; as a bare first arg shen-cl rejects it ('Invalid argument: -q') and exits before the REPL starts — which made the stdin-eof case pass spuriously without ever testing EOF." }, @@ -99,14 +105,14 @@ "repl": ["{bin}"], "hush_writes_file": true, "status": "production", - "kernel": "42", + "kernel": "42.2", "install": { "method": "git-build", "git": "https://github.com/pyrex41/shen-go", "ref": "v1.5.0", "needs": ["git", "go"] }, - "_install_note": "git-build clones the pyrex41 fork: it carries the standard launcher CLI (eval/script/--version, runtime kernel 42 / S42.0) that Bifrost drives. Upstream tiancaiamao/shen-go lacks those subcommands, so it cannot be driven by Bifrost. ref pins the shen-go release Bifrost is validated against (v1.5.0: audited natives, (fail) = shen.fail!, clean stdout); an existing clone is fetched before the checkout, and `bifrost install shen-go --ref master` (or BIFROST_SHEN_GO_REF=master) tracks master instead." + "_install_note": "git-build clones the pyrex41 fork: it carries the standard launcher CLI (eval/script/--version) that Bifrost drives. Upstream tiancaiamao/shen-go lacks those subcommands, so it cannot be driven by Bifrost. ref pins a shen-go release; an existing clone is fetched before the checkout, and `bifrost install shen-go --ref master` (or BIFROST_SHEN_GO_REF=master) tracks master instead. TODO(kernel 42.2): v1.5.0 still vendors Tarver S42.0 (runtime kernel 42). No shen-go release on ShenOSKernel 42.2 exists yet (pyrex41/shen-go#62, branch kernel-42.2); until one is tagged, install the 42.2 kernel with `--ref kernel-42.2` (or master once #62 merges), then move this pin to the new tag." }, "shen-joy": { @@ -121,8 +127,8 @@ "repl": ["{bin}", "unsupported", "repl"], "hush_writes_file": false, "status": "bounded-image-target", - "kernel": "bounded-s42-subset", - "_note": "Device target only. Ordinary Shen source/eval/REPL cases exit 3 (capability SKIP); Bifrost's --shake path uses Yggdrasil's joy builder to create and run a validated image." + "kernel": "bounded-42.2-subset", + "_note": "Device target only: a bounded first-order subset defined against ShenOSKernel 42.2 (shen-joy docs/shen-to-joy.md; differential reference shen-scheme 0.50). Ordinary Shen source/eval/REPL cases exit 3 (capability SKIP). Bifrost's --shake path uses Yggdrasil's joy builder to create and run a validated image: the one corpus program inside the subset is programs/joy-sum.shen (case bounded-tail-sum-file, the same program as Yggdrasil's tests/joy-sum.shen), which must print 32004000. tests/fib.shen and the other script programs fall outside the subset and are capability SKIPs (exit 3) on joy, so the --heavy fib.shen parity case never includes it." }, "shen-erl": { @@ -141,13 +147,13 @@ "repl": ["{bin}"], "hush_writes_file": true, "status": "production", - "kernel": "42", + "kernel": "42.2", "install": { "method": "git-build", "git": "https://github.com/pyrex41/shen-erl", - "needs": ["git", "make", "erl", "erlc", "cc", "curl", "unzip", "tar", "shasum"] + "needs": ["git", "make", "erl", "erlc", "cc", "curl", "tar", "shasum"] }, - "_install_note": "git-build clones the maintained S42.0 fork and compiles the Erlang runtime plus KLambda kernel. The launcher infers its ebin directory from its own location; $SHEN_ERL_ROOTDIR remains an optional override." + "_install_note": "git-build clones the maintained pyrex41 fork and compiles the Erlang runtime plus KLambda kernel. scripts/fetch-kernel.sh downloads ShenOSKernel-42.2.tar.gz (Shen-Language/shen-sources tag shen-42.2, SHA-256 pinned) with curl and unpacks it with tar; unzip is no longer needed. Until pyrex41/shen-erl#5 merges, the default branch still builds Tarver S42.0: pass `--ref kernel-42.2` for 42.2. The launcher infers its ebin directory from its own location; $SHEN_ERL_ROOTDIR remains an optional override. Bignum integers (exact above 2^63)." }, "shen-rust": { @@ -166,12 +172,13 @@ "repl": ["{bin}"], "hush_writes_file": false, "status": "production", - "kernel": "42", + "kernel": "42.2", "install": { "method": "git-build", "git": "https://github.com/pyrex41/shen-rust", "needs": ["git", "cargo"] - } + }, + "_note": "ShenOSKernel 42.2 booted through init.kl/shen.initialise; bundles extension-namespaces (the namespace-externals-expression case runs here). 64-bit integers that overflow to doubles (exact to 2^63). Release build needs several GB of disk under target/." }, "shen-lua": { @@ -186,7 +193,7 @@ "repl": ["{bin}", "-q"], "hush_writes_file": false, "status": "production", - "kernel": "42", + "kernel": "42.2", "build": { "cwd": "../shen-lua", "argv": ["bin/shen", "-e", "(version)"], @@ -201,7 +208,7 @@ "lua_version": "5.1", "needs": ["git", "luajit"] }, - "_install_note": "git-build checks out v0.11.0 and runs bin/shen from that checkout (KLambda is compiled on first boot and cached -- the build step just warms that cache). Needs luajit at runtime. `--method luarocks` installs the published shen 0.11.0-1 (kernel 42) with Lua 5.1, rather than an older kernel 41.2 rock. Bifrost finds its launcher on PATH when it is not under /usr/local/bin/shen.", + "_install_note": "git-build checks out the pinned ref and runs bin/shen from that checkout (KLambda is compiled on first boot and cached -- the build step just warms that cache). Needs luajit at runtime. `--method luarocks` installs the pinned rock with Lua 5.1. Bifrost finds its launcher on PATH when it is not under /usr/local/bin/shen, but only if it reports this adapter's kernel. TODO(kernel 42.2): v0.11.0 / rock 0.11.0-1 (and v0.11.1) still ship Tarver S42.0 (kernel 42), so with kernel 42.2 here an install of the pins is reported as not current. No shen-lua release on ShenOSKernel 42.2 exists yet (pyrex41/shen-lua#81, branch kernel-42.2); until one is tagged and published, use `bifrost install shen-lua --ref kernel-42.2` (git-build), then move ref/version to the new release.", "_version_note": "shen-lua has no --version flag; Bifrost falls back to eval of (version)." }, @@ -217,7 +224,7 @@ "repl": ["node", "{bin}"], "hush_writes_file": true, "status": "production", - "kernel": "42", + "kernel": "42.2", "build": { "cwd": "../ShenScript", "steps": [ @@ -242,7 +249,12 @@ ], "build": { "cwd": "../shen-scheme", - "argv": ["make", "all"], + "steps": [ + ["make", "fetch-kernel"], + ["make", "fetch-prebuilt"], + ["make", "precompile-with-prebuilt"], + ["make", "all"] + ], "out": "_build/bin/shen-scheme" }, "eval": ["{bin}", "eval", "-e", "{expr}"], @@ -251,15 +263,15 @@ "repl": ["{bin}"], "hush_writes_file": true, "status": "production", - "kernel": "42", + "kernel": "42.2", "install": { - "method": "brew", + "method": "git-build", "package": "shen-scheme", "git": "https://github.com/pyrex41/shen-scheme", - "needs": ["brew"] + "needs": ["git", "make", "cc", "curl", "tar"] }, - "_install_note": "Homebrew formula 'shen-scheme' (also GitHub release binaries). git-build alternative: --method git-build (needs make + a C compiler; auto-fetches Chez 10.3).", - "_note": "Chez Scheme port by tizoc (Bruno Deferrari). Binary finds its boot file (lib/shen-scheme/shen.boot) relative to the executable, so cwd-independent. -q maps to --quiet -> (set *hush* true), which silences stdout (so it is NOT passed for normal eval/script cases); file-stream pr still writes under -q. Converged with the 41.2 reference ports on all bifrost cases (kernel 41.2; clean stdin-EOF exit; hush gates stdout only)." + "_install_note": "Default git-build of the pyrex41 fork: tizoc/shen-scheme v0.50 (ShenOSKernel 42.2) merged, plus the two fork fixes the corpus asserts (stdin-EOF exit, *hush* gates stdout only). The build steps are the upstream CI recipe: make fetch-kernel (ShenOSKernel-42.2.tar.gz, SHA-256 1d022730...78d8), make fetch-prebuilt (tizoc v0.26 binary, only to precompile the kernel), make precompile-with-prebuilt, make all (downloads and builds Chez 10.3.0; several minutes). Until pyrex41/shen-scheme#9 merges, the fork's default branch is still 0.45 / Tarver S42.0: pass `--ref kernel-42.2`. UPSTREAM 0.50 (tizoc release binaries, the 42.2 reference) runs the same kernel; point $BIFROST_SHEN_SCHEME at its bin/shen-scheme to compare it. It differs from the fork only in hush-file-write (-q silences file writes) and stdin-eof-clean-exit (REPL loops at EOF), and FAILS those two cases by design. `--method brew` is NOT a 42.2 path: homebrew-core has no shen-scheme formula and the Shen-Language/homebrew-shen tap ships shen-scheme 0.17.", + "_note": "Chez Scheme port by tizoc (Bruno Deferrari), 0.50 on ShenOSKernel 42.2. There is no shen.boot any more: the launcher loads petite.boot + scheme.boot and lib/shen-scheme/shen-scheme/runtime.so from the lib/shen-scheme directory next to bin/ (override with $SHEN_SCHEME_HOME), so it is cwd-independent. -q maps to --quiet -> (set *hush* true), which silences stdout (so it is NOT passed for normal eval/script cases); in the fork, file-stream pr still writes under -q. Bundles extension-namespaces (the namespace-externals-expression case runs here). Bignum integers." }, "shen-julia": { @@ -273,7 +285,7 @@ "repl": ["{bin}"], "hush_writes_file": true, "status": "production", - "kernel": "42", + "kernel": "42.2", "build": { "cwd": "../shen-julia", "argv": ["julia", "--project=.", "-e", "using Pkg; Pkg.instantiate()"], @@ -284,8 +296,8 @@ "git": "https://github.com/pyrex41/shen-julia", "needs": ["git", "julia"] }, - "_install_note": "git-build: clone + Pkg.instantiate (deps). No sysimage by default (~12s boot); build one with `julia --project=. bin/build_sysimage.jl` and point $SHEN_JULIA_SYSIMAGE at it to cut startup.", - "_note": "Julia port (shen-julia), 41.2 kernel. The bin/shen wrapper drives the kernel's standard launcher (shen.x.launcher.main from extension-launcher.kl) -- same CLI surface as shen-go/shen-rust/ShenScript. Boots ~12s per invocation (no sysimage yet; set $SHEN_JULIA_SYSIMAGE to a prebuilt ShenJulia.sys to cut that); well within the 60s timeout. Do NOT pass -q for normal eval/script cases: -q sets *hush* which silences stdout (the value would not print). A host-side pr override makes *hush* gate ONLY stdout, never an explicit file stream, so it converges with the hardened hush-file-write case (writes). REPL exits cleanly on stdin EOF via a host-side readline loop." + "_install_note": "git-build: clone + Pkg.instantiate (deps). The first run precompiles the package (minutes); afterwards start-up is ~5 s without a sysimage. Build one with `julia --project=. bin/build_sysimage.jl` and point $SHEN_JULIA_SYSIMAGE at it to cut that further. Until pyrex41/shen-julia#11 merges, pass `--ref kernel-42.2` for the 42.2 kernel.", + "_note": "Julia port (shen-julia), ShenOSKernel 42.2 booted through init.kl/shen.initialise. --version prints `42.2 (port (\"Julia\" \"shen-julia\") implementation (...))`. The bin/shen wrapper drives the kernel's standard launcher (shen.x.launcher.main from extension-launcher.kl) -- same CLI surface as shen-go/shen-rust/ShenScript. Boots ~5 s per invocation without a sysimage (set $SHEN_JULIA_SYSIMAGE to a prebuilt ShenJulia.sys to cut that); well within the 60s timeout. Int64 integers wrap at 2^63. Known port gaps (also on its Tarver S42.0 main branch): the native integer? is false for integral floats such as 4.0 and hash of 1 and 1.0 differ, so integer-float-integral and hash-agrees-with-equality FAIL. Do NOT pass -q for normal eval/script cases: -q sets *hush* which silences stdout (the value would not print). A host-side pr override makes *hush* gate ONLY stdout, never an explicit file stream, so it converges with the hardened hush-file-write case (writes). REPL exits cleanly on stdin EOF via a host-side readline loop." }, "shen-swift": { "env": "BIFROST_SHEN_SWIFT", @@ -298,7 +310,7 @@ "repl": ["{bin}"], "hush_writes_file": true, "status": "production", - "kernel": "42", + "kernel": "42.2", "build": { "cwd": "../shen-swift", "argv": ["swift", "build", "-c", "release"], @@ -309,8 +321,8 @@ "git": "https://github.com/pyrex41/shen-swift", "needs": ["swift"] }, - "_install_note": "git-build: clone + `swift build -c release` (Swift 6+ toolchain; macOS or Linux with the open-source toolchain). Self-contained -- the 41.2 .kl kernel ships inside the SwiftPM resource bundle, no separate kernel checkout needed.", - "_note": "Swift port (shen-swift), 41.2 kernel, built from scratch as a tree-walking KLambda interpreter targeting iOS as well as macOS (no JIT/codegen -- runs inside the iOS sandbox). Drives the kernel's standard launcher (shen.x.launcher.main from extension-launcher.kl) -- same CLI surface as shen-go/shen-rust/shen-julia/ShenScript. Boots the kernel (~1-2.7s release) on every invocation; passes the 41.2 kernel test suite 35/35 groups. A host-side pr override makes *hush* gate ONLY stdout, never an explicit file stream, so -q silences the printed value but file writes still happen (hush_writes_file=true) -- this also lets shen-swift host yggdrasil shakes, which run under -q. -q is NOT passed for normal eval/script cases (it would suppress the value). REPL exits cleanly on stdin EOF (getchar -> exit)." + "_install_note": "git-build: clone + `swift build -c release` (Swift 6+ toolchain; macOS or Linux with the open-source toolchain). Self-contained -- the ShenOSKernel 42.2 .kl kernel ships inside the SwiftPM resource bundle, no separate kernel checkout needed. The 42.2 port is on branch kernel-42.2 (not yet on the default branch): pass `--ref kernel-42.2` once it is pushed.", + "_note": "Swift port (shen-swift) on ShenOSKernel 42.2, a tree-walking KLambda interpreter built from scratch that targets iOS as well as macOS (no JIT/codegen -- runs inside the iOS sandbox). Boot loads the 16 kernel modules and calls (shen.initialise), about 0.5 s per invocation in a release build; passes the 42.2 kernel test suite 143/143. Its launcher is native Swift that mirrors extension-launcher.kl (same CLI surface as shen-go/shen-rust/shen-julia/ShenScript; --stdlib loads stlib.kl). A host-side pr override makes *hush* gate ONLY stdout, never an explicit file stream: -q silences eval -e output but file writes still happen (hush_writes_file=true), which also lets shen-swift host yggdrasil shakes under -q. -q is NOT passed for normal eval/script cases. 64-bit integers that overflow to doubles. REPL exits cleanly on stdin EOF (getchar -> exit)." }, "shen-truffle": { "env": "BIFROST_SHEN_TRUFFLE", @@ -324,7 +336,7 @@ "repl": ["{bin}"], "hush_writes_file": true, "status": "production", - "kernel": "42", + "kernel": "42.2", "build": { "cwd": "../shen-truffle", "argv": ["mvn", "package", "-DskipTests"], @@ -335,7 +347,8 @@ "git": "https://github.com/pyrex41/shen-truffle", "needs": ["git", "mvn", "java"] }, - "_install_note": "git-build clones shen-truffle and runs `mvn package -DskipTests`; the relocatable launcher is target/shen-truffle/bin/shen-truffle. Set $BIFROST_SHEN_TRUFFLE to override the path." + "_install_note": "git-build clones shen-truffle and runs `mvn package -DskipTests`; the relocatable launcher is target/shen-truffle/bin/shen-truffle. Set $BIFROST_SHEN_TRUFFLE to override the path. The 42.2 port is pyrex41/shen-truffle#6 (branch kernel-42.2): pass `--ref kernel-42.2` until it merges.", + "_note": "Truffle/GraalVM port on ShenOSKernel 42.2. The launcher runs $JAVA_HOME/bin/java (else java on PATH) with --sun-misc-unsafe-memory-access=allow, so it needs JDK 23+ (GraalVM CE 25 in its flake): with an older java the JVM refuses to start. When JAVA_TOOL_OPTIONS / _JAVA_OPTIONS / JDK_JAVA_OPTIONS is set (agent proxies, CI images) the JVM prints a 'Picked up ...' line on stderr; Bifrost's normaliser strips that line, so cases still compare Shen output, but unset the variable when running the launcher by hand. 64-bit integers that overflow to doubles." }, "shen-c": { @@ -349,7 +362,7 @@ "repl": ["{bin}"], "hush_writes_file": true, "status": "experimental", - "kernel": "42", + "kernel": "42.2", "build": { "cwd": "../shen-c", "argv": ["make"], @@ -362,6 +375,6 @@ "needs": ["git", "make", "cc", "pkg-config"] }, "_install_note": "experimental C port. git-build clones pyrex41/shen-c and runs `make` (C17 + Boehm GC via Nix pkg-config bdw-gc, not Homebrew libgc). The compiled launcher is bin/shen-c; it self-locates the kernel via home_from_executable() looking for shen/src/kl/sys.kl, so Bifrost must not set $SHEN_C_HOME. Point $BIFROST_SHEN_C at bin/shen-c if needed. Install is blocked unless `--force` because status is experimental.", - "_note": "S42 C tree-walker (kernel (version) is 42 unquoted on eval; --version banner is Shen-C 0.2.3, not 41.2). CLI: eval -e / eval -q -l / script FILE / --version / bare REPL. Eval print is unquoted (shen.a). Host pr override: *hush* gates stdout only. Boot loads types.kl declare and declarations.kl (shen.build-lambda-table (external shen)); do not skip them. C17 + Nix pkg-config bdw-gc, no Homebrew, no C++ rewrite. Prefer ../shen-c/bin/shen-c (not the rlwrap wrapper). Do not claim corpus green without a live runme.shen log. Go is AOT+interpreter on S41.2; C is a tree-walker on S42 — do not claim C should match Go wall times without a profile." + "_note": "C tree-walker on ShenOSKernel 42.2 ((version) is 42.2 unquoted on eval; --version banner stays the port-local Shen-C 0.2.3, so version-banner-contains FAILS and `bifrost impls --versions` falls back to eval of (version)). Known port gaps, all also present on its Tarver S42.0 main branch: script mode echoes each toplevel value like load instead of the launcher's quiet-load (bounded-tail-sum-file, source-form-before-macros FAIL), and (+ 0.1 0.2) prints 0.3 (float-add-imprecise FAIL). Int64 integers wrap at 2^63. CLI: eval -e / eval -q -l / script FILE / --version / bare REPL. Eval print is unquoted (shen.a). Host pr override: *hush* gates stdout only. Boot loads the 16 defun-only 42.2 modules (incl. dict.kl and init.kl), installs the AOT natives and calls (shen.initialise); the Tarver S42 declarations.kl/types.kl top-level boot forms are gone. C17 + Nix pkg-config bdw-gc, no Homebrew, no C++ rewrite. Prefer ../shen-c/bin/shen-c (not the rlwrap wrapper). Do not claim corpus green without a live runme.shen log. Go is AOT+interpreter and C is a tree-walker, both on 42.2 — do not claim C should match Go wall times without a profile." } } diff --git a/cases/cli.json b/cases/cli.json index 059f3e4..4e0d92c 100644 --- a/cases/cli.json +++ b/cases/cli.json @@ -10,25 +10,25 @@ "doc": "CLI parity: 'eval -e EXPR' prints the value of EXPR to stdout" }, { - "name": "version-expr-42", + "name": "version-expr-42.2", "mode": "eval", "expr": "(version)", "expect": "output", - "golden": "42", - "doc": "CLI parity: (version) returns runtime kernel version 42 uniformly (works on every impl, incl. shen-lua which has no --version flag)" + "golden": "42.2", + "doc": "CLI parity: (version) returns the runtime kernel version, ShenOSKernel 42.2 (set in init.kl), uniformly on every port (incl. shen-lua, which has no --version flag). Eval print is unquoted, so shen-c's 42.2 matches. Was version-expr-42 (Tarver S42.0) until every port moved to the community 42.2 kernel (pyrex41/bifrost#26)." }, { "name": "version-banner-contains", "mode": "version", "expect": "version", - "contains": "42", - "doc": "CLI parity: the --version banner contains runtime kernel version 42. shen-lua has no --version flag, so Bifrost falls back to eval of (version). The experimental shen-c banner is port-local; this case uses kernels/adapters that emit 42." + "contains": "42.2", + "doc": "CLI parity: the --version banner contains the runtime kernel version 42.2. The standard launcher banner (extension-launcher.kl) leads with it, e.g. `42.2 (port (\"Julia\" \"shen-julia\") implementation (...))`. shen-lua has no --version flag, so Bifrost falls back to eval of (version). KNOWN PORT GAP: experimental shen-c prints a port-local `Shen-C 0.2.3` banner and FAILS here (it already did on Tarver S42.0); `bifrost impls --versions` falls back to eval of (version) for it." }, { "name": "stdin-eof-clean-exit", "mode": "repl-eof", "expect": "clean-exit", - "doc": "DELIBERATE IMPROVEMENT OVER CANONICAL (not a spec conformance): starting the REPL and closing stdin (EOF) exits cleanly on every maintained impl. Canonical Shen LOOPS FOREVER here — the kernel shen.loop catches the reader's EOF error, prints it, and recurses (e.g. official shen-cl emits ~26MB of 'NIL is not of type CHARACTER' in 8s). All ports here detect EOF at the input layer and exit 0; this case guards that they keep doing so. (Earlier this passed spuriously for shen-cl because its repl adapter used a bare -q, which shen-cl rejects as an invalid arg and exits before the REPL even starts — now fixed to bare {bin}.)" + "doc": "DELIBERATE IMPROVEMENT OVER CANONICAL (not a spec conformance): starting the REPL and closing stdin (EOF) exits cleanly on every maintained impl. Canonical Shen LOOPS FOREVER here — the kernel shen.loop catches the reader's EOF error, prints it, and recurses (e.g. official shen-cl emits ~26MB of 'NIL is not of type CHARACTER' in 8s). All ports here detect EOF at the input layer and exit 0; this case guards that they keep doing so. (Earlier this passed spuriously for shen-cl because its repl adapter used a bare -q, which shen-cl rejects as an invalid arg and exits before the REPL even starts — now fixed to bare {bin}.) UPSTREAM shen-scheme 0.50 (tizoc release, the 42.2 reference) keeps the canonical loop and FAILS here by design (~5 MB/s of EOF errors until the timeout; Bifrost caps captured output at 8 MB). The pyrex41 shen-scheme fork reapplies its EOF exit on top of 0.50 (pyrex41/shen-scheme#9, `make test-repl-eof`)." } ] } diff --git a/cases/divergences.json b/cases/divergences.json index 7091f73..694324d 100644 --- a/cases/divergences.json +++ b/cases/divergences.json @@ -5,7 +5,7 @@ "name": "hush-file-write", "mode": "special-hush", "expect": "agreement", - "doc": "RESOLVED (was a known divergence): under -q (*hush* = true), `pr` to a FILE stream now WRITES on every impl. *hush* only gates stdout, never stderr or an explicit file stream. Driven as `{bin} eval -q -l PROG`. S42 shen-c/shen-forth need host `pr` overrides (stock writer.kl returns without writing whenever *hush* for every stream) and eval -q/-l. Previously shen-lua/shen-rust SILENCED the write (zero-byte file) under -q while shen-cl/shen-go/ShenScript wrote. Hard agreement: every available impl must land in the same bucket (all 'written'); any impl that silences the write is a real FAIL." + "doc": "RESOLVED (was a known divergence): under -q (*hush* = true), `pr` to a FILE stream now WRITES on every impl. *hush* only gates stdout, never stderr or an explicit file stream. Driven as `{bin} eval -q -l PROG`. S42 shen-c/shen-forth need host `pr` overrides (stock writer.kl returns without writing whenever *hush* for every stream) and eval -q/-l. Previously shen-lua/shen-rust SILENCED the write (zero-byte file) under -q while shen-cl/shen-go/ShenScript wrote. Hard agreement: every available impl must land in the same bucket (all 'written'); any impl that silences the write is a real FAIL. UPSTREAM shen-scheme 0.50 (tizoc release) uses stock writer.kl, so `-q` SILENCES the file write there (zero bytes) and it FAILS this case by design; the pyrex41 fork reapplies the stdout-only *hush* override on top of 0.50 (pyrex41/shen-scheme#9)." }, { "name": "load-toplevel-echo", @@ -13,8 +13,49 @@ "expr": "(load \"programs/load-echo-probe.shen\")", "cwd": ".", "expect": "agreement", - "known_divergence": "s42-load-echo-format", - "doc": "RESOLVED (was a known divergence): `(load FILE)` now echoes each toplevel form's evaluated value on every impl, cache state notwithstanding. The canonical kernel load (klambda/load.kl -> shen.eval-and-print) PRINTS each toplevel result before returning `loaded` (the echo from `shen eval -l FILE` / `(load FILE)` at the REPL; NOT `script` mode, which is quiet everywhere -- so this case uses eval mode). shen-rust and shen-go always emitted it; shen-lua emitted it on a COLD fasl cache but DROPPED it on a WARM fasl-cache HIT (boot.lua returned `loaded` straight out of fasl_replay without re-running the kernel load), making `(load file)` stdout cache-state dependent -- a cross-port divergence AND an intra-port nondeterminism. Fixed in shen-lua by recording the per-form echo bytes as fasl \"e\" records on the miss and re-pr'ing them on the hit (pyrex41/shen-lua#40, PR #41). The cosmetic `run time:` banner stays dropped on a warm hit by design; the normaliser here strips the whole banner (text line + the blank line its leading newline produces) so that omission is not itself a divergence. Now asserted as a HARD agreement: every available impl must echo `\"PROBE\"` / `42` / `(fn p)` (the last stripped by the normaliser) then `loaded`; any impl that drops the value echo is a real FAIL." + "doc": "RESOLVED (was a known divergence): `(load FILE)` now echoes each toplevel form's evaluated value on every impl, cache state notwithstanding. The canonical kernel load (klambda/load.kl -> shen.eval-and-print) PRINTS each toplevel result before returning `loaded` (the echo from `shen eval -l FILE` / `(load FILE)` at the REPL; NOT `script` mode, which is quiet everywhere -- so this case uses eval mode). shen-rust and shen-go always emitted it; shen-lua emitted it on a COLD fasl cache but DROPPED it on a WARM fasl-cache HIT (boot.lua returned `loaded` straight out of fasl_replay without re-running the kernel load), making `(load file)` stdout cache-state dependent -- a cross-port divergence AND an intra-port nondeterminism. Fixed in shen-lua by recording the per-form echo bytes as fasl \"e\" records on the miss and re-pr'ing them on the hit (pyrex41/shen-lua#40, PR #41). The cosmetic `run time:` banner stays dropped on a warm hit by design; the normaliser strips the whole banner (text line + the blank line its leading newline produces; that blank-line drop was documented but missing until the 42.2 move, which left this case tagged as a known divergence) so that omission is not itself a divergence. Now asserted as a HARD agreement: every available impl must echo `\"PROBE\"` / `42` / `(fn p)` (the last stripped by the normaliser) then `loaded`; any impl that drops the value echo is a real FAIL." + }, + { + "name": "bigint-2pow53-plus-1", + "mode": "eval", + "expr": "(+ 9007199254740993 0)", + "expect": "agreement", + "known_divergence": "platform-integer-precision", + "table": { + "shen-cl": "9007199254740993", + "shen-erl": "9007199254740993", + "shen-scheme": "9007199254740993", + "shen-julia": "9007199254740993", + "shen-c": "9007199254740993", + "shen-rust": "9007199254740993", + "shen-swift": "9007199254740993", + "shen-truffle": "9007199254740993", + "shen-go": "9007199254740992", + "ShenScript": "9007199254740992", + "shen-lua": "9007199254740992" + }, + "doc": "DOCUMENTED DIVERGENCE (Shen leaves number precision to the platform). 2^53+1 is the first integer an IEEE double cannot hold. Ports with exact integers here print 9007199254740993: bignums (shen-cl on SBCL/ECL, shen-erl, shen-scheme) and 64-bit integers (shen-julia, shen-c, shen-rust, shen-swift, shen-truffle). Ports whose only number is a double (shen-go, ShenScript, shen-lua) print 9007199254740992. The table pins each port to its bucket: reported as DIVERGE, but a port that changes bucket FAILS." + }, + { + "name": "bigint-2pow63-ordered", + "mode": "eval", + "expr": "(> (+ 9223372036854775807 1) 9223372036854775807)", + "expect": "agreement", + "known_divergence": "platform-integer-precision", + "table": { + "shen-cl": "true", + "shen-erl": "true", + "shen-scheme": "true", + "shen-julia": "false", + "shen-c": "false", + "shen-rust": "false", + "shen-swift": "false", + "shen-truffle": "false", + "shen-go": "false", + "ShenScript": "false", + "shen-lua": "false" + }, + "doc": "DOCUMENTED DIVERGENCE (Shen leaves number precision to the platform). (+ (2^63-1) 1) is only greater than 2^63-1 with bignums: shen-cl, shen-erl and shen-scheme print true. Every other port prints false, for one of three reasons: 64-bit wraparound to -2^63 (shen-julia, shen-c); overflow promoted to a double equal to 2^63, which compares as not greater (shen-rust, shen-swift, shen-truffle); or doubles throughout (shen-go, ShenScript, shen-lua). The printed sum also differs by host float formatting (9223372036854775808 / -9223372036854775808 / 9223372036854776000 / 9.223372036854776e+18 / 9.223372036854776E18), which is why the case compares a boolean. The table pins each port to its bucket." } ] } diff --git a/cases/errors.json b/cases/errors.json index ce21e40..28d9495 100644 --- a/cases/errors.json +++ b/cases/errors.json @@ -21,8 +21,9 @@ "name": "error-message-head", "mode": "eval", "expr": "(trap-error (head []) (lambda E (error-to-string E)))", - "expect": "agreement", - "doc": "the kernel error MESSAGE for (head []) should agree across impls on the same kernel. S41.2 is 'head expects a non-empty list'; S42 sys.kl includes a trailing newline in the kernel string. Mixing 41.2 and 42 ports may FAIL because of that kernel split. shen-go now uses kernel 42." + "expect": "output", + "doc": "HARD AGREEMENT again (was a kernel-split agreement case): every port runs ShenOSKernel 42.2, whose sys.kl message for (head []) is 'head expects a non-empty list' plus a trailing newline (dropped by the normaliser). Pinned as a golden so a port whose native head raises its own host message FAILS. Earlier, mixing Tarver S42.0 and S41.2 ports split on this message.", + "golden": "head expects a non-empty list" }, { "name": "int-div-zero", diff --git a/cases/kernel-42.json b/cases/kernel-42.json new file mode 100644 index 0000000..3c294fb --- /dev/null +++ b/cases/kernel-42.json @@ -0,0 +1,39 @@ +{ + "group": "kernel-42.2", + "cases": [ + { + "name": "lineread-eof-no-newline", + "mode": "eval", + "cwd": ".", + "expr": "(let S (open \"programs/eof-lineread.txt\" in) L1 (lineread S) L2 (lineread S) (do (close S) [L1 L2]))", + "expect": "output", + "golden": "[[hello [+ 1 2]] [last line no newline]]", + "doc": "42.1 reader fix: lineread returns the final line of a stream that ends at EOF WITHOUT a trailing newline (programs/eof-lineread.txt has none). Driven through a file stream, not REPL stdin: upstream shen-scheme 0.50's REPL loops at stdin EOF, so a stdin probe would test the REPL rather than the reader." + }, + { + "name": "read-eof-no-newline", + "mode": "eval", + "cwd": ".", + "expr": "(let S (open \"programs/eof-read.txt\" in) A (read S) B (read S) (do (close S) [A B]))", + "expect": "output", + "golden": "[[+ 40 2] 42]", + "doc": "42.1 reader fix: read returns a final token that is terminated only by EOF (programs/eof-read.txt is `(+ 40 2) 42` with no trailing newline), so the bare 42 is not lost or left waiting for more input. File stream, not REPL stdin (see lineread-eof-no-newline)." + }, + { + "name": "source-form-before-macros", + "mode": "script", + "program": "source-form-order.shen", + "expect": "output", + "golden": "[(@p handler-then-macro 1) (@p handler-then-macro 2)]", + "doc": "42.1 source-form handlers run BEFORE ordinary macros, and macros still expand what a handler produces. bifrost-probe has both a handler (-> bifrost-probe2) and a macro rule (-> macro-first); the forms come from read-from-string after registration (a script is read whole before it runs, so the handler cannot apply to its own file). A port that does not boot shen.register-source-form prints the BIFROST-SKIP capability marker (capability SKIP)." + }, + { + "name": "namespace-externals-expression", + "mode": "script", + "program": "namespace-externals-expr.shen", + "expect": "output", + "golden": "[42 2]", + "doc": "42.2 namespaces: (externals EXPR) EVALUATES EXPR to a symbol list (here a call, (bifrost-ns-vocab)); 42.1 took only a literal list. bifrost-ns-ext stays global and local becomes bifrost.demo.local. Only ports that boot the experimental extension-namespaces.kl run it (shen-scheme, shen-rust at the time of writing); ports where (shen.x.namespaces.initialise) is unavailable print the BIFROST-SKIP capability marker (capability SKIP)." + } + ] +} diff --git a/docs/cli.md b/docs/cli.md index ef9010d..55174bf 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -23,7 +23,10 @@ bifrost install IMPL … # legacy; prefer env + Nix — docs/install.md 3. `~/.config/bifrost/impl` (`bifrost use X`; on Windows `%APPDATA%\bifrost\impl`) 4. `shen-cl` if present, else the first available port -`bifrost impls --versions` probes each port's `(version)` live. README badges +`bifrost impls --versions` probes each port's kernel live. It reads the +leading token of the standard `--version` banner (`42.2 (port ("Julia" +"shen-julia") …)`), and falls back to an eval of `(version)` when a banner is +port-local (shen-c prints `Shen-C 0.2.3`) or absent (shen-lua). README badges drift; this does not. ## How a launcher is found @@ -32,7 +35,8 @@ For each adapter in [`adapters.json`](../adapters.json): 1. the env-var override (e.g. `$BIFROST_SHEN_GO`) if it names an existing file 2. the first existing path in `default_paths` -3. for shen-lua, a `shen` command on `PATH` that reports `shen-lua` and kernel 42 +3. for shen-lua, a `shen` command on `PATH` that reports `shen-lua` and the + adapter's kernel (`42.2`) Missing ports are **skipped and reported**, never a hard error. @@ -51,8 +55,9 @@ Missing ports are **skipped and reported**, never a hard error. | `shen-truffle` | `BIFROST_SHEN_TRUFFLE` | `../shen-truffle/target/shen-truffle/bin/shen-truffle` | | `shen-c` | `BIFROST_SHEN_C` | `../shen-c/bin/shen-c` (experimental) | -Production ports target ShenOSKernel **42**. `shen-joy` is a bounded S42-derived -subset. `shen-c` is experimental and self-locates its kernel (do not set +Production ports target ShenOSKernel **42.2** (community `shen-sources` tag +`shen-42.2`, the kernel of upstream shen-scheme 0.50). `shen-joy` is a bounded +first-order subset defined against 42.2. `shen-c` is experimental and self-locates its kernel (do not set `$SHEN_C_HOME`). Unfinished `shen-forth`, `shen-inets`, `shen-ocaml`, and `shen-odin` have flakes but are not Bifrost implementations. diff --git a/docs/install.md b/docs/install.md index 0c6b7db..91d6437 100644 --- a/docs/install.md +++ b/docs/install.md @@ -7,14 +7,29 @@ Each adapter declares a fallback in `adapters.json`: | method | ports | what runs | |--------|-------|-----------| -| `brew` | shen-scheme (shen-cl via `--method brew`) | `brew install ` | +| `brew` | shen-cl / shen-scheme via `--method brew` only | `brew install ` | | `luarocks` | shen-lua via `--method luarocks` | `luarocks --lua-version=5.1 install shen 0.11.0-1` | -| `git-build` | the rest | clone (if absent) + the port `build` recipe | +| `git-build` | every port by default | clone (if absent) + the port `build` recipe | `install` prechecks the toolchain, names the missing tool, refuses `experimental` ports unless `--force`, skips an already-resolved launcher, and verifies the launcher afterward. +### Kernel 42.2 transition + +Every production port targets the community **ShenOSKernel 42.2** +(`Shen-Language/shen-sources` tag `shen-42.2`, commit `73507e06`, tarball +SHA-256 `1d022730…678d8`), the kernel upstream shen-scheme 0.50 ships. It was +adopted wholesale in place of Tarver's S42.0 (pyrex41/bifrost#26). Until a +port's `kernel-42.2` PR merges to its default branch, install it with +`bifrost install --ref kernel-42.2`. + +The pinned releases below (shen-go `v1.5.0`, shen-lua `v0.11.0` / rock +`0.11.0-1`) predate 42.2 and still report kernel `42`. No 42.2 releases exist +yet; once they are tagged, move the `ref`/`version` pins in `adapters.json`. +Until then a shen-lua launcher or rock that reports `42` is not accepted as +current (the adapter's `kernel` is `42.2`). + ### Notes - **shen-go / ShenScript** git-build clones the **pyrex41 forks** (standard @@ -23,20 +38,45 @@ verifies the launcher afterward. pinned to the release Bifrost was validated against (`ref` in `adapters.json`, currently `v1.5.0`); an existing `../shen-go` clone is fetched and checked out to that tag. `bifrost install shen-go --ref master` - or `BIFROST_SHEN_GO_REF=master` tracks master instead. + or `BIFROST_SHEN_GO_REF=master` tracks master instead. `v1.5.0` is Tarver + S42.0; use `--ref kernel-42.2` for 42.2 until a new release is tagged. - **shen-lua** runs from the checkout (`bin/shen`); the build step warms the KLambda cache. Needs `luajit`. The installer pins `v0.11.0` and fetches that tag for an existing checkout. The published `0.11.0-1` source rock provides - the same kernel 42 launcher through LuaRocks for Lua 5.1. Bifrost also finds + the same launcher through LuaRocks for Lua 5.1. Both are still kernel 42 + (Tarver S42.0); use `--ref kernel-42.2` until a 42.2 release and rock are + published. Bifrost also finds a matching `shen` launcher on `PATH`. Set `BIFROST_SHEN_LUA` to the rock's launcher when a sibling checkout is present and you want to use the rock. - **shen-cl** — a fresh clone has no `kernel/` or `compiled/`. Bootstrap once: ```bash - make fetch + make fetch # scripts/fetch-kernel.sh: ShenOSKernel-42.2, SHA-256 checked make precompile SHEN= bifrost install shen-cl # make build-sbcl ``` +- **shen-scheme** — the default is a git-build of the **pyrex41 fork**: upstream + tizoc/shen-scheme v0.50 (kernel 42.2) merged, plus the two fork fixes the + corpus asserts (REPL exits on stdin EOF; `*hush*` gates stdout only, so `-q` + still writes file streams). The build runs the upstream CI recipe: + `make fetch-kernel`, `make fetch-prebuilt` (tizoc's v0.26 binary, only used + to precompile the kernel), `make precompile-with-prebuilt`, `make all` + (downloads and builds Chez 10.3.0). 0.50 has no `shen.boot`: the launcher + loads `petite.boot`, `scheme.boot` and `lib/shen-scheme/shen-scheme/runtime.so` + from the `lib/shen-scheme` directory next to `bin/` (`$SHEN_SCHEME_HOME` + overrides it). + Upstream 0.50 release binaries run the same kernel; set + `BIFROST_SHEN_SCHEME=/path/to/shen-scheme-0.50/bin/shen-scheme` to compare. + Upstream FAILS exactly two cases by design: `hush-file-write` (stock + `writer.kl` drops file writes under `-q`) and `stdin-eof-clean-exit` (the + canonical REPL loops at EOF). `--method brew` is not a 42.2 path: + homebrew-core has no `shen-scheme` formula and the + `Shen-Language/homebrew-shen` tap ships 0.17. +- **shen-truffle** — the launcher needs JDK 23+ (it passes + `--sun-misc-unsafe-memory-access=allow`; its flake uses GraalVM CE 25) via + `$JAVA_HOME` or `java` on `PATH`. A set `JAVA_TOOL_OPTIONS` makes the JVM + print `Picked up JAVA_TOOL_OPTIONS: …` on stderr; Bifrost's normaliser drops + that line, but unset the variable when running the launcher by hand. ### Fresh machine (no Nix) @@ -47,7 +87,8 @@ Defaults assume sibling checkouts. Override with a project `adapters.json` or 2. `shen-rust` (`cargo`), `ShenScript` (`node`/`npm`), `shen-lua` (`luajit`). 3. shen-cl: clone, `make fetch`, `make precompile SHEN=`, then `bifrost install shen-cl` (`sbcl`). -4. `shen-scheme --method git-build`, `shen-julia`, `shen-swift`, `shen-truffle`. +4. `shen-scheme` (git-build of the fork; `make` + C compiler + `curl`), + `shen-julia`, `shen-swift`, `shen-truffle`. Forks: point `$BIFROST_SHEN_CL` at a built binary, or `bifrost install shen-cl --git URL --ref BRANCH`. diff --git a/docs/matrix.md b/docs/matrix.md index 66dd8dc..3767955 100644 --- a/docs/matrix.md +++ b/docs/matrix.md @@ -11,23 +11,38 @@ go run . --shake --only recursion-fib-file ``` `DIVERGE` rows (tagged `known_divergence`) are reported and do **not** fail. -Only `FAIL` sets a non-zero exit. There are currently **no** open divergences; -every tracked difference is now a hard agreement. +Only `FAIL` sets a non-zero exit. A divergence case may carry a `table` that +pins each port to its documented bucket; a port that leaves its bucket is a +real `FAIL`. The open divergences are platform number precision +(`bigint-2pow53-plus-1`, `bigint-2pow63-ordered`), which Shen leaves to the +host; every other tracked difference is a hard agreement. + +## Kernel + +Every production port runs the community **ShenOSKernel 42.2** +(`Shen-Language/shen-sources` tag `shen-42.2`, commit `73507e06`; the kernel of +upstream shen-scheme 0.50), adopted wholesale in place of Tarver's S42.0 +(pyrex41/bifrost#26). Cases that used to split by kernel (`error-message-head`) +are hard agreements again. ## Corpus `cases/*.json` driven by `programs/*.shen`: - Arithmetic (including floats), lists, strings, closures, `fix`, recursion, - a 100 000-deep tail-call countdown, a small Prolog query, `trap-error`. -- Kernel portables (any S42 port): `integer?`/`symbol?`/`boolean?`/`empty?`/`variable?`/`element?`, + a 100 000-deep tail-call countdown, a small Prolog query, `trap-error`. +- Kernel portables: `integer?`/`symbol?`/`boolean?`/`empty?`/`variable?`/`element?`, `=` of `1`/`1.0` and `true`/`(intern "true")`, hash agreeing with `=`, `@p`/`vector`, `put`/`get` on `*property-vector*`, freeze captured by a - lambda, trap-error whose *operand* raises. -- CLI: `eval -e` prints the value; `(version)` / `--version` is kernel **42**; - stdin-EOF exits cleanly. + lambda, trap-error whose *operand* raises, the `(head [])` error message. +- CLI: `eval -e` prints the value; `(version)` / `--version` is kernel + **42.2**; stdin-EOF exits cleanly. +- 42.1/42.2 behaviour (`cases/kernel-42.json`): `read` and `lineread` on a + file that ends without a trailing newline; source-form handlers running + before ordinary macros; namespace `externals` given an expression. - Closed divergences (now asserted): `float-formatting`, `int-div-zero`, `hush-file-write`, `load-toplevel-echo`. +- Documented divergences: integers above 2^53 and 2^63 (see below). - `--heavy`: `(yggdrasil.shake ["tests/fib.shen"] OUT)` on every host; the produced `kernel.kl` + `yggdrasil.manifest` must be byte-identical. User KL differs by gensym and is not asserted. @@ -35,6 +50,59 @@ every tracked difference is now a hard agreement. Each case is `expect: output` (golden) or `expect: agreement` (all impls match). +### Capability SKIP + +A launcher that exits **3** marks its cell `SKIP` (shen-joy for anything +outside its subset). Shen has no portable `exit`, so a corpus program that +needs an optional facility probes for it and, when it is missing, prints a +line starting with `BIFROST-SKIP:`; Bifrost treats that like exit 3. The +source-form and namespace cases use this: ports that do not boot +`shen.register-source-form` or `extension-namespaces.kl` SKIP instead of +failing. At the time of writing only shen-scheme and shen-rust boot the +namespaces extension. + +### Integer precision (documented divergence) + +Shen leaves number representation to the platform, so these cases report +buckets instead of asserting agreement: + +| case | expression | buckets | +|------|------------|---------| +| `bigint-2pow53-plus-1` | `(+ 9007199254740993 0)` | `9007199254740993`: shen-cl, shen-erl, shen-scheme (bignums); shen-julia, shen-c, shen-rust, shen-swift, shen-truffle (64-bit integers). `9007199254740992`: shen-go, ShenScript, shen-lua (IEEE doubles only) | +| `bigint-2pow63-ordered` | `(> (+ 9223372036854775807 1) 9223372036854775807)` | `true`: shen-cl, shen-erl, shen-scheme (bignums). `false`: shen-julia, shen-c (wrap to -2^63); shen-rust, shen-swift, shen-truffle (overflow to a double equal to 2^63); shen-go, ShenScript, shen-lua (doubles) | + +The printed 2^63 sum also differs by host float formatting +(`9223372036854776000`, `9.223372036854776e+18`, `9.223372036854776E18`), so +the second case compares a boolean. + +### Upstream shen-scheme 0.50 reference + +Pointing `BIFROST_SHEN_SCHEME` at an upstream tizoc 0.50 binary runs the same +kernel. It FAILS exactly two cases, by design: `hush-file-write` (stock +`writer.kl` silences file-stream `pr` under `-q`) and `stdin-eof-clean-exit` +(the canonical REPL loops on EOF; Bifrost caps captured output at 8 MB). The +pyrex41 fork (the default `shen-scheme` install) reapplies both fixes on top of +0.50. + +### Known port gaps + +These FAILs are port bugs, not intended differences; each was already present +on the port's Tarver S42.0 main branch: + +- shen-c (experimental): `version-banner-contains` (port-local `Shen-C 0.2.3` + banner), `float-add-imprecise` (prints `0.3`), and script mode echoing each + toplevel value like `load` instead of the launcher's quiet load + (`bounded-tail-sum-file`, `source-form-before-macros`). +- shen-julia: native `integer?` is false for `4.0`, and `hash` of `1` and `1.0` + differ (`integer-float-integral`, `hash-agrees-with-equality`). + +### Normaliser + +Before comparing, Bifrost drops the `run time:` banner (with the blank line its +leading newline produces), `(fn NAME)` load echoes, shen-lua's trailing value +echo, JVM `Picked up JAVA_TOOL_OPTIONS: …` notices (proxies and CI images set +that variable), and leading/trailing/repeated blank lines. + ## `--shake` (deploy path) Runs each **script-mode** program through Yggdrasil: shake once, build a @@ -43,7 +111,14 @@ stand-alone artifact per target, run, diff. Missing toolchains SKIP. Artifacts map: lisp→`shen-cl`, lua→`shen-lua`, go→`shen-go`, joy→`shen-joy`, erlang→`shen-erl`, rust→`shen-rust`, js→`ShenScript`, julia→`shen-julia`, scheme→`shen-scheme`, swift→`shen-swift`, truffle→`shen-truffle`. Joy is a -bounded image: unsupported KLambda exits 3 and is SKIP. +bounded image: unsupported KLambda exits 3 and is SKIP. The one corpus program +inside its subset is `programs/joy-sum.shen` (`bounded-tail-sum-file`, the same +program as Yggdrasil's `tests/joy-sum.shen`): its joy artifact must print +`32004000`. `tests/fib.shen` and the other script programs are capability SKIPs +on joy. The 42.1/42.2 script cases (`source-form-before-macros`, +`namespace-externals-expression`) use `eval` and `read-from-string` at run time, +so they are not meant for `--shake`; run `--shake` with `--only` on the +first-order programs. Needs `$BIFROST_YGGDRASIL_DIR` (default `../yggdrasil`) or `$BIFROST_YGGDRASIL_BIN`. This mode is minutes: Go/Rust compile from scratch; diff --git a/docs/port-layout.md b/docs/port-layout.md index a6a8179..90e89e3 100644 --- a/docs/port-layout.md +++ b/docs/port-layout.md @@ -17,8 +17,8 @@ this tree, those paths become defaults instead of special cases. flake.nix exports #toolchain (and usually #devShell) Makefile / language build file kernel/ - klambda/ S42 .kl modules, load order as install.lsp - PROVENANCE.md tag, hash, which Tarver/community tree + klambda/ ShenOSKernel 42.2 .kl modules; boot ends with (shen.initialise) + PROVENANCE.md tag, commit, SHA-256, lineage choice core.kl … tests/ official kernel suite (runme.shen, harness.shen, …) @@ -70,7 +70,7 @@ launcher**. A port is “layout-clean” when: -- `kernel/klambda/PROVENANCE.md` names the exact S42 (or later) drop +- `kernel/klambda/PROVENANCE.md` names the exact kernel drop (ShenOSKernel 42.2: tag `shen-42.2`, commit, tarball SHA-256) - `kernel/tests/runme.shen` is the official suite, unmodified - the launcher is `bin/shen` or `bin//shen` - `flake.nix` exports `#toolchain` diff --git a/front.go b/front.go index 17ef674..ef403ec 100644 --- a/front.go +++ b/front.go @@ -67,27 +67,47 @@ func resolveActiveImpl(a *Adapters, override string) (string, string) { return "", "default" } -// Port banners put the Shen kernel version first (for S42 this is the integer -// `42`), followed by implementation/port versions. Capture either an integer -// or dotted version and use the first token so `--versions` reports the runtime -// kernel rather than the port release (e.g. shen-cl's 3.0.3). -var versionRe = regexp.MustCompile(`\b(\d+(?:\.\d+)?)\b`) +// Standard launcher banners (extension-launcher.kl's version-string) put the +// kernel version first, e.g. `42.2 (port ("Julia" "shen-julia") ...)`, followed +// by the port and implementation versions. kernelVersionRe takes only that +// leading token (dotted, optionally quoted as an eval of (version) may print +// it), so `--versions` reports the runtime kernel rather than a port release +// such as shen-cl's 3.0.3. +var kernelVersionRe = regexp.MustCompile(`^"?(\d+(?:\.\d+)*)"?(?:\s|$)`) + +// parseKernelVersion returns the leading kernel version of a banner or of an +// eval of (version), after normalisation. +func parseKernelVersion(out string) (string, bool) { + m := kernelVersionRe.FindStringSubmatch(strings.TrimSpace(normalize(out, nil))) + if m == nil { + return "", false + } + return m[1], true +} // probeKernelVersion runs a port's version probe live (README badges drift). +// A port-local --version banner that does not lead with the kernel version +// (shen-c prints `Shen-C 0.2.3`) falls back to an eval of (version). func probeKernelVersion(impl Impl) string { - var c aCase + var probes []aCase if impl.Cfg.Version != nil { - c = aCase{Mode: "version"} - } else { - c = aCase{Mode: "eval", Expr: "(version)"} + probes = append(probes, aCase{Mode: "version"}) + } + probes = append(probes, aCase{Mode: "eval", Expr: "(version)"}) + timedOut := false + for _, c := range probes { + r := runInvocation(buildArgv(impl, c, ""), 30e9, false, "") + if r.Timeout { + timedOut = true + continue + } + if v, ok := parseKernelVersion(r.Out); ok { + return v + } } - r := runInvocation(buildArgv(impl, c, ""), 30e9, false, "") - if r.Timeout { + if timedOut { return "timeout" } - if m := versionRe.FindStringSubmatch(r.Out); m != nil { - return m[1] - } return "?" } diff --git a/matrix.go b/matrix.go index 09ce3ea..93ce221 100644 --- a/matrix.go +++ b/matrix.go @@ -217,7 +217,7 @@ func resultFromInvocation(r runResult, extraPrefixes []string) *implResult { Raw: r.Out, Norm: normalize(r.Out, extraPrefixes), Timeout: r.Timeout, Rc: r.Rc, } - if r.Rc == 3 { + if r.Rc == 3 || hasCapabilitySkip(result.Norm) { result.Status = "SKIP" } return result @@ -257,6 +257,11 @@ func evaluateCase(c aCase, available map[string]Impl, s suite, programsDir strin for name, im := range available { results[name] = runCaseOnImpl(im, c, s, programsDir, heavyTimeout) } + return judgeCase(c, results) +} + +// judgeCase decides a case's verdict from its per-impl results. +func judgeCase(c aCase, results map[string]*implResult) caseResult { res := caseResult{PerImpl: results, Verdict: "PASS"} active := map[string]*implResult{} for name, result := range results { @@ -350,17 +355,43 @@ func evaluateCase(c aCase, available map[string]Impl, s suite, programsDir strin } else { setAll(active, "PASS") } + if isDiv { + // Only one bucket among the ports that ran: still pin each + // listed port to its documented bucket. + for _, n := range sortedKeys(active) { + if want, ok := c.Table[n]; ok && active[n].Norm != want { + active[n].Status = "FAIL" + res.Verdict = "FAIL" + res.Detail += fmt.Sprintf("%s=%q left documented bucket %q; ", n, active[n].Norm, want) + } + } + } default: if isDiv { res.Verdict = "DIVERGE" setAll(active, "DIVERGE") + // A table pins each listed port to its documented bucket, so + // a port that moves bucket is a real FAIL, not more noise. + var moved []string + for _, n := range sortedKeys(active) { + if want, ok := c.Table[n]; ok && active[n].Norm != want { + active[n].Status = "FAIL" + moved = append(moved, fmt.Sprintf("%s=%q (table %q)", n, active[n].Norm, want)) + } + } + if len(moved) > 0 { + res.Verdict = "FAIL" + res.Detail = "left documented bucket: " + strings.Join(moved, ", ") + "; " + } } else { res.Verdict = "FAIL" setAll(active, "FAIL") } - res.Detail = bucketDetail(norms) + buckets := bucketDetail(norms) if isDiv { - res.Detail = fmt.Sprintf("KNOWN DIVERGENCE [%s]: %s", c.KnownDivergence, res.Detail) + res.Detail += fmt.Sprintf("KNOWN DIVERGENCE [%s]: %s", c.KnownDivergence, buckets) + } else { + res.Detail = buckets } } case "version": diff --git a/portable_test.go b/portable_test.go index 0f0d736..8850421 100644 --- a/portable_test.go +++ b/portable_test.go @@ -18,13 +18,13 @@ func TestShenLuaPathFallbackValidatesPortAndKernel(t *testing.T) { launcher := filepath.Join(tmp, "shen") t.Setenv("PATH", tmp+string(os.PathListSeparator)+os.Getenv("PATH")) a := &Adapters{baseDir: tmp} - cfg := Adapter{Kernel: "42"} + cfg := Adapter{Kernel: "42.2"} for _, tc := range []struct { port, kernel, want string }{ - {"shen-lua", "42", launcher}, - {"shen-go", "42", ""}, - {"shen-lua", "41.2", ""}, + {"shen-lua", "42.2", launcher}, + {"shen-go", "42.2", ""}, + {"shen-lua", "42", ""}, } { script := "#!/bin/sh\ncase \"$2\" in\n '(port)') printf '%s\\n' '" + tc.port + "' ;;\n '(version)') printf '%s\\n' '" + tc.kernel + "' ;;\nesac\n" if err := os.WriteFile(launcher, []byte(script), 0o755); err != nil { @@ -163,7 +163,7 @@ func TestShenTruffleAdapter(t *testing.T) { if err != nil { t.Fatal(err) } - if cfg.Env != "BIFROST_SHEN_TRUFFLE" || cfg.Kernel != "42" || cfg.Status != "production" { + if cfg.Env != "BIFROST_SHEN_TRUFFLE" || cfg.Kernel != "42.2" || cfg.Status != "production" { t.Fatalf("unexpected shen-truffle adapter: %#v", cfg) } got := buildArgv(Impl{Name: "shen-truffle", Cfg: cfg, Bin: "/tmp/shen-truffle"}, aCase{Mode: "eval", Expr: "(+ 1 2)"}, "") @@ -215,7 +215,7 @@ func TestShenCAdapter(t *testing.T) { if err != nil { t.Fatal(err) } - if cfg.Env != "BIFROST_SHEN_C" || cfg.Status != "experimental" || cfg.Kernel != "42" { + if cfg.Env != "BIFROST_SHEN_C" || cfg.Status != "experimental" || cfg.Kernel != "42.2" { t.Fatalf("unexpected shen-c adapter: %#v", cfg) } if len(cfg.DefaultPaths) != 1 || cfg.DefaultPaths[0] != "../shen-c/bin/shen-c" { @@ -336,3 +336,71 @@ func TestRouterRecognisesSubcommands(t *testing.T) { t.Error("test-matrix flags must not be treated as subcommands") } } + +func TestParseKernelVersion(t *testing.T) { + for _, tc := range []struct { + out, want string + ok bool + }{ + {"42.2 (port (\"Julia\" \"shen-julia\") implementation (\"1.13.1\" \"0.1\"))\n\n", "42.2", true}, + {"42.2 (port (\"Common Lisp\" \"3.0.3\") implementation (\"SBCL\" \"2.6.7\"))\n", "42.2", true}, + {"42.2\n", "42.2", true}, + {"\"42.2\"\n", "42.2", true}, + {"42\n", "42", true}, + {"Shen-C 0.2.3\n", "", false}, + {"", "", false}, + } { + got, ok := parseKernelVersion(tc.out) + if got != tc.want || ok != tc.ok { + t.Errorf("parseKernelVersion(%q) = %q, %v; want %q, %v", tc.out, got, ok, tc.want, tc.ok) + } + } +} + +func TestNormalizeDropsRunTimeBannerBlankAndJVMNotice(t *testing.T) { + julia := "\"PROBE\"\n42\n(fn p)\n\nrun time: 2.88 secs\nloaded\n" + lua := "\"PROBE\"\n42\n(fn p)\nloaded\n" + if a, b := normalize(julia, nil), normalize(lua, nil); a != b { + t.Fatalf("run-time banner blank line not dropped: %q vs %q", a, b) + } + jvm := "Picked up JAVA_TOOL_OPTIONS: -Dfoo=bar\n42\n" + if got := normalize(jvm, nil); got != "42" { + t.Fatalf("JVM notice not stripped: %q", got) + } +} + +func TestCapabilitySkipMarker(t *testing.T) { + r := resultFromInvocation(runResult{Out: "BIFROST-SKIP: namespaces extension not booted\n"}, nil) + if r.Status != "SKIP" { + t.Fatalf("status = %q, want SKIP", r.Status) + } + r = resultFromInvocation(runResult{Out: "[42 2]\n"}, nil) + if r.Status != "" { + t.Fatalf("status = %q, want unset", r.Status) + } +} + +func TestKnownDivergenceTablePinsBuckets(t *testing.T) { + c := aCase{Expect: "agreement", KnownDivergence: "x", Table: map[string]string{"a": "1", "b": "2"}} + mk := func(norms map[string]string) map[string]*implResult { + m := map[string]*implResult{} + for n, v := range norms { + m[n] = &implResult{Norm: v} + } + return m + } + for _, tc := range []struct { + norms map[string]string + want string + }{ + {map[string]string{"a": "1", "b": "2"}, "DIVERGE"}, + {map[string]string{"a": "1", "b": "2", "c": "3"}, "DIVERGE"}, + {map[string]string{"a": "2", "b": "2"}, "FAIL"}, + {map[string]string{"a": "1", "b": "1"}, "FAIL"}, + {map[string]string{"a": "1"}, "PASS"}, + } { + if got := judgeCase(c, mk(tc.norms)).Verdict; got != tc.want { + t.Errorf("%v: verdict %s, want %s", tc.norms, got, tc.want) + } + } +} diff --git a/programs/eof-lineread.txt b/programs/eof-lineread.txt new file mode 100644 index 0000000..4a3cc11 --- /dev/null +++ b/programs/eof-lineread.txt @@ -0,0 +1,2 @@ +hello (+ 1 2) +last line no newline \ No newline at end of file diff --git a/programs/eof-read.txt b/programs/eof-read.txt new file mode 100644 index 0000000..3b47baa --- /dev/null +++ b/programs/eof-read.txt @@ -0,0 +1 @@ +(+ 40 2) 42 \ No newline at end of file diff --git a/programs/namespace-externals-expr.shen b/programs/namespace-externals-expr.shen new file mode 100644 index 0000000..652df75 --- /dev/null +++ b/programs/namespace-externals-expr.shen @@ -0,0 +1,14 @@ +\\ namespace-externals-expr.shen -- 42.2 namespaces: (externals EXPR) evaluates +\\ EXPR to a symbol list (42.1 accepted only a literal list). bifrost-ns-ext +\\ stays global; local becomes bifrost.demo.local. +\\ Ports that do not boot extension-namespaces print the BIFROST-SKIP marker. +(define bifrost-ns-vocab -> [bifrost-ns-ext]) + +(define bifrost-ns-run -> + (do (map (/. F (eval F)) + (read-from-string "(shen.x.namespace bifrost.demo (externals (bifrost-ns-vocab)) (define bifrost-ns-ext X -> (+ X 1)) (define local X -> (bifrost-ns-ext X)))")) + [(bifrost-ns-ext 41) (bifrost.demo.local 1)])) + +(if (trap-error (do (shen.x.namespaces.initialise) true) (/. E false)) + (do (print (bifrost-ns-run)) (nl)) + (output "BIFROST-SKIP: namespaces extension not booted~%")) diff --git a/programs/source-form-order.shen b/programs/source-form-order.shen new file mode 100644 index 0000000..ef9063c --- /dev/null +++ b/programs/source-form-order.shen @@ -0,0 +1,19 @@ +\\ source-form-order.shen -- 42.1 source-form handlers run BEFORE ordinary +\\ macros, and macros still see what the handler produced. +\\ bifrost-probe has both a source-form handler (-> bifrost-probe2) and a +\\ macro rule (-> macro-first). Handler-first prints handler-then-macro. +\\ Ports that do not boot the hook print the BIFROST-SKIP capability marker. +(define bifrost-sf-handler + [bifrost-probe X] -> [[bifrost-probe2 X]]) + +(defmacro bifrost-sf-macro + [bifrost-probe X] -> [@p macro-first X] + [bifrost-probe2 X] -> [@p handler-then-macro X]) + +(define bifrost-sf-run -> + (do (shen.register-source-form bifrost-probe (fn bifrost-sf-handler)) + (map (/. F (eval F)) (read-from-string "(bifrost-probe 1) (bifrost-probe 2)")))) + +(if (trap-error (do (fn shen.register-source-form) true) (/. E false)) + (do (print (bifrost-sf-run)) (nl)) + (output "BIFROST-SKIP: source-form handlers not booted~%")) diff --git a/run.go b/run.go index 478d9bc..9aa180b 100644 --- a/run.go +++ b/run.go @@ -118,6 +118,16 @@ func normalize(text string, extraPrefixes []string) string { s := strings.TrimRight(ln, " \t") stripped := strings.TrimSpace(s) if strings.HasPrefix(stripped, "run time:") { + // The kernel prints the banner with a leading newline, so drop + // the blank line it produces too: a port that omits the banner + // (e.g. shen-lua on a warm fasl-cache hit) must not differ by + // that blank line alone. + if len(out) > 0 && strings.TrimSpace(out[len(out)-1]) == "" { + out = out[:len(out)-1] + } + continue + } + if isJVMOptionsNotice(stripped) { continue } if strings.HasPrefix(stripped, "(fn ") && strings.HasSuffix(stripped, ")") { @@ -166,6 +176,65 @@ func normalize(text string, extraPrefixes []string) string { return strings.Join(collapsed, "\n") } +// isJVMOptionsNotice matches the line a JVM writes to stderr whenever +// JAVA_TOOL_OPTIONS, _JAVA_OPTIONS or JDK_JAVA_OPTIONS is set (agent proxies +// and CI images set them). It is host chrome, not Shen output, so a JVM-hosted +// port such as shen-truffle is compared without it. +func isJVMOptionsNotice(line string) bool { + for _, v := range []string{"JAVA_TOOL_OPTIONS", "_JAVA_OPTIONS", "JDK_JAVA_OPTIONS"} { + if strings.HasPrefix(line, "Picked up "+v+":") || strings.HasPrefix(line, "NOTE: Picked up "+v+":") { + return true + } + } + return false +} + +// capabilitySkipMarker is printed by a corpus program when the port does not +// boot an optional kernel facility (e.g. the namespaces extension). Shen has +// no portable exit primitive, so this line is the in-language equivalent of a +// launcher exiting 3: the (impl, case) cell becomes a capability SKIP. +const capabilitySkipMarker = "BIFROST-SKIP:" + +func hasCapabilitySkip(norm string) bool { + for _, ln := range strings.Split(norm, "\n") { + if strings.HasPrefix(strings.TrimSpace(ln), capabilitySkipMarker) { + return true + } + } + return false +} + +// maxCapture bounds what one invocation may buffer. A port whose REPL loops +// at stdin EOF (canonical Shen, upstream shen-scheme 0.50) prints megabytes +// per second until the timeout; keeping the head is enough to report it. +const maxCapture = 8 << 20 + +type cappedBuffer struct { + buf bytes.Buffer + truncated bool +} + +func (c *cappedBuffer) Write(p []byte) (int, error) { + if room := maxCapture - c.buf.Len(); room > 0 { + if len(p) > room { + c.buf.Write(p[:room]) + c.truncated = true + } else { + c.buf.Write(p) + } + } else if len(p) > 0 { + c.truncated = true + } + return len(p), nil +} + +func (c *cappedBuffer) String() string { + if c.truncated { + return c.buf.String() + "\n[bifrost: output truncated]\n" + } + return c.buf.String() +} + // runResult captures the portable process outcome used by the matrix. type runResult struct { Rc int // -1 if unknown/timeout @@ -187,7 +256,7 @@ func runInvocation(argv []string, timeout time.Duration, stdinEOF bool, cwd stri if stdinEOF { cmd.Stdin = strings.NewReader("") } // else nil -> null device - var buf bytes.Buffer + var buf cappedBuffer cmd.Stdout = &buf cmd.Stderr = &buf err := cmd.Run()