Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
2ce7802
build(deps): bump the github-actions group with 2 updates
dependabot[bot] Sep 9, 2026
b63f9de
perf(gc): reuse discovery for internal reference counts
InauguralPhysicist Sep 12, 2026
ab881da
fix(ci): bound SIGUSR1 fixtures and support macOS shells
InauguralPhysicist Sep 12, 2026
39154c5
fix(ci): split full extension suites into required workers
InauguralPhysicist Sep 12, 2026
509ac64
fix(test): restore binary identity when Bash 3 exits
InauguralPhysicist Sep 12, 2026
56da89e
fix(lint): bound missing-file JSON diagnostic messages
InauguralPhysicist Sep 12, 2026
ee0cb8e
test: preserve useful context when a suite child fails
InauguralPhysicist Sep 12, 2026
affc5a5
fix(ci): give both sanitizer suites independent workers
InauguralPhysicist Sep 12, 2026
bb49292
fix: repair macOS executable paths, matrix results, and CI gates
InauguralPhysicist Sep 12, 2026
24ac299
Merge verified CI repairs into the action updates
InauguralPhysicist Sep 12, 2026
e35805c
Merge CI repairs and action updates into collector traversal reuse
InauguralPhysicist Sep 12, 2026
597b287
fix(ci): bound sanitizer startup and use fixed LLVM on macOS 26
InauguralPhysicist Sep 12, 2026
63ec630
Merge verified macOS sanitizer repair into action updates
InauguralPhysicist Sep 12, 2026
1617fb3
Merge macOS sanitizer completion fix into GC integration
InauguralPhysicist Sep 12, 2026
2c30282
fix(ci): retire leak-control roots before sanitizer inspection
InauguralPhysicist Sep 12, 2026
f75a2bf
Merge validated leak-control repair into dependency updates
InauguralPhysicist Sep 12, 2026
7fc12c0
Merge validated leak-control repair into GC integration
InauguralPhysicist Sep 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 214 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -295,14 +295,33 @@ jobs:
name: macos / ${{ matrix.os }}
needs: scope
runs-on: ${{ matrix.os }}
timeout-minutes: 30
# Intel completed the gfx gate and 540 compiler audit calls but reached
# 30 minutes in the audit's fault controls (#1130). Keep the full suite.
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
os: [macos-latest, macos-15-intel]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- if: needs.scope.outputs.code == 'true'
name: Shell harness portability controls
shell: bash
run: |
result=$(mktemp)
rc=0
bash tests/test_ci_portability.sh > "$result" 2>&1 || rc=$?
cat "$result"
[ "$rc" -eq 0 ] || exit "$rc"
[ "$(grep -cx 'ci-portability: checks=25 failures=0' "$result")" -eq 1 ]
rc=0
bash tests/test_ci_portability.sh --no-cleanup-deferral > "$result" 2>&1 || rc=$?
cat "$result"
[ "$rc" -eq 1 ]
grep -qxF 'FAIL: cleanup deferral fault abandoned READY child after delivered signal and matching trap exit' "$result"
! grep -q '^ci-portability: checks=' "$result"

- if: needs.scope.outputs.code == 'true'
name: Build
run: CC=clang ./build.sh
Expand All @@ -311,6 +330,33 @@ jobs:
name: Verify binary
run: ./src/eigenscript --version

# Use upstream LLVM's LSan runtime for the gfx gate, independently
# of the default Apple Clang used for the interpreter build.
# LLVM 20 deadlocks before main on macOS 26.4+ (llvm-project#182943).
# The fix was backported to LLVM 22 (llvm-project#188913). The gate's
# bounded executable control and intentional leak still verify it.
- if: needs.scope.outputs.code == 'true'
name: Select gfx LeakSanitizer toolchain
run: |
case '${{ matrix.os }}' in
macos-15-intel) formula=llvm@18 ;;
macos-latest)
formula=llvm@22
brew install --force-bottle "$formula"
;;
esac
if ! command -v timeout >/dev/null 2>&1 && ! command -v gtimeout >/dev/null 2>&1; then
brew install --force-bottle coreutils
fi
sanitizer_cc="$(brew --prefix "$formula")/bin/clang"
test -x "$sanitizer_cc"
"$sanitizer_cc" --version
echo "EIGS_ASAN_GFX_CC=$sanitizer_cc" >> "$GITHUB_ENV"

