diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index b1ed009c2..ec70ba1dd 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -58,7 +58,7 @@ jobs: timeout-minutes: ${{ github.event_name == 'workflow_dispatch' && 120 || 30 }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # gcc and g++ for C, C++ and Aether; GNU time for CPU time and memory. - name: Install C, C++ and GNU time @@ -66,12 +66,12 @@ jobs: sudo rm -f /etc/apt/sources.list.d/google-chrome.list; sudo apt-get update sudo apt-get install -y gcc g++ make time - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version: stable cache: false - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: distribution: temurin java-version: '21' @@ -129,7 +129,7 @@ jobs: - name: Upload results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: benchmark-results path: | diff --git a/.github/workflows/changelog-collect.yml b/.github/workflows/changelog-collect.yml index b5b85e437..192782234 100644 --- a/.github/workflows/changelog-collect.yml +++ b/.github/workflows/changelog-collect.yml @@ -42,9 +42,9 @@ concurrency: jobs: collect: name: Fold settled fragments - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: # The collector reads each fragment's commit time to decide whether # it has settled, so it needs the history, not a shallow clone. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f6a5ad5d..564d1b03a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,7 +136,7 @@ jobs: target: ci-sweep steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install toolchain if: matrix.install != '' @@ -159,7 +159,7 @@ jobs: # correctness never depends on a cache hit. - name: Restore miniaudio object cache id: audio_cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: .ci-cache key: audio-obj-${{ matrix.os }}-${{ matrix.cc }}-h${{ matrix.harden }}-${{ hashFiles('std/audio/miniaudio.h', 'std/audio/stb_vorbis.c', 'std/audio/aether_audio.c', 'std/audio/aether_audio.h') }} @@ -241,7 +241,7 @@ jobs: github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.audio_cache.outputs.cache-hit != 'true' && (matrix.target != 'ci-sweep' || matrix.unit == '1') - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: path: .ci-cache key: audio-obj-${{ matrix.os }}-${{ matrix.cc }}-h${{ matrix.harden }}-${{ hashFiles('std/audio/miniaudio.h', 'std/audio/stb_vorbis.c', 'std/audio/aether_audio.c', 'std/audio/aether_audio.h') }} @@ -273,12 +273,12 @@ jobs: shard: ["1/2", "2/2"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # The ARM64 leg's miniaudio object (same OS, compiler and build # variant, so the same key); restore only, the leg saves it. - name: Restore miniaudio object cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: .ci-cache key: audio-obj-macos-latest-clang-h0-${{ hashFiles('std/audio/miniaudio.h', 'std/audio/stb_vorbis.c', 'std/audio/aether_audio.c', 'std/audio/aether_audio.h') }} @@ -319,11 +319,11 @@ jobs: # ============================================================ ci-changelog: name: CHANGELOG entry present - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 if: github.event_name == 'pull_request' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 @@ -412,11 +412,11 @@ jobs: ci-freebsd-cross: name: FreeBSD / cross-compile (x86_64) - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install toolchain run: sudo rm -f /etc/apt/sources.list.d/google-chrome.list; sudo apt-get update && sudo apt-get install -y gcc make bc curl xz-utils @@ -424,7 +424,7 @@ jobs: # zig cc bundles no FreeBSD libc, so the base sysroot is what makes the # FreeBSD headers visible. Same source as the release leg. - name: Checkout aether-crossbuild - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: aether-lang-dev/aether-crossbuild path: crossbuild @@ -434,7 +434,7 @@ jobs: # dir every run. deps.lock pins both URLs and checksums. - name: Restore cross-toolchain download cache id: cross_cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: | .zig @@ -459,7 +459,7 @@ jobs: if: >- github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.cross_cache.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: path: | .zig @@ -484,10 +484,10 @@ jobs: # (no shm_open, no pthread_setaffinity_np, ...) on the PR, not at release. ci-android-cross: name: Android / cross-compile (aarch64) - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Cross-build for Android (ANDROID=1) run: | NDK="${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-}}" @@ -534,18 +534,18 @@ jobs: # ============================================================ ci-sqlite-cross: name: contrib.sqlite / self-contained cross builds - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install toolchain run: sudo rm -f /etc/apt/sources.list.d/google-chrome.list; sudo apt-get update && sudo apt-get install -y gcc make bc curl xz-utils file qemu-user - name: Restore zig + SQLite cross object cache id: sqlite_xcache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: | .zig @@ -577,7 +577,7 @@ jobs: if: >- github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.sqlite_xcache.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: path: | .zig @@ -586,11 +586,11 @@ jobs: ci-freebsd-native: name: FreeBSD / native build + unit tests - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # vmactions/freebsd-vm installs qemu with apt, under apt's default # connect timeout. When the runner's Azure mirror stopped answering, its @@ -633,7 +633,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install host language dev libraries run: | @@ -688,7 +688,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install build deps run: | @@ -737,7 +737,7 @@ jobs: # only ever supply the right one. Split restore/save as above. - name: Restore SQLite amalgamation object cache id: sqlite_obj_cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: .ci-cache/sqlite key: sqlite-obj-ubuntu-22.04-${{ hashFiles('contrib/sqlite/amalgamation.lock', 'tests/scripts/contrib_build.sh') }} @@ -759,7 +759,7 @@ jobs: if: >- github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.sqlite_obj_cache.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: path: .ci-cache/sqlite key: sqlite-obj-ubuntu-22.04-${{ hashFiles('contrib/sqlite/amalgamation.lock', 'tests/scripts/contrib_build.sh') }} @@ -883,7 +883,7 @@ jobs: runs-on: ${{ matrix.runner }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install build deps, lavapipe and gdb run: | @@ -1001,7 +1001,7 @@ jobs: shell: bash steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # Metal ships with macOS. Vulkan does not: the headers build # contrib.vulkan, the loader is what it opens at runtime, and MoltenVK is @@ -1077,7 +1077,7 @@ jobs: shell: msys2 {0} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: msys2/setup-msys2@v2 with: @@ -1179,7 +1179,7 @@ jobs: shell: msys2 {0} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: msys2/setup-msys2@v2 with: @@ -1246,7 +1246,7 @@ jobs: # (#1372); keep its object between runs, as on the Linux contrib leg. - name: Restore SQLite amalgamation object cache id: sqlite_obj_cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: .ci-cache/sqlite key: sqlite-obj-windows-${{ hashFiles('contrib/sqlite/amalgamation.lock', 'tests/scripts/contrib_build.sh') }} @@ -1295,7 +1295,7 @@ jobs: if: >- github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.sqlite_obj_cache.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: path: .ci-cache/sqlite key: sqlite-obj-windows-${{ hashFiles('contrib/sqlite/amalgamation.lock', 'tests/scripts/contrib_build.sh') }} @@ -1354,7 +1354,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install toolchain run: sudo rm -f /etc/apt/sources.list.d/google-chrome.list; sudo apt-get update && sudo apt-get install -y gcc make bc @@ -1374,7 +1374,7 @@ jobs: # could rot to a stub and stay green everywhere. OPENSSL=0 forces the # detection off regardless of what is installed on the runner. steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install toolchain run: sudo rm -f /etc/apt/sources.list.d/google-chrome.list; sudo apt-get update && sudo apt-get install -y gcc make bc @@ -1418,11 +1418,11 @@ jobs: # case that is skipped or does not agree, with the reason. ci-crypto-ct: name: Crypto constant-time (Valgrind) and conformance corpora - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install toolchain run: sudo rm -f /etc/apt/sources.list.d/google-chrome.list; sudo apt-get update && sudo apt-get install -y gcc make bc valgrind @@ -1439,7 +1439,7 @@ jobs: - name: Restore the conformance corpora id: conformance_cache if: success() || failure() - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: ~/.cache/aether-conformance key: conformance-${{ hashFiles('tests/conformance/*/run.sh') }} @@ -1457,7 +1457,7 @@ jobs: (success() || failure()) && github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.conformance_cache.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: path: ~/.cache/aether-conformance key: conformance-${{ hashFiles('tests/conformance/*/run.sh') }} @@ -1467,10 +1467,10 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install Emscripten - uses: mymindstorm/setup-emsdk@v14 + uses: mymindstorm/setup-emsdk@v15 with: version: 3.1.51 @@ -1551,7 +1551,7 @@ jobs: timeout-minutes: 15 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # --no-install-recommends: Recommends pulls libstdc++-arm-none-eabi-newlib, # 347 MB of C++ library this C-only build never links, and a slow Azure @@ -1577,7 +1577,7 @@ jobs: timeout-minutes: 45 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Free disk space for the ESP-IDF image run: | @@ -1610,7 +1610,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install RISC-V cross-toolchain + qemu run: | @@ -1634,7 +1634,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install tools run: | @@ -1680,7 +1680,7 @@ jobs: - name: Upload memory reports if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: memory-reports path: | diff --git a/.github/workflows/memory-check.yml b/.github/workflows/memory-check.yml index 6c0e71aea..29ec888c4 100644 --- a/.github/workflows/memory-check.yml +++ b/.github/workflows/memory-check.yml @@ -41,7 +41,7 @@ concurrency: jobs: valgrind-test: name: Valgrind Memory Leak Check - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 # A healthy run takes about a minute: 66 s on PR #2601, 25 s of it the # Valgrind run itself. It used to take an hour, while idle actor threads # spun (#2592) and Valgrind's default lock starved the threads they waited @@ -51,7 +51,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install dependencies run: | @@ -133,7 +133,7 @@ jobs: - name: Upload Valgrind report if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: valgrind-report path: | @@ -147,14 +147,14 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] + os: [ubuntu-26.04, macos-latest] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup GCC - if: matrix.os == 'ubuntu-latest' + if: matrix.os == 'ubuntu-26.04' run: | sudo rm -f /etc/apt/sources.list.d/google-chrome.list; sudo apt-get update sudo apt-get install -y gcc make @@ -167,7 +167,7 @@ jobs: - name: Build with AddressSanitizer run: | make clean - if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then + if [ "${{ matrix.os }}" = "ubuntu-26.04" ]; then # Linux supports LeakSanitizer make compiler CFLAGS="-fsanitize=address -fsanitize=leak -fno-omit-frame-pointer -O1 -g" \ LDFLAGS="-fsanitize=address -fsanitize=leak -pthread -lm" @@ -183,7 +183,7 @@ jobs: - name: Run tests with ASAN run: | - if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then + if [ "${{ matrix.os }}" = "ubuntu-26.04" ]; then export ASAN_OPTIONS="detect_leaks=1:halt_on_error=1:abort_on_error=1" export LSAN_OPTIONS="report_objects=1" else @@ -196,7 +196,7 @@ jobs: # nothing checked that, and every program leaked. Linux only: LeakSanitizer # is the whole point and macOS has no LSan. - name: Compiler leak gate - if: matrix.os == 'ubuntu-latest' + if: matrix.os == 'ubuntu-26.04' run: bash tests/scripts/check_compiler_leaks.sh # The C unit tests under UBSan, on macOS (Apple clang). On Linux they run @@ -222,11 +222,11 @@ jobs: memory-profiling: name: Memory Usage Profiling - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install dependencies run: | @@ -269,7 +269,7 @@ jobs: echo "SUCCESS: Peak memory usage acceptable" - name: Upload profiling report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: memory-profile path: | @@ -282,11 +282,11 @@ jobs: strategy: matrix: - os: [ubuntu-latest, macos-latest] + os: [ubuntu-26.04, macos-latest] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 # Linux only, as the setup steps above. The macOS runner already has # clang and make; the brew fallback that ran there installed nothing the @@ -294,7 +294,7 @@ jobs: # `brew update` failed the job whenever a tap preinstalled on the runner # could not be fetched (#2895). - name: Setup GCC - if: matrix.os == 'ubuntu-latest' + if: matrix.os == 'ubuntu-26.04' run: | sudo rm -f /etc/apt/sources.list.d/google-chrome.list; sudo apt-get update sudo apt-get install -y gcc make diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 508c075e0..00fb682eb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,7 +40,7 @@ jobs: # ================================================================== # prepare: name: Prepare release - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 if: github.ref == 'refs/heads/main' # `pull-requests: read` lets the decide step query PR state via gh, @@ -57,7 +57,7 @@ jobs: tag: ${{ steps.decide.outputs.tag }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 fetch-tags: true @@ -166,14 +166,14 @@ jobs: name: Auto-bump version needs: prepare if: needs.prepare.outputs.action == 'bump' || needs.prepare.outputs.action == 'refresh' - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 permissions: contents: write pull-requests: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 fetch-tags: true @@ -383,7 +383,7 @@ jobs: name: Tag release needs: prepare if: needs.prepare.outputs.action == 'tag' - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 permissions: contents: write @@ -393,7 +393,7 @@ jobs: version: ${{ steps.t.outputs.version }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 fetch-tags: true @@ -494,7 +494,7 @@ jobs: timeout: 80 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: ref: ${{ needs.tag.outputs.tag || github.ref }} # Full history + all tags. A shallow checkout of a tag does not @@ -678,7 +678,7 @@ jobs: cd release && TZ=UTC+12 zip -r ../aether-${VERSION}-${{ matrix.target }}.zip * - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: aether-${{ matrix.target }} path: | @@ -711,11 +711,11 @@ jobs: (needs.tag.result == 'success' && needs.tag.outputs.tag != '') || startsWith(github.ref, 'refs/tags/') ) - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: ref: ${{ needs.tag.outputs.tag || github.ref }} # Full history + all tags. A shallow checkout of a tag does not @@ -733,7 +733,7 @@ jobs: # libc) comes from the sibling aether-crossbuild repo, which fetches # FreeBSD's published base.txz (BSD-licensed, freely redistributable). - name: Checkout aether-crossbuild - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: aether-lang-dev/aether-crossbuild path: crossbuild @@ -908,7 +908,7 @@ jobs: || { echo "::error::the sysroot probe did not produce a FreeBSD binary"; exit 1; } - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: aether-freebsd-x86_64 path: | @@ -938,11 +938,11 @@ jobs: (needs.tag.result == 'success' && needs.tag.outputs.tag != '') || startsWith(github.ref, 'refs/tags/') ) - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: ref: ${{ needs.tag.outputs.tag || github.ref }} fetch-depth: 0 @@ -1008,7 +1008,7 @@ jobs: ls -la "aether-${VERSION}-android-aarch64-sysroot.tar.xz" - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: aether-android-aarch64 path: | @@ -1021,7 +1021,7 @@ jobs: # quietly drops a platform because its build broke is worse than no # release, and dropping it silently is what hid the last FreeBSD break. needs: [tag, build, build-freebsd, build-android] - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 if: | always() && needs.build.result == 'success' && needs.build-freebsd.result == 'success' @@ -1034,12 +1034,12 @@ jobs: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: ref: ${{ needs.tag.outputs.tag || github.ref }} - name: Download all artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: merge-multiple: true @@ -1058,7 +1058,7 @@ jobs: run: python3 .github/scripts/build_release_body.py ${{ steps.tag.outputs.version }} - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ steps.tag.outputs.tag }} name: Aether ${{ steps.tag.outputs.version }} diff --git a/.github/workflows/ubsan.yml b/.github/workflows/ubsan.yml index d33d421c3..a73cfd8b4 100644 --- a/.github/workflows/ubsan.yml +++ b/.github/workflows/ubsan.yml @@ -56,7 +56,7 @@ jobs: timeout-minutes: 35 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install toolchain run: sudo rm -f /etc/apt/sources.list.d/google-chrome.list; sudo apt-get update && sudo apt-get install -y gcc make bc pkg-config libgtk-4-dev; pip install --retries 10 --timeout 120 --break-system-packages websockets 2>/dev/null || pip install --retries 10 --timeout 120 websockets @@ -66,7 +66,7 @@ jobs: - name: Upload the UBSan reports if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: ubsan-reports path: build/ubsan/reports/ diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 4712e833b..430eef3ae 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -92,11 +92,11 @@ jobs: # ============================================================ windows-cross: name: Windows / cross-build (x86_64) - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 timeout-minutes: 25 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install toolchain run: sudo rm -f /etc/apt/sources.list.d/google-chrome.list; sudo apt-get update && sudo apt-get install -y gcc make bc curl xz-utils @@ -105,7 +105,7 @@ jobs: # mingw-w64 headers + CRT sources, so there is no sysroot to fetch. - name: Restore zig toolchain cache id: zig_cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: | .zig @@ -124,7 +124,7 @@ jobs: if: >- github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.zig_cache.outputs.cache-hit != 'true' - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: path: | .zig @@ -159,7 +159,7 @@ jobs: - name: Upload cross-built binaries if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: aether-windows-cross-x86_64 path: | @@ -181,7 +181,7 @@ jobs: # ============================================================ windows-wine: name: Windows / runtime under Wine (x86_64) - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 # Dependency installation can take 20 minutes on hosted runners; leave # time for the complete cross-build and runtime sweep after setup. timeout-minutes: 60 @@ -189,7 +189,7 @@ jobs: # a gate, so it must not add latency to an otherwise green run. steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install toolchain run: | @@ -263,7 +263,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up MSYS2 / MinGW-w64 uses: msys2/setup-msys2@v2 @@ -293,7 +293,7 @@ jobs: # key already embeds the source hash). - name: Restore miniaudio object cache id: audio_cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: .ci-cache key: audio-obj-windows-${{ matrix.root }}-${{ hashFiles('std/audio/miniaudio.h', 'std/audio/stb_vorbis.c', 'std/audio/aether_audio.c', 'std/audio/aether_audio.h') }} @@ -322,14 +322,14 @@ jobs: if: >- github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.audio_cache.outputs.cache-hit != 'true' && matrix.target == 'ci' - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: path: .ci-cache key: audio-obj-windows-${{ matrix.root }}-${{ hashFiles('std/audio/miniaudio.h', 'std/audio/stb_vorbis.c', 'std/audio/aether_audio.c', 'std/audio/aether_audio.h') }} - name: Upload Windows binaries if: matrix.target == 'ci' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: aether-windows-${{ matrix.root }} path: | @@ -383,7 +383,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 # MINGW64 (msvcrt), the C runtime the baseline was measured on: a heap # count is the C runtime's own. @@ -420,7 +420,7 @@ jobs: # failure is read from. - name: Upload the grid's report if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: ownership-grid-${{ matrix.slot }} path: | @@ -446,7 +446,7 @@ jobs: shell: msys2 {0} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up MSYS2 / MinGW-w64 uses: msys2/setup-msys2@v2 with: @@ -470,7 +470,7 @@ jobs: run: make ownership-grid-check OWNERSHIP_GRID_ARGS="--mode both --update-baseline" - name: Upload the baseline if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: ownership-grid-baseline path: | @@ -491,7 +491,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Visual Studio developer environment uses: ilammy/msvc-dev-cmd@v1 @@ -533,7 +533,7 @@ jobs: # is the one the windows-cross job built; the script runs under Git Bash, # with cl.exe on PATH from msvc-dev-cmd. - name: Download the cross-built aetherc - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: aether-windows-cross-x86_64 path: build/cross diff --git a/benchmarks/cross-language/aether/Makefile b/benchmarks/cross-language/aether/Makefile index d6d130ee4..0ecf1bfce 100644 --- a/benchmarks/cross-language/aether/Makefile +++ b/benchmarks/cross-language/aether/Makefile @@ -16,20 +16,28 @@ AETHER_INCLUDES := $(shell $(AETHER_CLI) cflags --cflags 2>/dev/null) OPT = -O3 -march=native -flto -fno-plt CFLAGS = $(OPT) $(AETHER_INCLUDES) # The generated C calls std as well as the runtime (string_retain and -# string_release for string values, aether_write_bytes for println), so the -# link takes the rest from libaether, by the toolchain's own `ae cflags -# --libs`, as the include set above does. The runtime compiled below defines -# its symbols first, so only std members come from the archive. -AETHER_LIBS := $(shell $(AETHER_CLI) cflags --libs 2>/dev/null) -LDFLAGS = -flto -pthread -lm $(AETHER_LIBS) +# string_release for string values, aether_write_bytes for println). std is +# taken from libaether.a, built with the defines its sources need (PCRE2, +# OpenSSL, zlib), with the archive's own runtime members taken out: the +# runtime is the one compiled below, and only it. `ae cflags --libs` gives the +# system libraries, less libaether itself. +# +# With the whole archive on the line the runtime was defined twice on ELF: +# GCC's LTO plugin does not list what top-level asm defines, and the +# rendezvous records are such asm (runtime/aether_rv_mark.h), so the link +# took the archive's aether_resource_caps.o for its anchor, beside the +# compiled one, and every symbol in it was then defined twice. +STD_LIB := ../build/aether-std/libaether_std.a +AETHER_LIBS := $(filter-out -laether,$(shell $(AETHER_CLI) cflags --libs 2>/dev/null)) +LDFLAGS = -flto -pthread -lm $(STD_LIB) $(AETHER_LIBS) # Runtime sources, taken from the build MANIFEST (regenerated on every # `make stdlib`, the same set linked into libaether.a) so this list never # drifts as the runtime grows. A hand-maintained subset went stale and the # link broke on a newly-emitted symbol (`aether_capsicum_autosandbox`), which # surfaced only as "Aether [SKIP] Build failed" in the suite. Compiled from # source (not libaether.a) so the runtime gets the same `-O3 -march=native -# -flto` as the benchmark, for a fair comparison. Only `runtime/` entries are -# needed — these actor microbenchmarks don't touch std. +# -flto` as the benchmark, for a fair comparison; std comes from the archive +# above. MANIFEST := ../../../build/MANIFEST RUNTIME_SRCS := $(addprefix ../../../,$(shell grep '^runtime/' $(MANIFEST) 2>/dev/null)) @@ -37,38 +45,43 @@ RUNTIME_SRCS := $(addprefix ../../../,$(shell grep '^runtime/' $(MANIFEST) 2>/de all: ping_pong counting thread_ring fork_join skynet -ping_pong: ping_pong.ae +$(STD_LIB): ../../../build/libaether.a $(MANIFEST) + @mkdir -p $(dir $@) + @cp ../../../build/libaether.a $@ + @ar d $@ $(notdir $(RUNTIME_SRCS:.c=.o)) + +ping_pong: ping_pong.ae $(STD_LIB) @echo "Compiling Aether ping_pong..." @$(AETHER_COMPILER) ping_pong.ae /tmp/ping_pong_aether.c @$(CC) $(CFLAGS) /tmp/ping_pong_aether.c $(RUNTIME_SRCS) -o ping_pong $(LDFLAGS) @echo "Done: ./ping_pong" -counting: counting.ae +counting: counting.ae $(STD_LIB) @echo "Compiling Aether counting..." @$(AETHER_COMPILER) counting.ae /tmp/counting_aether.c @$(CC) $(CFLAGS) /tmp/counting_aether.c $(RUNTIME_SRCS) -o counting $(LDFLAGS) @echo "Done: ./counting" -thread_ring: thread_ring.ae +thread_ring: thread_ring.ae $(STD_LIB) @echo "Compiling Aether thread_ring..." @$(AETHER_COMPILER) thread_ring.ae /tmp/thread_ring_aether.c @$(CC) $(CFLAGS) /tmp/thread_ring_aether.c $(RUNTIME_SRCS) -o thread_ring $(LDFLAGS) @echo "Done: ./thread_ring" -fork_join: fork_join.ae +fork_join: fork_join.ae $(STD_LIB) @echo "Compiling Aether fork_join..." @$(AETHER_COMPILER) fork_join.ae /tmp/fork_join_aether.c @$(CC) $(CFLAGS) /tmp/fork_join_aether.c $(RUNTIME_SRCS) -o fork_join $(LDFLAGS) @echo "Done: ./fork_join" -skynet: skynet.ae +skynet: skynet.ae $(STD_LIB) @echo "Compiling Aether skynet..." @$(AETHER_COMPILER) skynet.ae /tmp/skynet_aether.c @$(CC) $(CFLAGS) /tmp/skynet_aether.c $(RUNTIME_SRCS) -o skynet $(LDFLAGS) @echo "Done: ./skynet" clean: - rm -f ping_pong counting thread_ring fork_join skynet /tmp/*_aether.c* + rm -rf ping_pong counting thread_ring fork_join skynet /tmp/*_aether.c* $(dir $(STD_LIB)) # What the runner records with the results (run_benchmarks.ae). flags: diff --git a/compiler/aether_module.c b/compiler/aether_module.c index 897e1f550..6efa2aa9a 100644 --- a/compiler/aether_module.c +++ b/compiler/aether_module.c @@ -3662,16 +3662,20 @@ static const char* import_written_prefix(ASTNode* imp) { * and `vg.region`, both renamed off "region" by module_assign_namespaces) * doesn't have the first one's rewrite blindly consume every "region.name" * in the body before the second's ever runs — see - * rewrite_qualified_prefix_ambiguous. */ -static void rewrite_import_prefixes(ASTNode* body, ASTNode* scope_ast, - const char* self_name) { - if (!body || !scope_ast) return; - + * rewrite_qualified_prefix_ambiguous. The imports are collected once per + * scope (#3499): collected again for each of a program's top-level nodes, + * the merge grew with the square of the program. */ #define REWRITE_MAX_IMPORTS 64 +typedef struct { const char* writtens[REWRITE_MAX_IMPORTS]; AetherModule* mods[REWRITE_MAX_IMPORTS]; - int n = 0; - for (int i = 0; i < scope_ast->child_count && n < REWRITE_MAX_IMPORTS; i++) { + int n; +} ImportPrefixes; + +static void import_prefixes_collect(ImportPrefixes* ip, ASTNode* scope_ast, + const char* self_name) { + ip->n = 0; + for (int i = 0; scope_ast && i < scope_ast->child_count && ip->n < REWRITE_MAX_IMPORTS; i++) { ASTNode* imp = scope_ast->children[i]; if (!imp || imp->type != AST_IMPORT_STATEMENT || !imp->value) continue; if (imp->annotation && strcmp(imp->annotation, "synthetic") == 0) continue; @@ -3679,31 +3683,42 @@ static void rewrite_import_prefixes(ASTNode* body, ASTNode* scope_ast, const char* ns = module_namespace_of(imp->value); if (strcmp(written, ns) == 0) continue; if (self_name && strcmp(written, module_last_segment(self_name)) == 0) continue; - writtens[n] = written; - mods[n] = module_find(imp->value); - n++; + ip->writtens[ip->n] = written; + ip->mods[ip->n] = module_find(imp->value); + ip->n++; } +} +static void import_prefixes_rewrite(const ImportPrefixes* ip, ASTNode* body) { + if (!body) return; int done[REWRITE_MAX_IMPORTS] = {0}; - for (int i = 0; i < n; i++) { + for (int i = 0; i < ip->n; i++) { if (done[i]) continue; AetherModule* candidates[REWRITE_MAX_IMPORTS]; int ncand = 0; - candidates[ncand++] = mods[i]; + candidates[ncand++] = ip->mods[i]; done[i] = 1; - for (int j = i + 1; j < n; j++) { - if (!done[j] && strcmp(writtens[i], writtens[j]) == 0) { - candidates[ncand++] = mods[j]; + for (int j = i + 1; j < ip->n; j++) { + if (!done[j] && strcmp(ip->writtens[i], ip->writtens[j]) == 0) { + candidates[ncand++] = ip->mods[j]; done[j] = 1; } } if (ncand == 1) { - if (mods[i]) rewrite_qualified_prefix(body, writtens[i], module_namespace_of(mods[i]->name), NULL, 0); + if (ip->mods[i]) rewrite_qualified_prefix(body, ip->writtens[i], module_namespace_of(ip->mods[i]->name), NULL, 0); } else { - rewrite_qualified_prefix_ambiguous(body, writtens[i], candidates, ncand, NULL, 0); + rewrite_qualified_prefix_ambiguous(body, ip->writtens[i], candidates, ncand, NULL, 0); } } +} #undef REWRITE_MAX_IMPORTS + +static void rewrite_import_prefixes(ASTNode* body, ASTNode* scope_ast, + const char* self_name) { + if (!body || !scope_ast) return; + ImportPrefixes ip; + import_prefixes_collect(&ip, scope_ast, self_name); + import_prefixes_rewrite(&ip, body); } /* The registered module whose parsed AST is `mod_ast`, or NULL. */ @@ -4421,10 +4436,12 @@ void module_merge_into_program(ASTNode* program) { /* #2209: the program's own code names its imports by alias or last * segment; point each at the imported module's namespace before any * module body is cloned in beside it. */ + ImportPrefixes prog_imports; + import_prefixes_collect(&prog_imports, program, NULL); for (int i = 0; i < program->child_count; i++) { ASTNode* child = program->children[i]; if (!child || child->type == AST_IMPORT_STATEMENT) continue; - rewrite_import_prefixes(child, program, NULL); + import_prefixes_rewrite(&prog_imports, child); } // Find insertion point: just before AST_MAIN_FUNCTION diff --git a/compiler/analysis/type_inference.c b/compiler/analysis/type_inference.c index e90031a10..e75bc2c16 100644 --- a/compiler/analysis/type_inference.c +++ b/compiler/analysis/type_inference.c @@ -1091,6 +1091,11 @@ void collect_expression_constraints(ASTNode* node, InferenceContext* ctx) { if (array_type && array_type->kind == TYPE_ARRAY && array_type->element_type) { free_type(node->node_type); node->node_type = clone_type(array_type->element_type); + } else if (array_type && array_type->kind == TYPE_STRING) { + /* #3479: `s[i]` reads one byte of the string, as an + * `int` from 0 to 255. */ + free_type(node->node_type); + node->node_type = create_type(TYPE_INT); } } break; diff --git a/compiler/analysis/typechecker.c b/compiler/analysis/typechecker.c index d159dbe3b..0c0386b72 100644 --- a/compiler/analysis/typechecker.c +++ b/compiler/analysis/typechecker.c @@ -6367,6 +6367,8 @@ static void order_const_declarations(ASTNode* program) { } static void tc_clauses_reset(void); +static ASTNode* tc_first_def(ASTNode* program, const char* name); +static ASTNode* tc_first_extern(ASTNode* program, const char* name, const char* merged); /* ---- qualified type names: `mod.T`, `*mod.T` --------------------------- * @@ -9855,13 +9857,7 @@ static ASTNode* call_extern_decl(ASTNode* program, const char* name) { for (size_t i = 0; i < n; i++) merged[i] = name[i] == '.' ? '_' : name[i]; merged[n] = '\0'; } - for (int c = 0; c < program->child_count; c++) { - ASTNode* e = program->children[c]; - if (!e || e->type != AST_EXTERN_FUNCTION || !e->value) continue; - if (!strcmp(e->value, name)) return e; - if (dotted && (e->is_imported || e->origin_module) && !strcmp(e->value, merged)) return e; - } - return NULL; + return tc_first_extern(program, name, dotted ? merged : NULL); } /* The declaration a call is checked against: an Aether function, or an @@ -11811,15 +11807,12 @@ static const char* effect_call_capability(const char* ns) { return NULL; } +/* The first definition named `name`, from the checker's index (#3499): a + * scan of the program per call made checking grow with the square of its + * size. */ static ASTNode* effect_find_func(ASTNode* program, const char* name) { if (!program || !name) return NULL; - for (int i = 0; i < program->child_count; i++) { - ASTNode* c = program->children[i]; - if ((c->type == AST_FUNCTION_DEFINITION || c->type == AST_BUILDER_FUNCTION) && - c->value && strcmp(c->value, name) == 0) - return c; - } - return NULL; + return tc_first_def(program, name); } /* The definition a call names, or NULL when the walk cannot see one (a @@ -17842,6 +17835,16 @@ int typecheck_expression(ASTNode* expr, SymbolTable* table) { if (expr->node_type) free_type(expr->node_type); expr->node_type = clone_type(arr_type->element_type); } + /* #3479: `s[i]` on a string reads one byte as an `int` + * from 0 to 255, the type codegen always gave a local bound + * to it, now the checker's: the local was left to codegen's + * default with a warning. Not a `byte`, which would refuse + * `uint32 u = s[i]` and wrap `c = s[i]; c = c - 100`. */ + if (arr_type && arr_type->kind == TYPE_STRING && + (!expr->node_type || expr->node_type->kind != TYPE_INT)) { + if (expr->node_type) free_type(expr->node_type); + expr->node_type = create_type(TYPE_INT); + } // Indexing a bare `ptr` (void*, element_type == NULL) has no // element type: it lowers to a C index on `void*` and leaks @@ -19361,6 +19364,10 @@ typedef struct { } TcClauses; static StrMap g_tc_clauses; +/* #3499: the externs of the program by name, the first in program order, + * and of those a module brought, each as its position + 1. */ +static StrMap g_tc_externs; +static StrMap g_tc_imported_externs; static ASTNode* g_tc_clauses_program = NULL; static int g_tc_clauses_child_count = -1; @@ -19370,38 +19377,72 @@ static void tc_clauses_reset(void) { if (tc) { free(tc->nodes); free(tc); } } strmap_free(&g_tc_clauses); + strmap_free(&g_tc_externs); + strmap_free(&g_tc_imported_externs); g_tc_clauses_program = NULL; g_tc_clauses_child_count = -1; } +/* Index `program`'s definitions and externs, once per program shape: the + * checker adds top-level nodes (derived functions, a clause set's join) but + * renames and frees none, and an addition changes the count. */ +static void tc_decls_index(ASTNode* program) { + if (g_tc_clauses_program == program && g_tc_clauses_child_count == program->child_count) + return; + tc_clauses_reset(); + for (int i = 0; i < program->child_count; i++) { + ASTNode* c = program->children[i]; + if (c && c->value && c->type == AST_EXTERN_FUNCTION) { + void* at = (void*)(intptr_t)(i + 1); + if (!strmap_has(&g_tc_externs, c->value)) strmap_put(&g_tc_externs, c->value, at); + if ((c->is_imported || c->origin_module) && + !strmap_has(&g_tc_imported_externs, c->value)) + strmap_put(&g_tc_imported_externs, c->value, at); + continue; + } + if (!c || !c->value || + (c->type != AST_FUNCTION_DEFINITION && c->type != AST_BUILDER_FUNCTION)) continue; + TcClauses* tc = strmap_get(&g_tc_clauses, c->value); + if (!tc) { + tc = calloc(1, sizeof(TcClauses)); + if (!tc) continue; + strmap_put(&g_tc_clauses, c->value, tc); + } + if (tc->count == tc->cap) { + int cap = tc->cap ? tc->cap * 2 : 2; + ASTNode** nn = realloc(tc->nodes, sizeof(ASTNode*) * (size_t)cap); + if (!nn) continue; + tc->nodes = nn; + tc->cap = cap; + } + tc->nodes[tc->count++] = c; + } + g_tc_clauses_program = program; + g_tc_clauses_child_count = program->child_count; +} + +/* #3499: the first function or builder named `name` in `program`. */ +static ASTNode* tc_first_def(ASTNode* program, const char* name) { + tc_decls_index(program); + const TcClauses* tc = strmap_get(&g_tc_clauses, name); + return (tc && tc->count > 0) ? tc->nodes[0] : NULL; +} + +/* #3499: the first extern in `program` named `name`, or, when `merged` is + * given, a module's extern named `merged`, whichever comes first. */ +static ASTNode* tc_first_extern(ASTNode* program, const char* name, const char* merged) { + tc_decls_index(program); + intptr_t a = (intptr_t)strmap_get(&g_tc_externs, name); + intptr_t b = merged ? (intptr_t)strmap_get(&g_tc_imported_externs, merged) : 0; + intptr_t at = (a && b) ? (a < b ? a : b) : (a ? a : b); + return at ? program->children[at - 1] : NULL; +} + /* The clauses of the set `fn` is one of, or NULL for a single definition. */ static const TcClauses* tc_clauses_of(ASTNode* fn) { ASTNode* program = g_typecheck_program; if (!program || !fn || !fn->value) return NULL; - if (g_tc_clauses_program != program || g_tc_clauses_child_count != program->child_count) { - tc_clauses_reset(); - for (int i = 0; i < program->child_count; i++) { - ASTNode* c = program->children[i]; - if (!c || !c->value || - (c->type != AST_FUNCTION_DEFINITION && c->type != AST_BUILDER_FUNCTION)) continue; - TcClauses* tc = strmap_get(&g_tc_clauses, c->value); - if (!tc) { - tc = calloc(1, sizeof(TcClauses)); - if (!tc) continue; - strmap_put(&g_tc_clauses, c->value, tc); - } - if (tc->count == tc->cap) { - int cap = tc->cap ? tc->cap * 2 : 2; - ASTNode** nn = realloc(tc->nodes, sizeof(ASTNode*) * (size_t)cap); - if (!nn) continue; - tc->nodes = nn; - tc->cap = cap; - } - tc->nodes[tc->count++] = c; - } - g_tc_clauses_program = program; - g_tc_clauses_child_count = program->child_count; - } + tc_decls_index(program); const TcClauses* tc = strmap_get(&g_tc_clauses, fn->value); if (!tc || tc->count < 2) return NULL; for (int i = 0; i < tc->count; i++) diff --git a/compiler/codegen/codegen.c b/compiler/codegen/codegen.c index b87a90661..c3857b668 100644 --- a/compiler/codegen/codegen.c +++ b/compiler/codegen/codegen.c @@ -833,16 +833,11 @@ Type* lookup_fnptr_local(CodeGenerator* gen, const char* name) { * on a `void*`), which C rejects. NULL when `name` is not such a global. */ Type* lookup_fnptr_global(CodeGenerator* gen, const char* name) { if (!gen || !gen->program || !name) return NULL; - for (int i = 0; i < gen->program->child_count; i++) { - ASTNode* c = gen->program->children[i]; - if (c && c->type == AST_EXPORT_STATEMENT && c->child_count > 0) c = c->children[0]; - if (!c || c->type != AST_CONST_DECLARATION || !c->value) continue; - if (!(c->annotation && strcmp(c->annotation, "global_var") == 0) && - !is_fnptr_const_decl(c)) continue; - if (strcmp(c->value, name) != 0) continue; - return is_fnptr_type(c->node_type) ? c->node_type : NULL; - } - return NULL; + /* From the program index (#3499): asked for every call, a scan of the + * program made codegen grow with the square of its size. */ + ProgramIndex* ix = program_index(gen->program); + ASTNode* c = ix ? strmap_get(&ix->fn_globals, name) : NULL; + return (c && is_fnptr_type(c->node_type)) ? c->node_type : NULL; } /* #2648: a module-level `const` of a typed fn-pointer type. It holds a @@ -1308,6 +1303,13 @@ static void program_index_build(ProgramIndex* ix, ASTNode* program) { for (int i = 0; i < program->child_count; i++) { ASTNode* c = program->children[i]; if (!c) continue; + /* #3499: the module `var`s and fn-pointer `const`s a call may go + * through (lookup_fnptr_global), the first of each name. */ + ASTNode* g = (c->type == AST_EXPORT_STATEMENT && c->child_count > 0) ? c->children[0] : c; + if (g && g->type == AST_CONST_DECLARATION && g->value && + ((g->annotation && strcmp(g->annotation, "global_var") == 0) || is_fnptr_const_decl(g)) && + !strmap_has(&ix->fn_globals, g->value)) + strmap_put(&ix->fn_globals, g->value, g); if ((c->type == AST_FUNCTION_DEFINITION || c->type == AST_BUILDER_FUNCTION) && c->value) { DefClauses* dc = strmap_get(&ix->defs, c->value); if (!dc) { @@ -1391,6 +1393,7 @@ void program_index_reset(void) { strmap_free(&ix->clause_scopes); strmap_free(&ix->def_children); strmap_free(&ix->def_sets); + strmap_free(&ix->fn_globals); ix->program = NULL; ix->child_count = 0; ix->main_fn = NULL; @@ -6374,30 +6377,22 @@ static void mangle_keyword_value_idents(ASTNode* program) { * a program can see. A write to a module variable or an actor's state is * not a local, and keeps its name. Both walks visit a node's children before * the node, so a binding's initializer comes before the binding. */ -typedef struct { - const char** names; /* interned (cg_intern), never freed one by one */ - int count, cap; -} NameSet; +/* A set of names in insertion order, hashed (#3499): a list scanned per + * reference made this pass grow with the square of the program, since + * every reference is checked against every function. */ +typedef StrMap NameSet; static int nameset_has(const NameSet* s, const char* name) { - for (int i = 0; name && i < s->count; i++) - if (strcmp(s->names[i], name) == 0) return 1; - return 0; + return name && strmap_has(s, name); } static void nameset_add(NameSet* s, const char* name) { - if (!name || !name[0] || nameset_has(s, name)) return; - if (s->count == s->cap) { - s->cap = s->cap ? s->cap * 2 : 16; - s->names = (const char**)aether_xrealloc((void*)s->names, (size_t)s->cap * sizeof(*s->names)); - } - s->names[s->count++] = cg_intern(name); + if (!name || !name[0] || strmap_has(s, name)) return; + strmap_put(s, name, NULL); } static void nameset_free(NameSet* s) { - free((void*)s->names); - s->names = NULL; - s->count = s->cap = 0; + strmap_free(s); } static int is_value_name_node(const ASTNode* n) { @@ -6416,33 +6411,23 @@ static int is_binding_node(const ASTNode* n) { * as the extern declarations below emit them). */ typedef struct { NameSet names; /* every function and extern, by its Aether name */ - ASTNode** externs; - int extern_count, extern_cap; + StrMap externs; /* the first extern of each name */ } ShadowFns; static void shadow_fns_add_extern(ShadowFns* f, ASTNode* ext) { if (!ext || !ext->value) return; nameset_add(&f->names, ext->value); - if (f->extern_count == f->extern_cap) { - f->extern_cap = f->extern_cap ? f->extern_cap * 2 : 32; - f->externs = (ASTNode**)aether_xrealloc((void*)f->externs, - (size_t)f->extern_cap * sizeof(*f->externs)); - } - f->externs[f->extern_count++] = ext; + if (!strmap_has(&f->externs, ext->value)) strmap_put(&f->externs, ext->value, ext); } static void shadow_fns_free(ShadowFns* f) { nameset_free(&f->names); - free((void*)f->externs); - f->externs = NULL; - f->extern_count = f->extern_cap = 0; + strmap_free(&f->externs); } /* The extern named `name`, or NULL. */ static ASTNode* shadow_fns_extern(const ShadowFns* f, const char* name) { - for (int i = 0; name && i < f->extern_count; i++) - if (strcmp(f->externs[i]->value, name) == 0) return f->externs[i]; - return NULL; + return name ? (ASTNode*)strmap_get(&f->externs, name) : NULL; } /* Every C spelling a reference to `v` could have that names one of `fns`, @@ -6512,8 +6497,8 @@ static void rename_locals_shadowing_functions_in(ASTNode* scope, const ShadowFns const NameSet* fixed) { LocalShadowScan s = {fns, fixed, {0}, {0}, {0}}; scan_local_shadows(scope, &s); - for (int i = 0; i < s.renamed.count; i++) { - const char* from = s.renamed.names[i]; + for (int i = 0; i < strmap_count(&s.renamed); i++) { + const char* from = strmap_key_at(&s.renamed, i); char* to = prefixed_name("ae_local_", from); int active = 0; rename_local_after_binding(scope, from, to, &active); @@ -6527,7 +6512,7 @@ static void rename_locals_shadowing_functions_in(ASTNode* scope, const ShadowFns static void rename_locals_shadowing_functions(ASTNode* program) { if (!program) return; NameSet globals = {0}; - ShadowFns fns = {{0}, NULL, 0, 0}; + ShadowFns fns = {{0}, {0}}; for (int i = 0; i < program->child_count; i++) { ASTNode* c = program->children[i]; if (!c || !c->value) continue; @@ -6545,7 +6530,7 @@ static void rename_locals_shadowing_functions(ASTNode* program) { } } } - for (int i = 0; fns.names.count > 0 && i < program->child_count; i++) { + for (int i = 0; strmap_count(&fns.names) > 0 && i < program->child_count; i++) { ASTNode* c = program->children[i]; if (!c) continue; if (c->type == AST_FUNCTION_DEFINITION || c->type == AST_MAIN_FUNCTION || @@ -6553,7 +6538,7 @@ static void rename_locals_shadowing_functions(ASTNode* program) { rename_locals_shadowing_functions_in(c, &fns, &globals); } else if (c->type == AST_ACTOR_DEFINITION) { NameSet fixed = {0}; - for (int g = 0; g < globals.count; g++) nameset_add(&fixed, globals.names[g]); + for (int g = 0; g < strmap_count(&globals); g++) nameset_add(&fixed, strmap_key_at(&globals, g)); for (int k = 0; k < c->child_count; k++) { ASTNode* st = c->children[k]; if (st && st->type == AST_STATE_DECLARATION && st->value) @@ -6689,7 +6674,7 @@ static void rename_scoped_bindings(ASTNode* program) { } else if (c->type == AST_ACTOR_DEFINITION) { /* An actor's state is `self->name`, no local. */ NameSet fixed = {0}; - for (int g = 0; g < globals.count; g++) nameset_add(&fixed, globals.names[g]); + for (int g = 0; g < strmap_count(&globals); g++) nameset_add(&fixed, strmap_key_at(&globals, g)); for (int k = 0; k < c->child_count; k++) { ASTNode* st = c->children[k]; if (st && st->type == AST_STATE_DECLARATION && st->value) @@ -9830,6 +9815,17 @@ void generate_program(CodeGenerator* gen, ASTNode* program) { print_line(gen, " if (!s) return \"\";"); print_line(gen, " return aether_string_data(s);"); print_line(gen, "}"); + /* #3479: `s[i]` is the string's byte i as an int from 0 to 255, read + * unsigned (a plain char is signed on some targets). Index `length` is + * the terminator, 0, as a C-style scan (`while s[j] != 0`) reads it; one + * below 0 or past it panics, as a slice index outside its length does: + * it read past the string. */ + print_line(gen, "static inline int _aether_str_at(const void* s, int64_t i, const char* file, int line) {"); + print_line(gen, " uint64_t n = s ? (uint64_t)aether_string_length(s) : 0;"); + print_line(gen, " if ((uint64_t)i > n) aether_string_fail_index(i, n, file, line);"); + print_line(gen, " if ((uint64_t)i == n) return 0;"); + print_line(gen, " return (int)(unsigned char)_aether_safe_str(s)[i];"); + print_line(gen, "}"); /* Owned-print helpers: a heap-producing CALL in bare print/println * argument position (`println(string.concat(a, b))`) is a temporary * nothing else owns; these print it and free it in one composable diff --git a/compiler/codegen/codegen_expr.c b/compiler/codegen/codegen_expr.c index 7c39f3d9c..5e10e4678 100644 --- a/compiler/codegen/codegen_expr.c +++ b/compiler/codegen/codegen_expr.c @@ -11981,6 +11981,18 @@ void generate_expression(CodeGenerator* gen, ASTNode* expr) { } int str_base = base && base->node_type && base->node_type->kind == TYPE_STRING; + /* #3479: a read of a string's byte, checked and unsigned + * (a store through `s[i]` is refused by the checker). */ + if (str_base && !gen->generating_lvalue) { + fprintf(gen->output, "_aether_str_at("); + generate_expression(gen, base); + fprintf(gen->output, ", (int64_t)("); + generate_read(gen, expr->children[1]); + fprintf(gen->output, "), "); + emit_slice_diag_file(gen, expr); + fprintf(gen->output, ", %d)", expr->line); + break; + } /* #3113: an index into a fixed-size `T[N]` is checked * against N, as a slice's is against its length: a local, * a field, a parameter, an actor's state or a `const` diff --git a/compiler/codegen/codegen_internal.h b/compiler/codegen/codegen_internal.h index 0f7fe7c9a..7659a0402 100644 --- a/compiler/codegen/codegen_internal.h +++ b/compiler/codegen/codegen_internal.h @@ -820,6 +820,8 @@ typedef struct { * parameter) -> the definition (#2897) */ StrMap def_sets; /* `` of a definition -> the DefClauses it * is one of (#2897, fn_def_clause_set) */ + StrMap fn_globals; /* name -> the first module `var`/`const` that may + * hold a fn pointer (#3499, lookup_fnptr_global) */ } ProgramIndex; ProgramIndex* program_index(ASTNode* program); diff --git a/compiler/codegen/codegen_stmt.c b/compiler/codegen/codegen_stmt.c index b53950dc5..646cf8a4d 100644 --- a/compiler/codegen/codegen_stmt.c +++ b/compiler/codegen/codegen_stmt.c @@ -12446,6 +12446,21 @@ static void generate_statement_body(CodeGenerator* gen, ASTNode* stmt) { } // In main(), all returns go through main_exit so scheduler_wait() always runs if (gen->in_main_function) { + /* The value is computed first, while the locals it reads + * are live and inside the try frames it was written in; + * then the defers run and the frames are popped, as any + * other function's return does. The defers ran first: a + * value read a released local (`return string.length(s)` + * after `s`'s release), and once a panic in it was caught, + * main went on with its defers run, and ran them again. */ + int main_value = stmt->child_count > 0 && stmt->children[0] && + stmt->children[0]->type != AST_PRINT_STATEMENT; + if (main_value) { + print_indent(gen); + fprintf(gen->output, "main_exit_ret = "); + generate_expression(gen, stmt->children[0]); + fprintf(gen->output, ";\n"); + } if (gen->defer_count > 0) { emit_all_defers(gen); } @@ -12454,15 +12469,13 @@ static void generate_statement_body(CodeGenerator* gen, ASTNode* stmt) { * main() doesn't leak the panic frame. goto and * return are both non-local exits as far as the * try-frame stack is concerned. */ - emit_try_pops_for_nonlocal_exit(gen); - print_indent(gen); - if (stmt->child_count > 0 && stmt->children[0] && - stmt->children[0]->type != AST_PRINT_STATEMENT) { - fprintf(gen->output, "main_exit_ret = "); - generate_expression(gen, stmt->children[0]); - fprintf(gen->output, "; goto main_exit;\n"); + if (main_value) { + emit_try_pops_for_nonlocal_exit(gen); + print_line(gen, "goto main_exit;"); gen->main_exit_targeted = 1; } else { + emit_try_pops_for_nonlocal_exit(gen); + print_indent(gen); if (stmt->child_count > 0 && stmt->children[0] && stmt->children[0]->type == AST_PRINT_STATEMENT) { generate_statement(gen, stmt->children[0]); @@ -12656,10 +12669,11 @@ static void generate_statement_body(CodeGenerator* gen, ASTNode* stmt) { print_line(gen, "return;"); } else if (stmt->child_count > 1) { // Multi-value return: return a, b → return (_tuple_X_Y){a, b} - /* Issue #501: drain try frames first; the - * print_indent below is for the `return ...` - * line, which lands after the drained pops. */ - emit_try_pops_for_nonlocal_exit(gen); + /* Issue #501: drain try frames. Inside a `try` the + * tuple is built first, into `_ae_tr`, and the frames + * popped after, so a panic while building it is the + * try's to catch. */ + int in_try = gen->try_frame_depth > 0; print_indent(gen); // Use the function's known return type if it's a tuple // (avoids UNKNOWN types from unresolved identifiers) @@ -12685,18 +12699,42 @@ static void generate_statement_body(CodeGenerator* gen, ASTNode* stmt) { /* #2478: the positions are evaluated left to right. */ order_prelude_begin(gen, stmt->children, stmt->child_count, NULL); int arrays = tuple_return_has_array(gen, stmt); - if (arrays) fprintf(gen->output, "{ %s _ae_tr = (%s){", tname, tname); + if (arrays || in_try) fprintf(gen->output, "{ %s _ae_tr = (%s){", tname, tname); else fprintf(gen->output, "return (%s){", tname); for (int j = 0; j < stmt->child_count; j++) { if (j > 0) fprintf(gen->output, ", "); emit_tuple_return_position(gen, stmt->children[j], j); } fprintf(gen->output, "};\n"); - if (arrays) { - emit_tuple_array_fills(gen, stmt, "_ae_tr"); + if (arrays || in_try) { + if (arrays) emit_tuple_array_fills(gen, stmt, "_ae_tr"); + emit_try_pops_for_nonlocal_exit(gen); print_line(gen, "return _ae_tr; }"); } if (owned) free_type(tuple); + } else if (gen->try_frame_depth > 0 && stmt->child_count > 0) { + /* Inside a `try`: the value is computed inside its frames + * and they are popped after, as the defer-aware path + * does. Popped first, a panic while computing it (an + * index out of range, a call that panics) escaped the + * `try` written around it and ended the process. */ + print_line(gen, "{"); + gen->indent_level++; + Type* fn_ret = gen->current_func_return_type; + Type* ret_type = (fn_ret && fn_ret->kind != TYPE_VOID && + fn_ret->kind != TYPE_UNKNOWN) + ? fn_ret : stmt->children[0]->node_type; + const char* ret_c_type = + (ret_type && ret_type->kind != TYPE_VOID && ret_type->kind != TYPE_UNKNOWN) + ? c_result_type(ret_type) : "int"; + print_indent(gen); + fprintf(gen->output, "%s _builder_ret = ", ret_c_type); + emit_return_value(gen, stmt); + fprintf(gen->output, ";\n"); + emit_try_pops_for_nonlocal_exit(gen); + print_line(gen, "return _builder_ret;"); + gen->indent_level--; + print_line(gen, "}"); } else { /* Issue #501: drain try frames before the * print_indent for the `return ;` line. */ diff --git a/compiler/parser/parser.c b/compiler/parser/parser.c index 051a3cfff..6f2b6d3ad 100644 --- a/compiler/parser/parser.c +++ b/compiler/parser/parser.c @@ -1872,24 +1872,33 @@ static ASTNode* parse_primary_expression_inner(Parser* parser) { // chain of any length is nested two-armed if-expressions and // types as they do, the last `else` still required. Token* t = advance_token(parser); // consume 'if' + /* #3478: the `{` after the condition opens the then arm, so a + * call that ends the condition takes no trailing block, as in an + * `if` statement or a `match` (the in_condition guard). The arms + * sit inside braces, where a call may take one again. */ + int saved_in_condition = parser->in_condition; + parser->in_condition = 1; ASTNode* cond = parse_expression(parser); - if (!cond) return NULL; - if (!expect_token(parser, TOKEN_LEFT_BRACE)) return NULL; - ASTNode* then_expr = parse_expression(parser); - if (!then_expr) return NULL; - if (!expect_token(parser, TOKEN_RIGHT_BRACE)) return NULL; - if (!expect_token(parser, TOKEN_ELSE)) return NULL; + parser->in_condition = 0; + ASTNode* then_expr = NULL; ASTNode* else_expr = NULL; - Token* after_else = peek_token(parser); - if (after_else && after_else->type == TOKEN_IF) { - else_expr = parse_primary_expression(parser); - if (!else_expr) return NULL; - } else { - if (!expect_token(parser, TOKEN_LEFT_BRACE)) return NULL; - else_expr = parse_expression(parser); - if (!else_expr) return NULL; - if (!expect_token(parser, TOKEN_RIGHT_BRACE)) return NULL; + int ok = cond && expect_token(parser, TOKEN_LEFT_BRACE) && + (then_expr = parse_expression(parser)) != NULL && + expect_token(parser, TOKEN_RIGHT_BRACE) && + expect_token(parser, TOKEN_ELSE); + if (ok) { + Token* after_else = peek_token(parser); + if (after_else && after_else->type == TOKEN_IF) { + else_expr = parse_primary_expression(parser); + ok = else_expr != NULL; + } else { + ok = expect_token(parser, TOKEN_LEFT_BRACE) && + (else_expr = parse_expression(parser)) != NULL && + expect_token(parser, TOKEN_RIGHT_BRACE); + } } + parser->in_condition = saved_in_condition; + if (!ok) return NULL; ASTNode* if_expr = create_ast_node(AST_IF_EXPRESSION, NULL, t->line, t->column); if_expr->node_type = create_type(TYPE_UNKNOWN); @@ -3044,6 +3053,13 @@ static ASTNode* parse_postfix_expression(Parser* parser) { } // Parse arguments + /* Inside the parentheses a `{` cannot open the body of an if, a + * while or a match whose condition the call is part of, so the + * arguments parse as they do anywhere: a struct literal, a call + * with a trailing block (#3478 review; the `if` expression and the + * statements alike). */ + int saved_args_in_condition = parser->in_condition; + parser->in_condition = 0; if (!match_token(parser, TOKEN_RIGHT_PAREN)) { /* #2998: the `(` or `,` an argument has to follow, for the * error when it is missing: `x = f(` used to compile. */ @@ -3064,6 +3080,7 @@ static ASTNode* parse_postfix_expression(Parser* parser) { if (peek_token(parser) && peek_token(parser)->type == TOKEN_RIGHT_PAREN) advance_token(parser); /* as below */ + parser->in_condition = saved_args_in_condition; free_ast_node(func_call); return NULL; } @@ -3085,6 +3102,7 @@ static ASTNode* parse_postfix_expression(Parser* parser) { if (peek_token(parser) && peek_token(parser)->type == TOKEN_RIGHT_PAREN) advance_token(parser); + parser->in_condition = saved_args_in_condition; free_ast_node(func_call); return NULL; } @@ -3094,10 +3112,12 @@ static ASTNode* parse_postfix_expression(Parser* parser) { } while (match_token(parser, TOKEN_COMMA)); if (!expect_token(parser, TOKEN_RIGHT_PAREN)) { + parser->in_condition = saved_args_in_condition; free_ast_node(func_call); return NULL; } } + parser->in_condition = saved_args_in_condition; // Capture the closing paren's line for the trailing-block // line check below. Both arg-list paths (empty via diff --git a/docs/emit-lib.md b/docs/emit-lib.md index 17be4eeb4..5234edc92 100644 --- a/docs/emit-lib.md +++ b/docs/emit-lib.md @@ -596,13 +596,14 @@ worked Python round-trip, that helper included. ## Per-call resource caps Hosts loading untrusted Aether scripts bound the script's -resource use via two TLS-backed knobs in `include/libaether.h`: +resource use via two knobs in `include/libaether.h`, a process-wide memory +cap and a per-thread deadline: ```c #include aether_set_memory_cap(64 * 1024 * 1024); // 64 MiB ceiling -aether_set_call_deadline(50); // 50 ms wall-clock +aether_set_call_deadline(50); // this thread's deadline: 50 ms from now call_into_aether(); // returns inside both bounds @@ -645,7 +646,12 @@ process, inside an export or not: a message the scheduler has no memory to queue behind an actor's earlier messages, since it has no place that keeps their order. -**Wall-clock deadline.** Per-thread monotonic (`CLOCK_MONOTONIC`). +**Wall-clock deadline.** One instant per thread, on the monotonic clock +(`CLOCK_MONOTONIC`): `aether_set_call_deadline(ms)` sets it to `ms` from the +moment it is called. It is not a budget each call gets: every call the thread +makes after that instant is cut at its first safe point, until the host sets +a fresh deadline or `0`. A host that wants each call bounded sets the +deadline before each one. The codegen emits a safe point, `if (aether_caps_armed && aether_exec_poll()) { __aether_abort_call(); break; }`, at every `for` / `while` loop head under `--emit=lib` not under `--emit=exe` (zero overhead on diff --git a/docs/language-reference.md b/docs/language-reference.md index 8cb8447ad..550dcda1e 100644 --- a/docs/language-reference.md +++ b/docs/language-reference.md @@ -413,7 +413,7 @@ main() { - **`s[i]` is bounds-checked.** An index outside `0 ..< s.len` is a runtime panic naming the source line, the index and the length (`slice index 5 out of range for length 3`). It is an ordinary panic: `try` catches it, and an actor step that trips it dies cleanly. A negative index is out of range, never a read before the buffer. An index into a fixed-size `T[N]` array is checked the same way, against `N` ([Array Types](#array-types)). - **`s[lo..hi]`** is the half-open sub-slice `[lo, hi)`, checked the same way (`lo <= hi <= s.len`). `s[lo..]` runs to the end, `s[..hi]` from the start, `s[..]` is the whole thing. The base may be a slice or a `T[N]` array; the result is always a `T[]` over the *same* elements, so a write through it is visible through the original. - **A `T[N]` array converts to `T[]` implicitly** wherever a slice is expected: a parameter, a `T[]`-typed binding or field, a `return` from a `-> T[]` function. The length travels with it. `null`, and the empty literal `[]`, convert to the empty slice: `.len` is `0`, it views no storage, and it compares equal to `null`. So `s == []` is `s == null` (it asks whether `s` views any storage; test `s.len == 0` for emptiness), a C function's `T[]` parameter takes `[]` as the null pointer, and an if-expression or a `match` given to a `T[]` converts each arm on its own. A module-level `var` or `const` of type `T[]` takes either as its initial value. -- **A string is not a slice.** Passing one where a `T[]` is expected (a parameter, a binding, a `return`) is a type error; `string.bytes(s)` is the bounded, read-only `byte[]` view of a string's bytes, and it decays to their pointer at a C extern like any slice. Reading it costs what any slice's read does; a store through it (an element store, `+=`, `++`), or through any view made from it (a sub-slice, a pattern's tail), in this function or one it is given to, panics with "a write through a read-only view of a string", and one the checker can see through a local bound to it is a compile error (E0200) (#3025). A string's own bytes are read-only too: `s[i]` reads one, and a write (`s[i] = v`, `s[i] += v`, `s[i]++`) is a compile error (E0200). Build the new text as a new string, or work on the bytes in a `byte[N]` array of your own. +- **A string is not a slice.** Passing one where a `T[]` is expected (a parameter, a binding, a `return`) is a type error; `string.bytes(s)` is the bounded, read-only `byte[]` view of a string's bytes, and it decays to their pointer at a C extern like any slice. Reading it costs what any slice's read does; a store through it (an element store, `+=`, `++`), or through any view made from it (a sub-slice, a pattern's tail), in this function or one it is given to, panics with "a write through a read-only view of a string", and one the checker can see through a local bound to it is a compile error (E0200) (#3025). A string's own bytes are read-only too: `s[i]` reads one, an `int` from 0 to 255 (the value `string.bytes(s)[i]` has), checked against the string's length as a slice index is against its own: `s[length]` is the terminator, 0, and an index below 0 or past it is the panic `string index 5 out of range for length 3`. A write (`s[i] = v`, `s[i] += v`, `s[i]++`) is a compile error (E0200). Build the new text as a new string, or work on the bytes in a `byte[N]` array of your own. - **A slice decays to its pointer** wherever a `ptr` is expected, exactly as a `T[N]` array does: a `ptr` parameter, a C extern's parameter (including one declared `T[]`), a comparison against `null`, and pointer arithmetic (`s + n` is a `ptr` to element `n`). `s as ptr` spells the same conversion explicitly, for a `ptr` return or binding. The length is dropped at that boundary; nothing else about the extern ABI changes. - **`make([]T, n)`** allocates `n` zeroed elements and returns them as a bounded slice. The local that holds it releases it when it goes out of scope ([Slice ownership](#slice-ownership)), and `free(s)` releases it early, and first what the elements hold: each element is a place a store into it takes a reference for (a string, a closure, a struct that holds one), so the free gives each back, as a struct's destroy does its fields (#3159). The elements of C memory viewed as a slice hold no reference, and its free releases only the memory. Only the slice that holds such a buffer, or a view of a raw pointer the program owns from its start (`(p as T[])[0..n]`), may be freed: a view owns nothing. `free` of a sub-slice (`b[2..4]`, `b[1..]`, a list pattern's tail), of an array viewed as a slice, of `string.bytes(s)`, or of an array `T[N]`, is a compile error where the checker sees it (a local bound once to one, or the expression itself), and a named panic at run time anywhere else (`free of a view of elements another slice or an array holds, which owns nothing`). Before, the allocator was given a pointer it never handed out. In a module that defines a `free` of its own (`bignum.free`, `json.free`), `free(s)` of a slice is still this release, unless the module's `free` itself takes a slice: the module's function would only see the slice's pointer (#3116). - **`T[][]` is a slice of slices**, and `T[][][]` one deeper: each further `[]` wraps the type once more, in any position a type is written (a parameter, a return type, a field, a typed local, a function type). `make([]int[], n)` builds one; `g[i] = row` stores a row, `g[i][j]` reads an element and `g[i].len` is a row's length. The outer slice owns its rows: a store into `g[i]` releases the row it replaces, and releasing the outer slice (its owner's scope exit, or `free(g)`) releases every row first; `free(g[i])` releases a row early and leaves it empty. A row read out (`r = g[i]`) is a view of it. Only the empty `[]` repeats; there is no `T[][3]`. @@ -3722,9 +3722,11 @@ each sender's messages still arrive in the order they were sent (#3241). ### Reply Statement -The `reply` statement sends a response back to the waiting `?` caller. Omitting -`reply` in a handler that was invoked via `?` causes the caller to time out -(`err` is `"ask timed out after 5000 ms"` in the two-name form above). +The `reply` statement sends a response back to the waiting `?` caller. A +handler invoked via `?` that returns without a `reply` ends the ask at once: +`err` is `"the handler returned without replying"` in the two-name form +above, and the caller does not wait out its timeout. The timeout +(`"ask timed out after 5000 ms"`) is for a handler that has not returned. Actors respond using the `reply` statement: diff --git a/docs/stdlib-reference.md b/docs/stdlib-reference.md index b0bc55562..ec5b5c138 100644 --- a/docs/stdlib-reference.md +++ b/docs/stdlib-reference.md @@ -1356,7 +1356,7 @@ if kind == fs.KIND_OK { ## In-process script gateway (`std.http.script_gateway`) -CGI-style ergonomics, one `.ae` script file per route, without paying the per-request fork/exec cost. The script is pre-compiled with `aetherc --emit=lib --with=net script.ae -o script.so` to a shared library; the host server `dlopen()`s it once at mount time and dispatches matched requests via a direct indirect call. Empirically ~50× faster than the equivalent subprocess-spawn dispatch. +CGI-style ergonomics, one `.ae` script file per route, without paying the per-request fork/exec cost. The script is pre-compiled with `ae build --emit=lib --shared-runtime --with=net script.ae -o script.so` to a shared library; the host server `dlopen()`s it once at mount time and dispatches matched requests via a direct indirect call. Empirically ~50× faster than the equivalent subprocess-spawn dispatch. ### Script shape @@ -1373,10 +1373,10 @@ import std.http } ``` -Build it as a shared library, `--with=net` is required because `std.http` is the `net` capability and `--emit=lib` is capability-empty by default: +Build it as a shared library on the shared runtime (below). `--with=net` is required because `std.http` is the `net` capability and `--emit=lib` is capability-empty by default: ```sh -aetherc --emit=lib --with=net greeting.ae -o /var/aether/scripts/greeting.so +ae build --emit=lib --shared-runtime --with=net greeting.ae -o /var/aether/scripts/greeting.so ``` ### Mount in the host @@ -1398,14 +1398,20 @@ main() { Now `GET /greet/anything` runs `aether_script_handle` from `greeting.so` directly on the connection thread. -On Linux, FreeBSD and macOS a script built the default way carries a runtime of its own and runs on it (#3481); built with `--shared-runtime`, it shares the host's. On Windows the script is a DLL loaded with `LoadLibrary`, which cannot bind to the host executable's runtime, so the host and each script are built on the shared runtime (`aether.dll`) and run on it together: +The script and the host run on one runtime: the script fills request and response objects the host's runtime frees. A script built the default way (`ae build --emit=lib`) carries a runtime of its own and keeps it (#3481), so it is refused at mount on every platform with `KIND_IO` and a message naming `--shared-runtime` (#3501). Build scripts on the shared runtime: + +```sh +ae build --emit=lib --shared-runtime --with=net greeting.ae -o greeting.so +``` + +On Linux and FreeBSD such a script runs on the host's runtime whatever the host was built with: an `ae build` program is linked `-rdynamic`, and the script's calls bind to its runtime. On macOS a script binds to the shared runtime it links, so its host is built with `ae build --shared-runtime` too (with a static host it is refused, naming that). On Windows the script is a DLL loaded with `LoadLibrary`, which cannot bind to the host executable's runtime, so the host and each script are built on the shared runtime (`aether.dll`) and run on it together: ```sh ae build --emit=lib --shared-runtime --with=net greeting.ae -o greeting.dll ae build --shared-runtime host.ae -o host ``` -A host or script on its own static runtime is refused at mount with `KIND_IO` and a message naming `--shared-runtime`. +A host or script that would run on a runtime of its own is refused at mount with `KIND_IO` and a message naming `--shared-runtime`. ### API @@ -1418,7 +1424,7 @@ A host or script on its own static runtime is refused at mount with `KIND_IO` an | `KIND_OK` | 0 | mount succeeded | | `KIND_NOT_FOUND` | 1 | `so_path` is missing or unreadable | | `KIND_INVALID` | 6 | null arg, or `.so` missing the `aether_script_handle` entrypoint | -| `KIND_IO` | 5 | `dlopen` / `LoadLibrary` failure (incompatible ABI, etc.); on Windows also a host or script not built with `--shared-runtime` | +| `KIND_IO` | 5 | `dlopen` / `LoadLibrary` failure (incompatible ABI, etc.); also a script that carries a runtime of its own, on Windows a host not on the shared runtime, and on macOS a `--shared-runtime` script whose host is not on it (the message names `--shared-runtime`) | | `KIND_UNAVAILABLE` | 99 | a platform with no dynamic loader; no supported platform returns it | ### Sandbox @@ -1971,7 +1977,7 @@ Coming from Java's `java.security`, Python's `cryptography`, or Go's `crypto/*`, - **Public-key crypto (RSA, ECDSA, Ed25519, X25519), symmetric ciphers (AES, ChaCha20-Poly1305), and key derivation (KDFs).** These live under [`std.cryptography`](../std/cryptography/) (`rsa`, `aes`, `chacha20poly1305`, `ed25519`, `x25519`, `p256`, `secp256k1`, `pem`, `asn1`, ...), pure-Aether ports with no OpenSSL dependency. Each family is a separate sub-module you import explicitly. - **URL-safe Base64 (RFC 4648 §5).** Standard alphabet only; URL-safe (`-` / `_` instead of `+` / `/`) is a separate variant the wrappers don't expose. -- **Constant-time comparison.** Equality checks via `string.equals` are not constant-time; callers comparing hashes for security-sensitive cases need their own constant-time helper. +- **Constant-time comparison.** Equality checks via `string.equals` are not constant-time. Compare a MAC tag, a hash or any other secret-derived bytes with `std.cryptography.ct`: `ct.bytes_eq(a, b, n)` for two buffers, or `ct.bytes_eq_slice(a, b)` for two `byte[]` (1 when equal; slices of different lengths are unequal), which take the same time wherever the bytes differ (`examples/03-std/11-cryptography.ae` checks an HMAC tag this way). Raw externs: `cryptography_sha1_hex_raw`, `cryptography_sha256_hex_raw` return a fresh counted string the caller owns, or NULL on failure. The Go-style wrappers translate the NULL into `("", "openssl unavailable")`. diff --git a/include/libaether.h b/include/libaether.h index 8dfca5171..40f122e86 100644 --- a/include/libaether.h +++ b/include/libaether.h @@ -63,9 +63,11 @@ extern "C" { * Thread-safe: relaxed atomic store. */ AETHER_API void aether_set_memory_cap(uint64_t bytes); -/* Per-thread wall-clock deadline. After this call, every safe point - * (every loop head in `--emit=lib` builds, and every 4096 values a - * JSON parse reads) checks `now >= deadline_at`. Inside an export the +/* Per-thread wall-clock deadline: one instant, `deadline_at = now + ms` + * when this is called, not a budget each call gets. After this call, + * every safe point (every loop head in `--emit=lib` builds, and every + * 4096 values a JSON parse reads) of every call on this thread checks + * `now >= deadline_at`, until a fresh deadline or 0. Inside an export the * first one past the deadline ends the call (#2956): the export returns * its zero value with the outcome AETHER_DEADLINE, its defers and owned * locals released on the way, and a `try` in the script does not catch diff --git a/new_changelogs/20261101T120000Z-fixed-3499-compile-time-linear.md b/new_changelogs/20261101T120000Z-fixed-3499-compile-time-linear.md new file mode 100644 index 000000000..f0592ba08 --- /dev/null +++ b/new_changelogs/20261101T120000Z-fixed-3499-compile-time-linear.md @@ -0,0 +1,12 @@ +- **Compile time grows linearly with a program's size again (#3499).** + Three lookups by name scanned the whole program for each call or each + top-level node: the checker's callee and extern lookups, codegen's + function-pointer globals and its set of function names that a local may + shadow, and the merge's import prefixes. A chain of 8000 functions took + 1 s here and 22 to 25 s on the release's macOS x86_64 runner, which + failed the release twice. Each lookup is now answered from an index + (the checker's clause index, codegen's program index, a hashed name set), + and the same chain compiles in a third of a second; at 32000 functions, + 1.3 s instead of about 20. `order_effects_scaling` now compares 4000 and + 16000 functions on the same runner, so it no longer depends on the + runner's speed. diff --git a/new_changelogs/20261101T130000Z-fixed-3478-if-expression-call-condition.md b/new_changelogs/20261101T130000Z-fixed-3478-if-expression-call-condition.md new file mode 100644 index 000000000..72201c1ee --- /dev/null +++ b/new_changelogs/20261101T130000Z-fixed-3478-if-expression-call-condition.md @@ -0,0 +1,5 @@ +- **An `if` used as a value parses when its condition ends in a call + (#3478).** `s = if leap(year) { "leap" } else { "common" }` failed with + "Expected LEFT_BRACE, got ELSE": the expression form parsed its condition + without the guard the `if` statement uses, so the call took the then + arm's `{` as a trailing block. A call inside an arm still takes one. diff --git a/new_changelogs/20261101T130100Z-fixed-3479-string-index-byte.md b/new_changelogs/20261101T130100Z-fixed-3479-string-index-byte.md new file mode 100644 index 000000000..4b72581dd --- /dev/null +++ b/new_changelogs/20261101T130100Z-fixed-3479-string-index-byte.md @@ -0,0 +1,8 @@ +- **`s[i]` on a string is a checked `int` from 0 to 255 (#3479).** A local + bound to it warned that its type was left to codegen; the read was a + plain `char`, so a byte past 127 (any non-ASCII UTF-8 byte) was negative + on x86 and positive on ARM; and an index past the end read past the + string. It is now an `int` with the value `string.bytes(s)[i]` has. + `s[length]` is the terminator, 0, as a C-style scan reads it, and an + index below 0 or past the terminator is the panic `string index 5 out of + range for length 3`, which a `try` catches, as a slice index's is. diff --git a/new_changelogs/20261101T140000Z-fixed-3485-refused-import-cleans-up.md b/new_changelogs/20261101T140000Z-fixed-3485-refused-import-cleans-up.md new file mode 100644 index 000000000..2915fc5d0 --- /dev/null +++ b/new_changelogs/20261101T140000Z-fixed-3485-refused-import-cleans-up.md @@ -0,0 +1,6 @@ +- **A refused binary import leaves nothing in the temp directory (#3485).** + `ae` refused it from inside the stub it was writing, so on Windows the + open file kept its `ae-binimport--` directory from being + removed: one more for every refused build, over a thousand on one + development machine. The stub is now closed and removed before `ae` + exits. diff --git a/new_changelogs/20261101T150000Z-fixed-3480-reference-statements.md b/new_changelogs/20261101T150000Z-fixed-3480-reference-statements.md new file mode 100644 index 000000000..c954bf011 --- /dev/null +++ b/new_changelogs/20261101T150000Z-fixed-3480-reference-statements.md @@ -0,0 +1,9 @@ +- **Three reference statements now say what the runtime does (#3480).** A + handler that takes an ask and returns without a `reply` ends the ask at + once with "the handler returned without replying"; the language + reference said the caller timed out. std.cryptography's note on + constant-time comparison now points to `ct.bytes_eq` and + `ct.bytes_eq_slice` instead of telling callers to write their own. And + `aether_set_call_deadline(ms)` sets one instant per thread, `ms` from + when it is called, which every later call is cut at until a fresh + deadline, not a budget per call (emit-lib.md and libaether.h). diff --git a/new_changelogs/20261101T160000Z-fixed-3501-script-gateway-one-runtime.md b/new_changelogs/20261101T160000Z-fixed-3501-script-gateway-one-runtime.md new file mode 100644 index 000000000..c4ca26122 --- /dev/null +++ b/new_changelogs/20261101T160000Z-fixed-3501-script-gateway-one-runtime.md @@ -0,0 +1,10 @@ +- **`script_gateway.mount` refuses a script that carries a runtime of its + own on Linux, FreeBSD and macOS too (#3501).** Since #3481 a script built + the default way (`ae build --emit=lib`) keeps its own runtime on every + platform, and on POSIX it was mounted unchecked: two runtimes, each with + its own caps, config and panic frames, while the script filled objects + the host's runtime frees. It is now refused at mount with `KIND_IO` and a + message naming `--shared-runtime`, as Windows refuses it. A + `--shared-runtime` script mounts on Linux and FreeBSD whatever the host + was built with; on macOS the host is built with `--shared-runtime` too. + The module's documentation now builds scripts with `--shared-runtime`. diff --git a/new_changelogs/20261101T170000Z-fixed-return-inside-try-catches.md b/new_changelogs/20261101T170000Z-fixed-return-inside-try-catches.md new file mode 100644 index 000000000..db3b056b4 --- /dev/null +++ b/new_changelogs/20261101T170000Z-fixed-return-inside-try-catches.md @@ -0,0 +1,11 @@ +- **A panic while a `return` inside a `try` computes its value is caught, + and `main` computes what it returns before its defers run.** A `return` + with no defers to run popped the try frames before it computed the + value, so `try { return xs[i] } catch e { ... }` with `i` out of range, + or a returned call that panics, ended the process with "panic outside + any try/catch". The value is now computed inside the frames and they are + popped after, for a single value and a tuple, as a return with defers + already did. `main` ran its defers, and released its owned locals, + before computing the value it returns: `return string.length(s)` read a + released `s`. It now computes the value first, as every other function + does. diff --git a/runtime/aether_slice.h b/runtime/aether_slice.h index 03bedf955..54fb09d62 100644 --- a/runtime/aether_slice.h +++ b/runtime/aether_slice.h @@ -164,6 +164,14 @@ static inline void aether_array_fail_index(int64_t idx, uint64_t len, file, line, (long long)idx, (unsigned long long)len); } +/* #3479: `s[i]` on a string outside `0 ..= length` (`length` is its + * terminator, 0), the slice's panic with the string's length. */ +static inline void aether_string_fail_index(int64_t idx, uint64_t len, + const char* file, int line) { + aether_slice_panicf("%s:%d: string index %lld out of range for length %llu", + file, line, (long long)idx, (unsigned long long)len); +} + /* #3113: `a[i]` on a `T[N]`: `i`, checked against N, for the generated C * to index with (`a[aether_array_index(i, N, ...)]`), so the element stays * an lvalue and `i` is evaluated once. A negative index is out of range. diff --git a/std/http/script_gateway/aether_script_gateway.c b/std/http/script_gateway/aether_script_gateway.c index d39210765..194026f60 100644 --- a/std/http/script_gateway/aether_script_gateway.c +++ b/std/http/script_gateway/aether_script_gateway.c @@ -4,8 +4,8 @@ * 1. Load the library through std.dl: dlopen(RTLD_NOW | RTLD_LOCAL) on * POSIX, LoadLibrary on Windows. * 2. Look up "aether_script_handle" in it. - * 3. On Windows, check that the host and the script run on one runtime - * (see sg_host_runtime_check and sg_script_runtime_check). + * 3. Check that the host and the script run on one runtime + * (sg_script_runtime_check; on Windows sg_host_runtime_check too). * 4. Allocate a ScriptMount struct holding (path_prefix, fn). * 5. Register a middleware on the host server that fires the fn * when req->path starts with path_prefix. @@ -14,10 +14,15 @@ * fork, no exec, no IPC. */ +#if !defined(_WIN32) && !defined(_GNU_SOURCE) +#define _GNU_SOURCE /* glibc declares dladdr under it (#3501) */ +#endif + #include "aether_script_gateway.h" #include "../../net/aether_http_server.h" #include "../../dl/aether_dl.h" #include "../../../runtime/aether_sandbox.h" +#include "../../../runtime/aether_abi.h" /* aether_runtime_abi, #3501 */ #include "../../../runtime/utils/aether_compiler.h" /* AETHER_TLS */ #include @@ -29,6 +34,7 @@ #include /* _access */ #else #include /* access */ +#include /* dladdr, #3501 */ #endif /* (handle, kind, message) tuple: same ABI as fs.copy's tuple but @@ -157,6 +163,70 @@ static const char* sg_script_runtime_check(void* script, const char* so_path) { so_path, rt_name); return g_sg_err; } +#else /* POSIX */ +/* #3501: on POSIX a host shares its runtime with a script when the script + * carries none (its runtime calls left unresolved, bound to the host's at + * dlopen: an `ae build` program is linked -rdynamic) or links the shared + * runtime the host runs on. One built the default way carries a runtime of + * its own and keeps it (on ELF `ae` links it -Bsymbolic, #3481): two runtimes, + * each with its own caps, config and panic frames, while the script fills + * objects the host's runtime frees. It is refused at mount, as on Windows. + * + * What runtime the script gets is read from where its loader finds one: the + * image that defines the runtime's `aether_runtime_abi` for the script's + * handle, against the image that holds this runtime's (the host's). */ + +#if defined(__wasi__) +/* WASI has no dynamic loader (and no dladdr): a mount fails at the load, + * before there is a script to check. Should a loader load one, what its + * runtime is cannot be read, so it is refused rather than run unchecked. */ +static const char* sg_script_runtime_check(void* script, const char* so_path) { + (void)script; + (void)so_path; + return "script_gateway: no way to tell which runtime a script runs on here (WASI)"; +} +#else +/* The reason a script was refused, per thread like std.dl's last error. */ +static AETHER_TLS char g_sg_err[512]; + +static const void* sg_image_of(const void* addr) { + Dl_info info; + if (!addr || !dladdr(addr, &info)) return NULL; + return info.dli_fbase; +} + +static const char* sg_script_runtime_check(void* script, const char* so_path) { + const void* host_rt = sg_image_of((const void*)&aether_runtime_abi); + void* abi = aether_dl_symbol_raw(script, "aether_runtime_abi"); + /* No runtime in the script or what it links: its calls bind to the host's. */ + if (!abi || !host_rt) return NULL; + const void* script_rt = sg_image_of(abi); + if (!script_rt || script_rt == host_rt) return NULL; + const void* script_img = sg_image_of(aether_dl_symbol_raw(script, "aether_script_handle")); + if (script_rt == script_img) { + snprintf(g_sg_err, sizeof(g_sg_err), + "script_gateway: %s carries a runtime of its own, not the host's; " + "build it with `ae build --emit=lib --shared-runtime`", + so_path); + return g_sg_err; + } +#if defined(__APPLE__) + /* The shared runtime, while the host runs on its own: a Mach-O image binds + * each import to the library that provides it, so the script runs on the + * shared runtime and the host on another. */ + snprintf(g_sg_err, sizeof(g_sg_err), + "script_gateway: %s runs on the shared runtime and the host on its own; " + "build the host with `ae build --shared-runtime` too", + so_path); + return g_sg_err; +#else + /* The shared runtime, while the host's is the program's own: on ELF the + * script's calls bind to the first definition in the global scope, the + * program's (linked -rdynamic), so both run on the host's. */ + return NULL; +#endif +} +#endif /* __wasi__ */ #endif /* _WIN32 */ _aether_sg_tuple script_gateway_mount_so_raw(void* server, @@ -228,7 +298,6 @@ _aether_sg_tuple script_gateway_mount_so_raw(void* server, return out; } -#ifdef _WIN32 const char* script_err = sg_script_runtime_check(h, so_path); if (script_err) { /* Its runtime is not the host's, so nothing of it may run here. */ @@ -236,7 +305,6 @@ _aether_sg_tuple script_gateway_mount_so_raw(void* server, _aether_sg_tuple out = { 0, AETHER_SCRIPT_GATEWAY_KIND_IO, script_err }; return out; } -#endif ScriptMount* m = (ScriptMount*)calloc(1, sizeof(ScriptMount)); if (!m) { diff --git a/std/http/script_gateway/aether_script_gateway.h b/std/http/script_gateway/aether_script_gateway.h index 1c6a679db..8dc79f481 100644 --- a/std/http/script_gateway/aether_script_gateway.h +++ b/std/http/script_gateway/aether_script_gateway.h @@ -15,14 +15,17 @@ * with RTLD_LOCAL so symbols don't pollute the host process's * global namespace. * - * On POSIX a script built the default way carries a runtime of its own and - * runs on it (on Linux and FreeBSD too since #3481, where `ae` links it - * -Bsymbolic); one built with --shared-runtime shares the host's. On - * Windows the library is a DLL loaded with LoadLibrary, and the host and - * the script must both be built with `ae build --shared-runtime`: a DLL - * cannot bind to the host executable's runtime, so they share aether.dll - * instead. A mismatch is refused at mount with KIND_IO - * (sg_host_runtime_check and sg_script_runtime_check in the .c). + * A script built the default way carries a runtime of its own and keeps it + * (on Linux and FreeBSD too since #3481, where `ae` links it -Bsymbolic): it + * is refused at mount with KIND_IO on every platform (#3501), and a script + * is built with `ae build --emit=lib --shared-runtime`. On Linux and FreeBSD + * such a script runs on the host's runtime whatever the host was built with + * (the program is linked -rdynamic); on macOS its host is built with + * --shared-runtime too. On Windows the library is a DLL loaded with + * LoadLibrary, and the host and the script must both be built with + * `ae build --shared-runtime`: a DLL cannot bind to the host executable's + * runtime, so they share aether.dll instead. A mismatch is refused at mount + * with KIND_IO (sg_host_runtime_check and sg_script_runtime_check in the .c). * * Lifetime: dlopen handles are intentionally not dlclose()d. The * gateway is a long-lived hosting feature; the .so stays mapped diff --git a/std/http/script_gateway/module.ae b/std/http/script_gateway/module.ae index 191d136cd..a56238fe1 100644 --- a/std/http/script_gateway/module.ae +++ b/std/http/script_gateway/module.ae @@ -25,10 +25,11 @@ // } // ``` // -// Build it as a shared library: +// Build it as a shared library on the shared runtime, so it runs on the +// host's (see "One runtime" below): // // ```sh -// ae build --emit=lib greeting.ae -o /var/aether/scripts/greeting.so +// ae build --emit=lib --shared-runtime --with=net greeting.ae -o /var/aether/scripts/greeting.so // ``` // // And mount in the host: @@ -51,24 +52,30 @@ // Now `GET /greet/anything` runs `aether_script_handle` from // greeting.so directly on the connection thread. // -// ### Windows -// -// The library is a DLL, loaded with LoadLibrary. On Linux, FreeBSD and -// macOS a script built the default way carries a runtime of its own and runs -// on it (#3481); one built with --shared-runtime shares the host's. Windows -// has no way for a DLL to bind to the host executable, so there the host and -// every script are built on the shared runtime (aether.dll) and run on it -// together: +// ### One runtime +// +// A script fills request and response objects the host's runtime frees, so +// the two run on one runtime. A script built the default way +// (`ae build --emit=lib`) carries a runtime of its own and keeps it (#3481): +// it is refused at mount, on every platform, with KIND_IO and a message +// naming `--shared-runtime`, rather than run on two runtimes that each keep +// their own caps, config and panic frames (#3501). +// +// On Linux and FreeBSD a `--shared-runtime` script runs on the host's +// runtime whatever the host was built with: an `ae build` program is linked +// -rdynamic, and the script's calls bind to its runtime. On macOS such a +// script binds to the shared runtime it links, so its host is built with +// `ae build --shared-runtime` too (with a static host it is refused, naming +// that). +// On Windows the library is a DLL, loaded with LoadLibrary, which cannot +// bind to the host executable: there too the host and every script are +// built on the shared runtime (aether.dll) and run on it together: // // ```sh // ae build --emit=lib --shared-runtime --with=net greeting.ae -o greeting.dll // ae build --shared-runtime host.ae -o host // ``` // -// A host or a script on its own static runtime is refused at mount with -// KIND_IO and a message naming `--shared-runtime`, rather than run on two -// runtimes that each keep their own caps, config and panic frames. -// // ### Lifetime // // The dlopen handle is intentionally long-lived (process-lifetime). @@ -111,9 +118,11 @@ extern script_gateway_mount_so_raw(server: ptr, path_prefix: string, so_path: st // // KIND_NOT_FOUND: `so_path` is missing or unreadable // KIND_INVALID: null arg, or .so missing the entrypoint -// KIND_IO: dlopen failure (incompatible ABI, etc.), or on -// Windows a host or script not built with -// --shared-runtime +// KIND_IO: dlopen failure (incompatible ABI, etc.), or a +// script that carries a runtime of its own, on Windows +// a host not on the shared runtime, on macOS a +// --shared-runtime script whose host is not on it: +// the message names --shared-runtime mount(server: ptr, path_prefix: string, so_path: string) -> { return script_gateway_mount_so_raw(server, path_prefix, so_path) } diff --git a/tests/integration/binary_import_ownership/test_binary_import_ownership.sh b/tests/integration/binary_import_ownership/test_binary_import_ownership.sh index d3253f35c..5ad24b6e8 100644 --- a/tests/integration/binary_import_ownership/test_binary_import_ownership.sh +++ b/tests/integration/binary_import_ownership/test_binary_import_ownership.sh @@ -62,11 +62,20 @@ build_app() { # refused