- if: needs.scope.outputs.code == 'true'
name: Verify gfx sanitizer controls
run: bash tests/test_asan_gfx.sh --toolchain-only

- if: needs.scope.outputs.code == 'true'
name: Run test suite
run: cd tests && bash run_all_tests.sh
Expand All @@ -331,8 +377,38 @@ jobs:
# (eigs_embed.c is otherwise 0% — embed-smoke is the only thing that runs
# it), compile-check the LSP (bitrots invisibly otherwise), and run the
# standalone JIT emitter smoke tests.
# Preserve the existing required check name while requiring every variant.
# Each worker still checks out on docs-only changes; its code steps skip.
# A failed/cancelled/skipped worker is never an aggregate success.
extensions:
name: extensions (http+model+gfx suite; embed/lsp/jit-smoke)
needs: [scope, extensions-http, extensions-gfx, extensions-zlib, extensions-net]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- name: Require complete extension coverage
shell: bash
env:
SCOPE_RESULT: ${{ needs.scope.result }}
CODE: ${{ needs.scope.outputs.code }}
HTTP_RESULT: ${{ needs.extensions-http.result }}
GFX_RESULT: ${{ needs.extensions-gfx.result }}
ZLIB_RESULT: ${{ needs.extensions-zlib.result }}
NET_RESULT: ${{ needs.extensions-net.result }}
run: |
set -euo pipefail
[ "$SCOPE_RESULT" = success ]
case "$CODE" in true|false) ;; *) exit 1;; esac
for result in "$HTTP_RESULT" "$GFX_RESULT" "$ZLIB_RESULT" "$NET_RESULT"; do
[ "$result" = success ] || exit 1
done
echo "Extension coverage complete (code=$CODE; four workers succeeded)"

extensions-http:
name: extensions / http+model and ancillary checks
needs: [dev-image, scope]
runs-on: ubuntu-latest
timeout-minutes: 30
Expand All @@ -347,6 +423,23 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- if: needs.scope.outputs.code == 'true'
name: Shell harness portability controls
shell: bash
run: |
result=$(mktemp)
rc=0
bash tests/test_ci_portability.sh > "$result" 2>&1 || rc=$?
cat "$result"
[ "$rc" -eq 0 ] || exit "$rc"
[ "$(grep -cx 'ci-portability: checks=25 failures=0' "$result")" -eq 1 ]
rc=0
bash tests/test_ci_portability.sh --no-cleanup-deferral > "$result" 2>&1 || rc=$?
cat "$result"
[ "$rc" -eq 1 ]
grep -qxF 'FAIL: cleanup deferral fault abandoned READY child after delivered signal and matching trap exit' "$result"
! grep -q '^ci-portability: checks=' "$result"

- if: needs.scope.outputs.code == 'true'
name: JIT emitter smoke tests
run: make jit-smoke
Expand Down Expand Up @@ -384,6 +477,37 @@ jobs:
name: Run full suite against http+model build
run: cd tests && bash run_all_tests.sh

- if: needs.scope.outputs.code == 'true'
name: Compile-check LSP
run: make lsp

- if: needs.scope.outputs.code == 'true'
name: Compile-check DAP server
run: make dap

# 44 deterministic adversarial inputs against the ASan-instrumented
# stdin harness (same compile->vm pipeline as main.c). Seconds to run;
# any exit other than 0/1 is a crash.
- if: needs.scope.outputs.code == 'true'
name: Fuzz smoke (ASan harness, curated corpus)
run: make fuzz && bash fuzz/run_fuzz.sh

extensions-gfx:
name: extensions / gfx full suite
needs: [dev-image, scope]
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
packages: read
container:
image: ${{ needs.dev-image.outputs.image }}
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- if: needs.scope.outputs.code == 'true'
name: Build gfx variant
run: make gfx
Expand All @@ -392,6 +516,23 @@ jobs:
name: Run full suite against gfx build (audio [62], containment [132], gfx examples [97])
run: cd tests && bash run_all_tests.sh


extensions-zlib:
name: extensions / zlib full suite
needs: [dev-image, scope]
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
packages: read
container:
image: ${{ needs.dev-image.outputs.image }}
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- if: needs.scope.outputs.code == 'true'
name: Build zlib variant
run: make zlib
Expand All @@ -400,6 +541,23 @@ jobs:
name: Run full suite against zlib build (executes DEFLATE section [124])
run: cd tests && bash run_all_tests.sh


extensions-net:
name: extensions / net full suite
needs: [dev-image, scope]
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
packages: read
container:
image: ${{ needs.dev-image.outputs.image }}
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- if: needs.scope.outputs.code == 'true'
name: Build net variant
run: make net
Expand All @@ -408,20 +566,6 @@ jobs:
name: Run full suite against net build (executes network section [125])
run: cd tests && bash run_all_tests.sh

- if: needs.scope.outputs.code == 'true'
name: Compile-check LSP
run: make lsp

- if: needs.scope.outputs.code == 'true'
name: Compile-check DAP server
run: make dap

# 44 deterministic adversarial inputs against the ASan-instrumented
# stdin harness (same compile->vm pipeline as main.c). Seconds to run;
# any exit other than 0/1 is a crash.
- if: needs.scope.outputs.code == 'true'
name: Fuzz smoke (ASan harness, curated corpus)
run: make fuzz && bash fuzz/run_fuzz.sh

# Build the `full` variant (http+model+db) against a real PostgreSQL service
# and run the suite with DATABASE_URL set. Without this, ext_db.c never
Expand Down Expand Up @@ -472,8 +616,35 @@ jobs:
# The net that catches use-after-free, buffer overflow, and UB the normal
# -O2 build silently tolerates. Per-iteration leak regressions are covered
# separately by tests/test_leak_guard.sh.
# Keep the required status while both complete sanitizer variants run in
# independent checkouts. Docs-only workers skip code steps but must succeed.
sanitizers:
name: asan + ubsan (full suite)
needs: [scope, sanitizers-core, sanitizers-http]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- name: Require complete sanitizer coverage
shell: bash
env:
SCOPE_RESULT: ${{ needs.scope.result }}
CODE: ${{ needs.scope.outputs.code }}
CORE_RESULT: ${{ needs.sanitizers-core.result }}
HTTP_RESULT: ${{ needs.sanitizers-http.result }}
run: |
set -euo pipefail
[ "$SCOPE_RESULT" = success ]
case "$CODE" in true|false) ;; *) exit 1;; esac
for result in "$CORE_RESULT" "$HTTP_RESULT"; do
[ "$result" = success ] || exit 1
done
echo "Sanitizer coverage complete (code=$CODE; two workers succeeded)"

sanitizers-core:
name: asan + ubsan / core and LSP
needs: [dev-image, scope]
runs-on: ubuntu-latest
# 45, not 30: the green run before #915's [99u] section landed took 19m57,
Expand All @@ -499,6 +670,10 @@ jobs:
name: Build with AddressSanitizer + UBSan
run: make asan

- if: needs.scope.outputs.code == 'true'
name: Collector traversal ownership and reach
run: python3 tools/gc_traversal_check.py --variant asan

- if: needs.scope.outputs.code == 'true'
name: Run full suite under sanitizers
env:
Expand All @@ -516,6 +691,29 @@ jobs:
# sanitizer report from the LSP process (leaks, UB).
run: cd tests && bash test_lsp_asan.sh

sanitizers-http:
name: asan + ubsan / HTTP and model full suite
needs: [dev-image, scope]
runs-on: ubuntu-latest
# 45, not 30: the green run before #915's [99u] section landed took 19m57,
# and that section legitimately adds minutes under ASan (it launches the
# sanitized binary ~60 times; sanitizer process startup dominates) — a slow
# runner then hit the old ceiling and the job rendered as CANCELLED at
# 30:02 with the suite mid-section. A timeout is not a verdict: the same
# tree's local ASan suite was 4117/4117. Headroom target ~2x the observed
# green duration, per the suite-runtime-baseline rule.
timeout-minutes: 45
permissions:
contents: read
packages: read
container:
image: ${{ needs.dev-image.outputs.image }}
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

# The extension surface under sanitizers. `make asan` above compiles the
# HTTP + model extensions OUT, so ext_http.c — a network-facing server
# and client, the most exposed code in the repo — was never sanitized
Expand All @@ -532,8 +730,7 @@ jobs:
# path (#731) are therefore NOT caught by this step; that class needs an
# RSS-growth gate, measured separately.
#
# Runs last in this job: every Makefile variant writes src/eigenscript,
# so this must not clobber the binary the steps above are using.
# This worker owns its checkout and alias, independently of core ASan.
- if: needs.scope.outputs.code == 'true'
name: Run suite under sanitizers with the HTTP+model extensions
env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up QEMU
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
uses: docker/setup-qemu-action@1f40c72289eff860ee54a304f1438e3cff362e0a # v4.3.0

- name: Set up Docker buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ All notable changes to EigenScript are documented here.

### Fixed

- Standard-library imports and `exe_path` retain an absolute executable
anchor after `chdir` on macOS, including relative and PATH launches (#1133).
- Matrix products use separate binary64 multiplication and addition across
platforms, preserving strict-mode invalid-result detection (#1131).
- CI completes extension and sanitizer variants in separate workers. macOS
harnesses handle filename rejection, use an LLVM leak-detection runtime,
and reject sanitizer startup failures (#1126).

- **`EIGS_STRICT=1` reaches the graphics and audio extension (#1007).**
`src/ext_gfx.c` had no raise path at all — `grep -c rt_error src/ext_gfx.c`
was 0 — while ~89 argument reads went straight through `items[N]->data.num`,
Expand Down
3 changes: 3 additions & 0 deletions docs/COMPARISON.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ list
`buffer of [rows, cols]` and `reshape of [buf, rows, cols]` build the
flat-backed matrix. `matmul`, `softmax`, `sum`, `mean`, `norm`, `gather` and
the rest accept either, and hand back a buffer when every operand was one.
`matmul`, `matmul_at`, and `matmul_bt` round each multiplication to binary64
before adding it to the accumulator, in ascending inner-index order, on both
containers. They do not fuse the multiplication and addition.

One place EigenScript is louder than NumPy: an out-of-range index in `gather`
**raises** rather than answering a stand-in, on both containers — NumPy's
Expand Down
5 changes: 5 additions & 0 deletions docs/DIAGNOSTICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,11 @@ so `--lint --json 2>/dev/null` is pure JSON). Each element is:
errors show `line:col` too, and the LSP diagnostic range starts at that
column. (Warning elements are line-only for now — per-warning spans are the
remaining #407 work.)
- An unreadable file emits `E000` with exit 1. Its decoded JSON `message`
follows the same 255-byte UTF-8 limit: a long `cannot read file '…'` message
retains the longest complete character prefix fitting 252 bytes, followed
by `...` (#1132). The separate `file` field keeps its existing path escaping
and output-buffer budget; clipping the message does not shorten that field.
- Exit code follows `--lint-level` (see below); the default fails on any
surviving warning.

Expand Down
Loading
Loading