diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 1c9fc6c..1c44942 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -16,7 +16,7 @@ oscan tests/test_html.osc --run ## Language: Oscan -This project is written in [Oscan](https://github.com/lucabol/Oscan), a minimalist language compiling to freestanding C99. Refer to the Oscan repo's `.github/instructions/oscan.instructions.md` for the full language reference. Critical reminders: +This project is written in [Oscan](https://github.com/lucabol/Oscan), a minimalist language with LLVM, Cranelift, and C backends. Refer to the Oscan repo's `.github/instructions/oscan.instructions.md` for the full language reference. Critical reminders: - `fn` = pure (no I/O), `fn!` = impure. Pure functions cannot call impure ones. - Use `and`/`or`/`not` instead of `&&`/`||`/`!`. @@ -27,6 +27,8 @@ This project is written in [Oscan](https://github.com/lucabol/Oscan), a minimali - String indexing: use `str_to_chars(s)` to get `[i32]`, then index that array. - No self-referential structs. Use flat arrays with integer indices (see DOM design below). - Oscan's anti-shadowing rule: a variable name cannot be reused in the same or nested scope, even in different `for` loops within the same function. +- Mutating collection operations (`push`, `pop`, `array_clear`, `array_extend`, and similar) require a mutable local or global root. Array parameters must first be assigned to a `let mut` alias. +- Prefer the canonical `array_*` collection intrinsics. Use typed search functions such as `array_index_of_str` for primitive arrays; avoid callback-heavy conversions where closures or struct support would be required. ## Architecture @@ -52,7 +54,7 @@ struct HtmlNode { } ``` -`html_parse(nodes, source)` takes a mutable `[HtmlNode]` array and returns the root index. To walk children: `let mut child: i32 = node.first_child; while child != -1 { ... child = nodes[child].next_sibling; };` +`html_parse(nodes, source)` updates a caller-owned `[HtmlNode]` array through a mutable local alias and returns the root index. To walk children: `let mut child: i32 = node.first_child; while child != -1 { ... child = nodes[child].next_sibling; };` ### Rendering (browser.osc) @@ -99,7 +101,9 @@ Oscan's `arena { }` frees child-arena allocations on exit. Any `push()` to a lon ### Mutable array clearing -Never reassign arrays inside loops: `arr = [];` allocates a new array that may be freed. Instead clear in-place: `while len(arr) > 0 { pop(arr); };` +Never reassign arrays inside loops: `arr = [];` allocates a new array that may be freed. Clear a mutable array in place with `array_clear(arr);`. + +`array_extend`, `array_insert`, `array_slice`, `array_map_*`, and `array_filter_*` can allocate. Growing a persistent array must remain outside child `arena { }` scopes. ### Keyboard input handling outside rendering diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8e4bd5..1c16107 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,36 +11,36 @@ permissions: contents: read env: - OSCAN_VERSION: 0.0.39 + OSCAN_VERSION: 0.0.43 concurrency: group: ci-${{ github.ref }} cancel-in-progress: true jobs: - build-and-test: - name: ${{ matrix.name }} + c-build-and-test: + name: ${{ matrix.name }} C strategy: fail-fast: false matrix: include: - os: windows-latest name: windows-x86_64 - backend: native - trusted_native_link_arg: -AllowElevatedNativeLink - oscan_archive: oscan-v{OSCAN_VER}-windows-x86_64-full.zip + backend: c + trusted_native_link_arg: "" + oscan_archive: oscan-v{OSCAN_VER}-windows-x86_64-c.zip oscan_bin: oscan.exe - os: ubuntu-latest name: linux-x86_64 - backend: native + backend: c trusted_native_link_arg: "" - oscan_archive: oscan-v{OSCAN_VER}-linux-x86_64-full.tar.xz + oscan_archive: oscan-v{OSCAN_VER}-linux-x86_64-c.tar.xz oscan_bin: oscan - os: macos-15 name: macos-x86_64 backend: c trusted_native_link_arg: "" - oscan_archive: oscan-v{OSCAN_VER}-macos-x86_64.tar.gz + oscan_archive: oscan-v{OSCAN_VER}-macos-x86_64-c.tar.gz oscan_bin: oscan runs-on: ${{ matrix.os }} steps: @@ -90,6 +90,17 @@ jobs: OSCAN_DIR=$(dirname "$(realpath "$OSCAN_BIN")") chmod +x "$OSCAN_DIR/${{ matrix.oscan_bin }}" 2>/dev/null || true + # v0.0.43's Linux C archive omits the BearSSL static library even + # though its freestanding TLS code requires it. Stage the matching + # prebuilt archive from the same Oscan tag in the compiler's + # documented release-bundle search path. + if [ "${{ runner.os }}" = "Linux" ]; then + mkdir -p "$OSCAN_DIR/toolchain/lib" + curl --fail --location --silent --show-error --retry 3 \ + --output "$OSCAN_DIR/toolchain/lib/libbearssl.a" \ + "https://raw.githubusercontent.com/lucabol/Oscan/$OSCAN_TAG/packaging/prebuilt/linux-x86_64/libbearssl.a" + ar t "$OSCAN_DIR/toolchain/lib/libbearssl.a" >/dev/null + fi # On Windows, convert to a native path so pwsh in later steps can # find oscan.exe via PATH (bash-style paths like /d/a/... are not # recognized by PowerShell). @@ -106,3 +117,160 @@ jobs: - name: Tests shell: pwsh run: ./build.ps1 -Backend ${{ matrix.backend }} ${{ matrix.trusted_native_link_arg }} -Test -V + + hosted-llvm: + name: ${{ matrix.name }} hosted LLVM + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + name: windows-x86_64 + llvm_archive: oscan-v{OSCAN_VER}-windows-x86_64-llvm.zip + c_archive: oscan-v{OSCAN_VER}-windows-x86_64-c.zip + oscan_bin: oscan.exe + trusted_native_link_arg: -AllowElevatedNativeLink + - os: ubuntu-latest + name: linux-x86_64 + llvm_archive: oscan-v{OSCAN_VER}-linux-x86_64-llvm.tar.xz + c_archive: oscan-v{OSCAN_VER}-linux-x86_64-c.tar.xz + oscan_bin: oscan + trusted_native_link_arg: "" + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v5 + + - name: Install hosted LLVM Oscan + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + OSCAN_TAG="v${OSCAN_VERSION}" + OSCAN_VER="${OSCAN_TAG#v}" + ASSET_DIR=/tmp/oscan-assets + SRC_DIR=/tmp/oscan-source + mkdir -p "$ASSET_DIR" + + LLVM_ARCHIVE="${{ matrix.llvm_archive }}" + LLVM_ARCHIVE="${LLVM_ARCHIVE//\{OSCAN_VER\}/$OSCAN_VER}" + gh release download "$OSCAN_TAG" --repo lucabol/Oscan \ + --pattern "$LLVM_ARCHIVE" --dir "$ASSET_DIR" + case "$LLVM_ARCHIVE" in + *.zip) unzip -q "$ASSET_DIR/$LLVM_ARCHIVE" -d "$ASSET_DIR" ;; + *.tar.xz) tar xf "$ASSET_DIR/$LLVM_ARCHIVE" -C "$ASSET_DIR" ;; + esac + + C_ARCHIVE="${{ matrix.c_archive }}" + C_ARCHIVE="${C_ARCHIVE//\{OSCAN_VER\}/$OSCAN_VER}" + gh release download "$OSCAN_TAG" --repo lucabol/Oscan \ + --pattern "$C_ARCHIVE" --dir "$ASSET_DIR" + case "$C_ARCHIVE" in + *.zip) unzip -q "$ASSET_DIR/$C_ARCHIVE" -d "$ASSET_DIR" ;; + *.tar.xz) tar xf "$ASSET_DIR/$C_ARCHIVE" -C "$ASSET_DIR" ;; + esac + + git clone --quiet --depth 1 --recurse-submodules \ + --branch "$OSCAN_TAG" https://github.com/lucabol/Oscan.git "$SRC_DIR" + cargo build --quiet --locked --release --manifest-path "$SRC_DIR/Cargo.toml" + + OSCAN_DIR="$SRC_DIR/target/release" + LLVM_LIB=$(find "$ASSET_DIR" -type f \( -name 'libLLVM-22.dll' -o -name 'libLLVM.so.22.1' \) | head -1) + if [ -z "$LLVM_LIB" ]; then + echo "ERROR: LLVM provider not found in $LLVM_ARCHIVE" + exit 1 + fi + LLVM_LIB_DIR=$(dirname "$LLVM_LIB") + + if [ "${{ runner.os }}" = "Windows" ]; then + OSCAN_CC=$(find "$ASSET_DIR" -type f -path '*/toolchain/bin/clang.exe' | head -1) + if [ -z "$OSCAN_CC" ]; then + echo "ERROR: Hosted C compiler not found in $C_ARCHIVE" + exit 1 + fi + OSCAN_DIR=$(cygpath -w "$OSCAN_DIR") + LLVM_LIB=$(cygpath -w "$LLVM_LIB") + LLVM_LIB_DIR=$(cygpath -w "$LLVM_LIB_DIR") + OSCAN_CC=$(cygpath -w "$OSCAN_CC") + else + OSCAN_CC=$(find "$ASSET_DIR" -type f -path '*/toolchain/bin/gcc' | head -1) + OSCAN_ARCHIVE_AR=$(find "$ASSET_DIR" -type f -path '*/toolchain/bin/x86_64-linux-musl-ar' | head -1) + if [ -z "$OSCAN_CC" ] || [ -z "$OSCAN_ARCHIVE_AR" ]; then + echo "ERROR: Hosted musl compiler or archiver not found in $C_ARCHIVE" + exit 1 + fi + echo "LD_LIBRARY_PATH=$LLVM_LIB_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" >> "$GITHUB_ENV" + echo "OSCAN_ARCHIVE_CC=$OSCAN_CC" >> "$GITHUB_ENV" + echo "OSCAN_ARCHIVE_AR=$OSCAN_ARCHIVE_AR" >> "$GITHUB_ENV" + fi + + echo "$OSCAN_DIR" >> "$GITHUB_PATH" + echo "$LLVM_LIB_DIR" >> "$GITHUB_PATH" + echo "OSCAN_LLVM_LIB=$LLVM_LIB" >> "$GITHUB_ENV" + echo "OSCAN_CC=$OSCAN_CC" >> "$GITHUB_ENV" + echo "OSCAN_NATIVE_LINKER=$OSCAN_CC" >> "$GITHUB_ENV" + echo "OSCAN_NATIVE_LINKER_FLAVOR=compiler-driver" >> "$GITHUB_ENV" + + - name: Verify compiler metadata + shell: pwsh + run: | + $metadata = oscan --version | Out-String + if ($metadata -notmatch 'backends: llvm, cranelift, c' -or + $metadata -notmatch 'toolchain-free: no') { + throw "Unexpected hosted compiler metadata:`n$metadata" + } + + - name: Build + shell: pwsh + run: ./build.ps1 -Backend llvm ${{ matrix.trusted_native_link_arg }} -V + + - name: Tests + shell: pwsh + run: ./build.ps1 -Backend llvm ${{ matrix.trusted_native_link_arg }} -Test -V + + collection-parity: + name: Collections / ${{ matrix.backend }} + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + include: + - backend: llvm + archive: oscan-v{OSCAN_VER}-windows-x86_64-llvm.zip + object_backend: true + - backend: cranelift + archive: oscan-v{OSCAN_VER}-windows-x86_64-cranelift.zip + object_backend: true + - backend: c + archive: oscan-v{OSCAN_VER}-windows-x86_64-c.zip + object_backend: false + steps: + - uses: actions/checkout@v5 + + - name: Install backend package + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + OSCAN_TAG="v${OSCAN_VERSION}" + OSCAN_VER="${OSCAN_TAG#v}" + ARCHIVE="${{ matrix.archive }}" + ARCHIVE="${ARCHIVE//\{OSCAN_VER\}/$OSCAN_VER}" + mkdir -p /tmp/oscan-parity + gh release download "$OSCAN_TAG" --repo lucabol/Oscan \ + --pattern "$ARCHIVE" --dir /tmp/oscan-parity + unzip -q "/tmp/oscan-parity/$ARCHIVE" -d /tmp/oscan-parity + OSCAN_BIN=$(find /tmp/oscan-parity -name oscan.exe -type f | head -1) + OSCAN_DIR=$(cygpath -w "$(dirname "$OSCAN_BIN")") + echo "$OSCAN_DIR" >> "$GITHUB_PATH" + + - name: Run collection intrinsics + shell: pwsh + run: | + $oscanArgs = @('tests/test_collections.osc', '--backend', '${{ matrix.backend }}', '--run') + if ('${{ matrix.object_backend }}' -eq 'true') { + $oscanArgs += @('--native-target', 'host', '--allow-elevated-native-link') + } + & oscan @oscanArgs + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b8d246..b40e3ea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,12 +9,17 @@ on: description: "Version tag (e.g. v0.1.0)" required: true type: string + dry_run: + description: "Build and test release artifacts without publishing" + required: false + default: false + type: boolean permissions: contents: write env: - OSCAN_VERSION: 0.0.39 + OSCAN_VERSION: 0.0.43 jobs: build: @@ -25,26 +30,32 @@ jobs: include: - os: windows-latest name: windows-x86_64 - backend: native + backend: llvm + compiler_mode: hosted-llvm trusted_native_link_arg: -AllowElevatedNativeLink optimize_size_arg: -OptimizeSize - oscan_archive: oscan-v{OSCAN_VER}-windows-x86_64-full.zip + oscan_archive: oscan-v{OSCAN_VER}-windows-x86_64-llvm.zip + c_archive: oscan-v{OSCAN_VER}-windows-x86_64-c.zip oscan_bin: oscan.exe output: oscanweb-windows-x86_64.exe - os: ubuntu-latest name: linux-x86_64 - backend: native + backend: llvm + compiler_mode: hosted-llvm trusted_native_link_arg: "" optimize_size_arg: -OptimizeSize - oscan_archive: oscan-v{OSCAN_VER}-linux-x86_64-full.tar.xz + oscan_archive: oscan-v{OSCAN_VER}-linux-x86_64-llvm.tar.xz + c_archive: oscan-v{OSCAN_VER}-linux-x86_64-c.tar.xz oscan_bin: oscan output: oscanweb-linux-x86_64 - os: macos-15 name: macos-x86_64 backend: c + compiler_mode: package trusted_native_link_arg: "" optimize_size_arg: "" - oscan_archive: oscan-v{OSCAN_VER}-macos-x86_64.tar.gz + oscan_archive: oscan-v{OSCAN_VER}-macos-x86_64-c.tar.gz + c_archive: "" oscan_bin: oscan output: oscanweb-macos-x86_64 runs-on: ${{ matrix.os }} @@ -83,33 +94,79 @@ jobs: ARCHIVE_TEMPLATE="${{ matrix.oscan_archive }}" ARCHIVE_NAME="${ARCHIVE_TEMPLATE//\{OSCAN_VER\}/$OSCAN_VER}" - gh release download "$OSCAN_TAG" \ - --repo lucabol/Oscan \ - --pattern "$ARCHIVE_NAME" \ - --dir /tmp/oscan-dl + mkdir -p /tmp/oscan-dl + gh release download "$OSCAN_TAG" --repo lucabol/Oscan \ + --pattern "$ARCHIVE_NAME" --dir /tmp/oscan-dl - cd /tmp/oscan-dl case "$ARCHIVE_NAME" in - *.zip) unzip -q "$ARCHIVE_NAME" ;; - *.tar.xz) tar xf "$ARCHIVE_NAME" ;; - *.tar.gz) tar xf "$ARCHIVE_NAME" ;; + *.zip) unzip -q "/tmp/oscan-dl/$ARCHIVE_NAME" -d /tmp/oscan-dl ;; + *.tar.xz) tar xf "/tmp/oscan-dl/$ARCHIVE_NAME" -C /tmp/oscan-dl ;; + *.tar.gz) tar xf "/tmp/oscan-dl/$ARCHIVE_NAME" -C /tmp/oscan-dl ;; esac - OSCAN_BIN=$(find . -name "${{ matrix.oscan_bin }}" -type f | head -1) - if [ -z "$OSCAN_BIN" ]; then - echo "ERROR: Could not find ${{ matrix.oscan_bin }}" - find . -type f - exit 1 + if [ "${{ matrix.compiler_mode }}" = "hosted-llvm" ]; then + SRC_DIR=/tmp/oscan-source + git clone --quiet --depth 1 --recurse-submodules \ + --branch "$OSCAN_TAG" https://github.com/lucabol/Oscan.git "$SRC_DIR" + cargo build --quiet --locked --release --manifest-path "$SRC_DIR/Cargo.toml" + OSCAN_DIR="$SRC_DIR/target/release" + LLVM_LIB=$(find /tmp/oscan-dl -type f \( -name 'libLLVM-22.dll' -o -name 'libLLVM.so.22.1' \) | head -1) + if [ -z "$LLVM_LIB" ]; then + echo "ERROR: LLVM provider not found in $ARCHIVE_NAME" + exit 1 + fi + LLVM_LIB_DIR=$(dirname "$LLVM_LIB") + + C_ARCHIVE="${{ matrix.c_archive }}" + C_ARCHIVE="${C_ARCHIVE//\{OSCAN_VER\}/$OSCAN_VER}" + gh release download "$OSCAN_TAG" --repo lucabol/Oscan \ + --pattern "$C_ARCHIVE" --dir /tmp/oscan-dl + case "$C_ARCHIVE" in + *.zip) unzip -q "/tmp/oscan-dl/$C_ARCHIVE" -d /tmp/oscan-dl ;; + *.tar.xz) tar xf "/tmp/oscan-dl/$C_ARCHIVE" -C /tmp/oscan-dl ;; + esac + + if [ "${{ runner.os }}" = "Windows" ]; then + OSCAN_CC=$(find /tmp/oscan-dl -type f -path '*/toolchain/bin/clang.exe' | head -1) + if [ -z "$OSCAN_CC" ]; then + echo "ERROR: Hosted C compiler not found in $C_ARCHIVE" + exit 1 + fi + OSCAN_DIR=$(cygpath -w "$OSCAN_DIR") + LLVM_LIB=$(cygpath -w "$LLVM_LIB") + LLVM_LIB_DIR=$(cygpath -w "$LLVM_LIB_DIR") + OSCAN_CC=$(cygpath -w "$OSCAN_CC") + else + OSCAN_CC=$(find /tmp/oscan-dl -type f -path '*/toolchain/bin/gcc' | head -1) + OSCAN_ARCHIVE_AR=$(find /tmp/oscan-dl -type f -path '*/toolchain/bin/x86_64-linux-musl-ar' | head -1) + if [ -z "$OSCAN_CC" ] || [ -z "$OSCAN_ARCHIVE_AR" ]; then + echo "ERROR: Hosted musl compiler or archiver not found in $C_ARCHIVE" + exit 1 + fi + echo "LD_LIBRARY_PATH=$LLVM_LIB_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" >> "$GITHUB_ENV" + echo "OSCAN_ARCHIVE_CC=$OSCAN_CC" >> "$GITHUB_ENV" + echo "OSCAN_ARCHIVE_AR=$OSCAN_ARCHIVE_AR" >> "$GITHUB_ENV" + fi + + echo "$LLVM_LIB_DIR" >> "$GITHUB_PATH" + echo "OSCAN_LLVM_LIB=$LLVM_LIB" >> "$GITHUB_ENV" + echo "OSCAN_CC=$OSCAN_CC" >> "$GITHUB_ENV" + echo "OSCAN_NATIVE_LINKER=$OSCAN_CC" >> "$GITHUB_ENV" + echo "OSCAN_NATIVE_LINKER_FLAVOR=compiler-driver" >> "$GITHUB_ENV" + else + OSCAN_BIN=$(find /tmp/oscan-dl -name "${{ matrix.oscan_bin }}" -type f | head -1) + if [ -z "$OSCAN_BIN" ]; then + echo "ERROR: Could not find ${{ matrix.oscan_bin }}" + find /tmp/oscan-dl -type f + exit 1 + fi + OSCAN_DIR=$(dirname "$(realpath "$OSCAN_BIN")") + chmod +x "$OSCAN_DIR/${{ matrix.oscan_bin }}" 2>/dev/null || true + if [ "${{ runner.os }}" = "Windows" ]; then + OSCAN_DIR=$(cygpath -w "$OSCAN_DIR") + fi fi - OSCAN_DIR=$(dirname "$(realpath "$OSCAN_BIN")") - chmod +x "$OSCAN_DIR/${{ matrix.oscan_bin }}" 2>/dev/null || true - # On Windows, convert to a native path so pwsh in later steps can - # find oscan.exe via PATH (bash-style paths like /d/a/... are not - # recognized by PowerShell). - if [ "${{ runner.os }}" = "Windows" ]; then - OSCAN_DIR=$(cygpath -w "$OSCAN_DIR") - fi echo "$OSCAN_DIR" >> "$GITHUB_PATH" echo "Oscan installed from $OSCAN_DIR" env: @@ -132,6 +189,7 @@ jobs: release: name: Publish Release needs: build + if: github.event_name != 'workflow_dispatch' || !inputs.dry_run runs-on: ubuntu-latest steps: - name: Resolve tag diff --git a/README.md b/README.md index f95b1a6..12a30e7 100644 --- a/README.md +++ b/README.md @@ -74,38 +74,38 @@ Switch to a TrueType font (`TT`) and bump the size (`16pt`) for a clean, modern ## Prerequisites -- **[Oscan compiler](https://github.com/lucabol/Oscan)** — required (includes TLS support). Native Windows/Linux builds require Oscan `v0.0.39` or newer with `--backend c|native`, `--native-target`, `--extra-obj`, `--extra-lib`, `--allow-elevated-native-link`, native user-extern `str` parameter/return shims, and native hosted Windows/Linux TLS plus Windows canvas/gfx/clipboard/img/svg/tt runtime support. +- **[Oscan compiler](https://github.com/lucabol/Oscan)** `v0.0.43` or newer — required (includes TLS support and the collection intrinsics used by OscaWeb). - **PowerShell** — for the build script ## Build backends -`build.ps1` selects an explicit Oscan backend instead of relying on the compiler default: +`build.ps1` accepts `auto`, `llvm`, `cranelift`, and `c`. It reads `oscan --version` metadata and individual help capabilities instead of assuming a backend from the host OS. -| Platform | Default | Runtime | Notes | -| --- | --- | --- | --- | -| Windows x86-64 | `native` | hosted libc (`--libc`) | Links QuickJS, WinHTTP, Winsock, gzip bridge, and miniz. Release builds pass the Windows GUI subsystem flag. | -| Linux x86-64 | `native` | hosted libc (`--libc`) | Adds `-D_GNU_SOURCE` for bundled QuickJS and `-static` so release/test binaries do not depend on a host musl loader, avoids the known QuickJS/freestanding crash path, and links gzip bridge + miniz through the native backend. | -| macOS x86-64 | `c` | C backend | Native backend is not supported on macOS, so macOS intentionally stays on the C backend. | +| Backend | Runtime | Use | +| --- | --- | --- | +| `llvm` | hosted libc (`--libc`) | Recommended Windows/Linux backend and the release default. | +| `cranelift` | hosted libc (`--libc`) | Alternate object backend for comparison and debugging. | +| `c` | C backend | Compatibility baseline and the supported macOS backend. | +| `auto` | capability-dependent | Chooses hosted LLVM when available, otherwise C, then hosted Cranelift. | -If your installed `oscan` predates `--backend`, `build.ps1` falls back to the legacy C backend for `-Backend auto`/`-Backend c`. Explicit `-Backend native` requires a newer compiler. +OscaWeb compiles QuickJS, miniz, and two bridge files through `--extra-c`. The stock v0.0.43 LLVM and Cranelift release packages are intentionally toolchain-free and therefore cannot build the full browser. Use the v0.0.43 C package, or build Oscan from a recursive source checkout so `toolchain-free: no` and `--libc`, `--extra-c`, and `--extra-cflags` are available. The v0.0.43 Linux C archive omits `libbearssl.a`; place the matching tag's `packaging/prebuilt/linux-x86_64/libbearssl.a` under the extracted package's `toolchain/lib/` directory before building TLS-enabled programs. CI stages that archive automatically. Hosted LLVM CI source-builds Oscan and combines it with the packaged LLVM provider plus the matching packaged C toolchain; Linux pins both runtime-archive compilation and final linking to that package's musl GCC/binutils so static release binaries remain ABI-compatible and portable. ```powershell -# Default: native on supported Windows/Linux, C on macOS +# Choose from installed compiler metadata .\build.ps1 -# Deliberate C backend fallback +# Explicit backends +.\build.ps1 -Backend llvm -NativeTarget host +.\build.ps1 -Backend cranelift -NativeTarget host .\build.ps1 -Backend c -# Native build for an explicit target tag -.\build.ps1 -Backend native -NativeTarget host - -# Native release build: optimize bundled C inputs/final link and strip the binary -.\build.ps1 -Backend native -OptimizeSize +# Object-backend release build: optimize bundled C inputs/final link and strip +.\build.ps1 -Backend llvm -OptimizeSize ``` -Trusted Windows CI/release jobs pass `-AllowElevatedNativeLink`, which forwards Oscan's explicit `--allow-elevated-native-link` opt-in for GitHub-hosted runners that execute with an elevated token. Local builds do not pass this switch by default, so Oscan's elevated native-link protection stays visible and enforced for normal users; the switch is rejected for the C backend. +`-Backend native` remains a warning-producing compatibility alias for `cranelift`; it never selects LLVM. -`-OptimizeSize` is an opt-in for native Windows/Linux release artifacts. It passes separate flags through Oscan's `--extra-cflags` mechanism: `-Oz -ffunction-sections -fdata-sections -s` on Windows and `-Os -ffunction-sections -fdata-sections -s` on Linux. These flags optimize bundled C inputs and the final compiler-driver link, including stripping the executable; they do not change Cranelift code generation. Linux keeps its required `-D_GNU_SOURCE -static` flags. Normal local builds remain unchanged, and the release workflow enables the switch only for native Windows/Linux jobs. +Trusted Windows CI/release jobs pass `-AllowElevatedNativeLink`, which forwards Oscan's explicit `--allow-elevated-native-link` opt-in. `-OptimizeSize` is available for LLVM and Cranelift on Windows/Linux; it optimizes bundled C inputs and the final compiler-driver link. Linux object builds retain `-D_GNU_SOURCE -static`. The Release workflow's `dry_run` dispatch input builds and tests every release target without publishing. ## Quick Start @@ -410,14 +410,15 @@ supported CSS feature. # Run tests through a specific backend .\build.ps1 -Backend c -Test -.\build.ps1 -Backend native -Test +.\build.ps1 -Backend llvm -Test +.\build.ps1 -Backend cranelift -Test # Run individual test suites oscan tests/test_url.osc --run oscan tests/test_html.osc --run ``` -On Linux, `test_js.osc` is skipped only for the legacy C/freestanding path because QuickJS is known to crash there. Native Linux tests use hosted libc and should run the JS coverage once the required Oscan native extern shims are available. +On Linux, `test_js.osc` is skipped only for the C/freestanding path because QuickJS is known to crash there. Hosted LLVM and Cranelift builds run the JS coverage. ### Real-world page regression suite @@ -570,12 +571,12 @@ APIs, and the persistent HTTP cache. .\build.ps1 -Test ``` -2. Match the existing Oscan style — see [`.github/copilot-instructions.md`](.github/copilot-instructions.md) if present, or the conventions documented throughout this README (no `arena { }` around `push()` on persistent arrays, in-place `while len(arr) > 0 { pop(arr); };` clearing, etc.). +2. Match the existing Oscan style — see [`.github/copilot-instructions.md`](.github/copilot-instructions.md) if present, or the conventions documented throughout this README (no allocating mutation of persistent arrays inside `arena { }`, use `array_clear` for in-place clearing, etc.). 3. Keep commits focused and include a short description of what and why. ## Built With -- **[Oscan](https://github.com/lucabol/Oscan)** — Minimalist language designed for LLM code generation, compiling to C99 +- **[Oscan](https://github.com/lucabol/Oscan)** — Minimalist language designed for LLM code generation, with LLVM, Cranelift, and C backends - **[QuickJS-ng](https://github.com/nicotordev/quickjs-ng)** — Lightweight JavaScript engine (embedded via C bridge) - **[Vimium](https://github.com/philc/vimium)** — Inspiration for the keyboard shortcut scheme diff --git a/browser.osc b/browser.osc index 5972bb9..1cfb71a 100644 --- a/browser.osc +++ b/browser.osc @@ -82,32 +82,33 @@ let TTC_LEN: i32 = 14; // mode is on, effective size is ~70% of base so the 8pt/12pt chrome // button visibly shrinks TT text too. fn! tt_recache(tt_reg: handle, tt_mono: handle, tt_head: handle, tt_cfg: [i32], small_font: bool) { + let mut tt_cfg_mut: [i32] = tt_cfg; let body_base: i32 = tt_cfg[TTC_BASE_BODY_PX]; let mono_base: i32 = tt_cfg[TTC_BASE_MONO_PX]; let mut body_eff: i32 = if small_font { (body_base * 7) / 10 } else { body_base }; let mut mono_eff: i32 = if small_font { (mono_base * 7) / 10 } else { mono_base }; if body_eff < 8 { body_eff = 8; }; if mono_eff < 8 { mono_eff = 8; }; - tt_cfg[TTC_BODY_PX] = body_eff; - tt_cfg[TTC_MONO_PX] = mono_eff; + tt_cfg_mut[TTC_BODY_PX] = body_eff; + tt_cfg_mut[TTC_MONO_PX] = mono_eff; if (tt_reg as i64) != (0 as i64) { - tt_cfg[TTC_REG_ASCENT] = tt_ascent(tt_reg, body_eff as f64); - tt_cfg[TTC_REG_LINEH] = tt_line_height(tt_reg, body_eff as f64); - tt_cfg[TTC_REG_SPACEW] = tt_text_width(tt_reg, " ", body_eff as f64); + tt_cfg_mut[TTC_REG_ASCENT] = tt_ascent(tt_reg, body_eff as f64); + tt_cfg_mut[TTC_REG_LINEH] = tt_line_height(tt_reg, body_eff as f64); + tt_cfg_mut[TTC_REG_SPACEW] = tt_text_width(tt_reg, " ", body_eff as f64); }; if (tt_mono as i64) != (0 as i64) { - tt_cfg[TTC_MONO_ASCENT] = tt_ascent(tt_mono, mono_eff as f64); - tt_cfg[TTC_MONO_LINEH] = tt_line_height(tt_mono, mono_eff as f64); - tt_cfg[TTC_MONO_SPACEW] = tt_text_width(tt_mono, " ", mono_eff as f64); + tt_cfg_mut[TTC_MONO_ASCENT] = tt_ascent(tt_mono, mono_eff as f64); + tt_cfg_mut[TTC_MONO_LINEH] = tt_line_height(tt_mono, mono_eff as f64); + tt_cfg_mut[TTC_MONO_SPACEW] = tt_text_width(tt_mono, " ", mono_eff as f64); }; if (tt_head as i64) != (0 as i64) { - tt_cfg[TTC_HEAD_ASCENT] = tt_ascent(tt_head, body_eff as f64); - tt_cfg[TTC_HEAD_LINEH] = tt_line_height(tt_head, body_eff as f64); - tt_cfg[TTC_HEAD_SPACEW] = tt_text_width(tt_head, " ", body_eff as f64); + tt_cfg_mut[TTC_HEAD_ASCENT] = tt_ascent(tt_head, body_eff as f64); + tt_cfg_mut[TTC_HEAD_LINEH] = tt_line_height(tt_head, body_eff as f64); + tt_cfg_mut[TTC_HEAD_SPACEW] = tt_text_width(tt_head, " ", body_eff as f64); } else { - tt_cfg[TTC_HEAD_ASCENT] = 0; - tt_cfg[TTC_HEAD_LINEH] = 0; - tt_cfg[TTC_HEAD_SPACEW] = 0; + tt_cfg_mut[TTC_HEAD_ASCENT] = 0; + tt_cfg_mut[TTC_HEAD_LINEH] = 0; + tt_cfg_mut[TTC_HEAD_SPACEW] = 0; }; } @@ -484,6 +485,9 @@ fn! img_scale(src_px: [i32], src_w: i32, src_h: i32, dst_w: i32, dst_h: i32) -> fn! img_fetch_and_decode(src: str, base_scheme: str, base_host: str, base_port: i32, base_path: str, img_cache_idx: map, img_cache_data: [[i32]], img_cache_w: [i32], img_cache_h: [i32]) -> i32 { + let mut img_cache_data_mut: [[i32]] = img_cache_data; + let mut img_cache_w_mut: [i32] = img_cache_w; + let mut img_cache_h_mut: [i32] = img_cache_h; // Check cache if map_has(img_cache_idx, src) { let idx_str: str = map_get(img_cache_idx, src); @@ -609,10 +613,10 @@ fn! img_fetch_and_decode(src: str, base_scheme: str, base_host: str, base_port: final_px = img_scale(dec_px, dec_w, dec_h, final_w, final_h); }; - let cache_idx: i32 = len(img_cache_data); - push(img_cache_data, final_px); - push(img_cache_w, final_w); - push(img_cache_h, final_h); + let cache_idx: i32 = len(img_cache_data_mut); + push(img_cache_data_mut, final_px); + push(img_cache_w_mut, final_w); + push(img_cache_h_mut, final_h); let idx_str: str = i32_to_str(cache_idx); map_set(img_cache_idx, src, idx_str); cache_idx @@ -651,6 +655,9 @@ fn! flush_pre_segment(segment_chars: [i32], x_ptr: [i32], y_ptr: [i32], links: [LinkInfo], link_href: str, in_chrome: bool, theme: [i32], tt_reg: handle, tt_mono: handle, tt_head: handle, tt_cfg: [i32]) { + let mut x_ptr_mut: [i32] = x_ptr; + let mut word_map_mut: [WordInfo] = word_map; + let mut links_mut: [LinkInfo] = links; let gh: i32 = tt_glyph_h_of(scale, ds, tt_reg, tt_cfg, font); let lh: i32 = tt_line_h_of(scale, ds, tt_reg, tt_cfg, font); let draw_y: i32 = y_ptr[0] - vp_top + CHROME_TOP * ds; @@ -674,12 +681,12 @@ fn! flush_pre_segment(segment_chars: [i32], x_ptr: [i32], y_ptr: [i32], x: x_ptr[0], y: draw_y, w: seg_w, h: lh, href: link_href, label: "", chrome: in_chrome, }; - push(links, li); + push(links_mut, li); }; let wi: WordInfo = WordInfo { x: x_ptr[0], y: draw_y, w: seg_w, text: seg }; - push(word_map, wi); + push(word_map_mut, wi); }; - x_ptr[0] = x_ptr[0] + seg_w; + x_ptr_mut[0] = x_ptr_mut[0] + seg_w; } else if x_ptr[0] == line_left { // Empty line: still paint the full-width band so the block reads // as one cohesive code rectangle rather than disconnected stripes. @@ -700,6 +707,10 @@ fn! render_text(text: str, x_ptr: [i32], y_ptr: [i32], color: i32, indent: i32, in_chrome: bool, theme: [i32], tt_reg: handle, tt_mono: handle, tt_head: handle, tt_cfg: [i32]) { + let mut x_ptr_mut: [i32] = x_ptr; + let mut y_ptr_mut: [i32] = y_ptr; + let mut links_mut: [LinkInfo] = links; + let mut word_map_mut: [WordInfo] = word_map; // Normalize whitespace: collapse \n, \r, \t into spaces so split by space // doesn't emit them as visible "words" (which render as missing-glyph ?). let raw_chars: [i32] = str_to_chars(text); @@ -741,12 +752,12 @@ fn! render_text(text: str, x_ptr: [i32], y_ptr: [i32], color: i32, x: link_x, y: link_y, w: link_w, h: lh, href: href, label: "", chrome: in_chrome, }; - push(links, li); + push(links_mut, li); link_recorded = true; }; }; - x_ptr[0] = indent + PAD * ds; - y_ptr[0] = y_ptr[0] + lh; + x_ptr_mut[0] = indent + PAD * ds; + y_ptr_mut[0] = y_ptr_mut[0] + lh; }; let draw_y: i32 = y_ptr[0] - vp_top + CHROME_TOP * ds; @@ -801,7 +812,7 @@ fn! render_text(text: str, x_ptr: [i32], y_ptr: [i32], color: i32, }; // Record word position for text selection let wi: WordInfo = WordInfo { x: x_ptr[0], y: draw_y, w: word_w, text: word }; - push(word_map, wi); + push(word_map_mut, wi); }; // Update link bounds @@ -809,7 +820,7 @@ fn! render_text(text: str, x_ptr: [i32], y_ptr: [i32], color: i32, link_w = (x_ptr[0] + word_w) - link_x; }; - x_ptr[0] = x_ptr[0] + word_w + gap; + x_ptr_mut[0] = x_ptr_mut[0] + word_w + gap; }; // Emit single LinkInfo for the entire link text @@ -819,7 +830,7 @@ fn! render_text(text: str, x_ptr: [i32], y_ptr: [i32], color: i32, x: link_x, y: link_y, w: link_w, h: lh, href: href, label: "", chrome: in_chrome, }; - push(links, li); + push(links_mut, li); }; }; } @@ -831,6 +842,7 @@ fn! render_text(text: str, x_ptr: [i32], y_ptr: [i32], color: i32, fn! measure_inline_text(nodes: [HtmlNode], idx: i32, styles: [ComputedStyle], font: i32, scale: i32, ds: i32, out: [i32], tt_reg: handle, tt_mono: handle, tt_head: handle, tt_cfg: [i32]) { + let mut out_mut: [i32] = out; if idx < 0 or idx >= len(nodes) { return; }; if idx < len(styles) and styles[idx].display_none { return; }; let node: HtmlNode = nodes[idx]; @@ -856,13 +868,13 @@ fn! measure_inline_text(nodes: [HtmlNode], idx: i32, styles: [ComputedStyle], while len(norm) > 0 and norm[len(norm) - 1] == 32 { pop(norm); }; if len(norm) == 0 { return; }; let collapsed: str = str_from_chars(norm); - out[0] = out[0] + text_width_f(collapsed, font, scale, ds, tt_reg, tt_mono, tt_head, tt_cfg); + out_mut[0] = out_mut[0] + text_width_f(collapsed, font, scale, ds, tt_reg, tt_mono, tt_head, tt_cfg); return; }; if html_is_skip_tag(node.tag) { return; }; let mut child: i32 = node.first_child; while child != -1 { - measure_inline_text(nodes, child, styles, font, scale, ds, out, tt_reg, tt_mono, tt_head, tt_cfg); + measure_inline_text(nodes, child, styles, font, scale, ds, out_mut, tt_reg, tt_mono, tt_head, tt_cfg); child = nodes[child].next_sibling; }; } @@ -1003,6 +1015,12 @@ fn! render_node(nodes: [HtmlNode], idx: i32, x_ptr: [i32], y_ptr: [i32], body_mono_font: i32, theme: [i32], tt_reg: handle, tt_mono: handle, tt_head: handle, tt_cfg: [i32]) { + let mut x_ptr_mut: [i32] = x_ptr; + let mut y_ptr_mut: [i32] = y_ptr; + let mut ol_counter_mut: [i32] = ol_counter; + let mut clickables_mut: [ClickInfo] = clickables; + let mut fragment_y_out_mut: [i32] = fragment_y_out; + let mut outline_y_out_mut: [i32] = outline_y_out; let node: HtmlNode = nodes[idx]; // Compute whether this node is itself chrome (nav/aside/header/footer, @@ -1057,7 +1075,7 @@ fn! render_node(nodes: [HtmlNode], idx: i32, x_ptr: [i32], y_ptr: [i32], }; }; if matches { - fragment_y_out[0] = y_ptr[0]; + fragment_y_out_mut[0] = y_ptr[0]; }; }; @@ -1067,7 +1085,7 @@ fn! render_node(nodes: [HtmlNode], idx: i32, x_ptr: [i32], y_ptr: [i32], if not node.is_text and len(outline_idx) > 0 { for oi in 0..len(outline_idx) { if outline_idx[oi] == idx and outline_y_out[oi] < 0 { - outline_y_out[oi] = y_ptr[0]; + outline_y_out_mut[oi] = y_ptr[0]; break; }; }; @@ -1097,9 +1115,9 @@ fn! render_node(nodes: [HtmlNode], idx: i32, x_ptr: [i32], y_ptr: [i32], links, link_href, in_chrome, theme, tt_reg, tt_mono, tt_head, tt_cfg); - while len(segment_chars) > 0 { pop(segment_chars); }; - y_ptr[0] = y_ptr[0] + pre_lh; - x_ptr[0] = line_left; + array_clear(segment_chars); + y_ptr_mut[0] = y_ptr[0] + pre_lh; + x_ptr_mut[0] = line_left; } else if c == 9 { // Tab → 4 spaces. No tab-stop alignment; real-world // verbatim blocks (Wikipedia, MDN) indent with spaces @@ -1190,13 +1208,13 @@ fn! render_node(nodes: [HtmlNode], idx: i32, x_ptr: [i32], y_ptr: [i32], if is_block { if x_ptr[0] > eff_indent + PAD * ds { - y_ptr[0] = y_ptr[0] + entry_line_h; + y_ptr_mut[0] = y_ptr[0] + entry_line_h; }; - x_ptr[0] = eff_indent + PAD * ds; + x_ptr_mut[0] = eff_indent + PAD * ds; }; if is_block and pad_top_px > 0 { - y_ptr[0] = y_ptr[0] + pad_top_px; - x_ptr[0] = eff_indent + PAD * ds; + y_ptr_mut[0] = y_ptr[0] + pad_top_px; + x_ptr_mut[0] = eff_indent + PAD * ds; }; let css_margin_top_zero: bool = idx >= 0 and idx < len(styles) and styles[idx].margin_t == 0; @@ -1206,28 +1224,28 @@ fn! render_node(nodes: [HtmlNode], idx: i32, x_ptr: [i32], y_ptr: [i32], // the built-in user-agent spacing for dense surfaces like search results. if (str_eq(tag, "h1") or str_eq(tag, "h2") or str_eq(tag, "h3")) and not css_margin_top_zero { - y_ptr[0] = y_ptr[0] + entry_line_h; + y_ptr_mut[0] = y_ptr[0] + entry_line_h; }; if str_eq(tag, "p") and not css_margin_top_zero { - y_ptr[0] = y_ptr[0] + entry_line_h / 2; + y_ptr_mut[0] = y_ptr[0] + entry_line_h / 2; }; // HR if str_eq(tag, "hr") { - y_ptr[0] = y_ptr[0] + entry_line_h / 2; + y_ptr_mut[0] = y_ptr[0] + entry_line_h / 2; let draw_y: i32 = y_ptr[0] - vp_top + CHROME_TOP * ds; if draw_y >= CHROME_TOP * ds and draw_y < canvas_height() - CHROME_BOT * ds { gfx_line(PAD * ds, draw_y, eff_vp_width - PAD * ds, draw_y, theme[T_HR]); }; - y_ptr[0] = y_ptr[0] + entry_line_h / 2; - x_ptr[0] = eff_indent + PAD * ds; + y_ptr_mut[0] = y_ptr[0] + entry_line_h / 2; + x_ptr_mut[0] = eff_indent + PAD * ds; return; }; // BR if str_eq(tag, "br") { - y_ptr[0] = y_ptr[0] + entry_line_h; - x_ptr[0] = eff_indent + PAD * ds; + y_ptr_mut[0] = y_ptr[0] + entry_line_h; + x_ptr_mut[0] = eff_indent + PAD * ds; return; }; @@ -1312,16 +1330,16 @@ fn! render_node(nodes: [HtmlNode], idx: i32, x_ptr: [i32], y_ptr: [i32], // Start on new line for block images if x_ptr[0] > indent + PAD * ds { - x_ptr[0] = indent + PAD * ds; - y_ptr[0] = y_ptr[0] + LINE_H * ds; + x_ptr_mut[0] = indent + PAD * ds; + y_ptr_mut[0] = y_ptr[0] + LINE_H * ds; }; let draw_y: i32 = y_ptr[0] - vp_top + CHROME_TOP * ds; if draw_y + draw_h >= CHROME_TOP * ds and draw_y < canvas_height() - CHROME_BOT * ds { gfx_blit(x_ptr[0], draw_y, draw_w, draw_h, draw_px); }; - y_ptr[0] = y_ptr[0] + draw_h + 4 * ds; - x_ptr[0] = indent + PAD * ds; + y_ptr_mut[0] = y_ptr[0] + draw_h + 4 * ds; + x_ptr_mut[0] = indent + PAD * ds; return; }; }; @@ -1332,7 +1350,7 @@ fn! render_node(nodes: [HtmlNode], idx: i32, x_ptr: [i32], y_ptr: [i32], if draw_y + FONT_H * ds >= CHROME_TOP * ds and draw_y < canvas_height() - CHROME_BOT * ds { draw_text_f(x_ptr[0], draw_y, display, theme[T_HR], font, scale, ds, tt_reg, tt_mono, tt_head, tt_cfg); }; - x_ptr[0] = x_ptr[0] + text_width_f(display, font, scale, ds, tt_reg, tt_mono, tt_head, tt_cfg) + text_width_f(" ", font, scale, ds, tt_reg, tt_mono, tt_head, tt_cfg); + x_ptr_mut[0] = x_ptr[0] + text_width_f(display, font, scale, ds, tt_reg, tt_mono, tt_head, tt_cfg) + text_width_f(" ", font, scale, ds, tt_reg, tt_mono, tt_head, tt_cfg); return; }; @@ -1342,8 +1360,8 @@ fn! render_node(nodes: [HtmlNode], idx: i32, x_ptr: [i32], y_ptr: [i32], let tr_idxs: [i32] = collect_tr_indices(nodes, node); if len(tr_idxs) == 0 { // Empty table — skip default block handling. - y_ptr[0] = y_ptr[0] + LINE_H * ds; - x_ptr[0] = eff_indent + PAD * ds; + y_ptr_mut[0] = y_ptr[0] + LINE_H * ds; + x_ptr_mut[0] = eff_indent + PAD * ds; return; }; @@ -1441,7 +1459,7 @@ fn! render_node(nodes: [HtmlNode], idx: i32, x_ptr: [i32], y_ptr: [i32], if tbl_top_y >= CHROME_TOP * ds and tbl_top_y < canvas_height() - CHROME_BOT * ds { gfx_line(eff_indent + PAD * ds, tbl_top_y, eff_indent + PAD * ds + avail_w, tbl_top_y, theme[T_TABLE_BORDER]); }; - y_ptr[0] = y_ptr[0] + 2 * ds; + y_ptr_mut[0] = y_ptr[0] + 2 * ds; // Pass 2: render each row with word-wrap; row height = max line count. for ri2 in 0..len(rows_cells) { @@ -1497,8 +1515,8 @@ fn! render_node(nodes: [HtmlNode], idx: i32, x_ptr: [i32], y_ptr: [i32], // LINE_H here would produce double-spacing on sites // (HN, RSS readers) that pack many short rows. if x_ptr[0] > eff_indent + PAD * ds { - y_ptr[0] = y_ptr[0] + LINE_H * ds; - x_ptr[0] = eff_indent + PAD * ds; + y_ptr_mut[0] = y_ptr[0] + LINE_H * ds; + x_ptr_mut[0] = eff_indent + PAD * ds; }; continue; }; @@ -1543,7 +1561,7 @@ fn! render_node(nodes: [HtmlNode], idx: i32, x_ptr: [i32], y_ptr: [i32], }; }; }; - y_ptr[0] = y_ptr[0] + LINE_H * ds * max_lines; + y_ptr_mut[0] = y_ptr[0] + LINE_H * ds * max_lines; // Header separator: draw after the first row if it's all/mostly . if ri2 == 0 and row_any_th { @@ -1551,7 +1569,7 @@ fn! render_node(nodes: [HtmlNode], idx: i32, x_ptr: [i32], y_ptr: [i32], if sep_y >= CHROME_TOP * ds and sep_y < canvas_height() - CHROME_BOT * ds { gfx_line(eff_indent + PAD * ds, sep_y, eff_indent + PAD * ds + avail_w, sep_y, theme[T_TABLE_BORDER]); }; - y_ptr[0] = y_ptr[0] + 2 * ds; + y_ptr_mut[0] = y_ptr[0] + 2 * ds; }; }; @@ -1560,8 +1578,8 @@ fn! render_node(nodes: [HtmlNode], idx: i32, x_ptr: [i32], y_ptr: [i32], if tbl_bot_y >= CHROME_TOP * ds and tbl_bot_y < canvas_height() - CHROME_BOT * ds { gfx_line(eff_indent + PAD * ds, tbl_bot_y, eff_indent + PAD * ds + avail_w, tbl_bot_y, theme[T_TABLE_BORDER]); }; - y_ptr[0] = y_ptr[0] + LINE_H * ds; - x_ptr[0] = eff_indent + PAD * ds; + y_ptr_mut[0] = y_ptr[0] + LINE_H * ds; + x_ptr_mut[0] = eff_indent + PAD * ds; return; }; @@ -1662,7 +1680,7 @@ fn! render_node(nodes: [HtmlNode], idx: i32, x_ptr: [i32], y_ptr: [i32], // List item: numbered or bulleted if str_eq(tag, "li") { - x_ptr[0] = eff_indent + PAD * ds; + x_ptr_mut[0] = eff_indent + PAD * ds; let draw_y: i32 = y_ptr[0] - vp_top + CHROME_TOP * ds; if ol_counter[0] > 0 { let num_str: str = i32_to_str(ol_counter[0]); @@ -1670,13 +1688,13 @@ fn! render_node(nodes: [HtmlNode], idx: i32, x_ptr: [i32], y_ptr: [i32], if draw_y + tt_glyph_h_of(child_scale, ds, tt_reg, tt_cfg, child_font) >= CHROME_TOP * ds and draw_y < canvas_height() - CHROME_BOT * ds { draw_text_f(x_ptr[0], draw_y, li_label, child_color, child_font, child_scale, ds, tt_reg, tt_mono, tt_head, tt_cfg); }; - x_ptr[0] = x_ptr[0] + text_width_f(li_label, child_font, child_scale, ds, tt_reg, tt_mono, tt_head, tt_cfg); - ol_counter[0] = ol_counter[0] + 1; + x_ptr_mut[0] = x_ptr[0] + text_width_f(li_label, child_font, child_scale, ds, tt_reg, tt_mono, tt_head, tt_cfg); + ol_counter_mut[0] = ol_counter[0] + 1; } else { if draw_y + tt_glyph_h_of(child_scale, ds, tt_reg, tt_cfg, child_font) >= CHROME_TOP * ds and draw_y < canvas_height() - CHROME_BOT * ds { draw_text_f(x_ptr[0], draw_y, "* ", child_color, child_font, child_scale, ds, tt_reg, tt_mono, tt_head, tt_cfg); }; - x_ptr[0] = x_ptr[0] + text_width_f("* ", child_font, child_scale, ds, tt_reg, tt_mono, tt_head, tt_cfg); + x_ptr_mut[0] = x_ptr[0] + text_width_f("* ", child_font, child_scale, ds, tt_reg, tt_mono, tt_head, tt_cfg); }; }; @@ -1695,9 +1713,9 @@ fn! render_node(nodes: [HtmlNode], idx: i32, x_ptr: [i32], y_ptr: [i32], let avail: i32 = line_end - line_start; if measured > 0 and measured <= avail { if ta == 1 { - x_ptr[0] = line_start + (avail - measured) / 2; + x_ptr_mut[0] = line_start + (avail - measured) / 2; } else { - x_ptr[0] = line_end - measured; + x_ptr_mut[0] = line_end - measured; }; }; }; @@ -1743,19 +1761,19 @@ fn! render_node(nodes: [HtmlNode], idx: i32, x_ptr: [i32], y_ptr: [i32], h: onclick_h, node_idx: idx, }; - push(clickables, cci); + push(clickables_mut, cci); }; // Block post-spacing (includes CSS padding-bottom). if is_block { - x_ptr[0] = eff_indent + PAD * ds; - y_ptr[0] = y_ptr[0] + effective_line_h(styles, idx, child_scale, ds, tt_reg, tt_cfg, child_font); + x_ptr_mut[0] = eff_indent + PAD * ds; + y_ptr_mut[0] = y_ptr[0] + effective_line_h(styles, idx, child_scale, ds, tt_reg, tt_cfg, child_font); if pad_bot_px > 0 { - y_ptr[0] = y_ptr[0] + pad_bot_px; + y_ptr_mut[0] = y_ptr[0] + pad_bot_px; }; }; if (str_eq(tag, "h1") or str_eq(tag, "h2")) and not css_margin_bottom_zero { - y_ptr[0] = y_ptr[0] + effective_line_h(styles, idx, child_scale, ds, tt_reg, tt_cfg, child_font) / 2; + y_ptr_mut[0] = y_ptr[0] + effective_line_h(styles, idx, child_scale, ds, tt_reg, tt_cfg, child_font) / 2; }; // Paragraph spacing: pre-p already adds half a line (line ~945); // block post-spacing adds a full line. That's 1.5 lines between @@ -1845,6 +1863,9 @@ fn! dom_has_chrome(nodes: [HtmlNode], start_idx: i32) -> bool { // coordinate is filled later by render_node via outline_y_out. fn! build_outline(nodes: [HtmlNode], start_idx: i32, o_idx: [i32], o_level: [i32], o_text: [str]) { + let mut o_idx_mut: [i32] = o_idx; + let mut o_level_mut: [i32] = o_level; + let mut o_text_mut: [str] = o_text; if start_idx < 0 or start_idx >= len(nodes) { return; }; let node: HtmlNode = nodes[start_idx]; if not node.is_text { @@ -1856,20 +1877,20 @@ fn! build_outline(nodes: [HtmlNode], start_idx: i32, if lvl > 0 { let txt: str = str_trim(html_get_text_content(nodes, start_idx)); if str_len(txt) > 0 { - push(o_idx, start_idx); - push(o_level, lvl); + push(o_idx_mut, start_idx); + push(o_level_mut, lvl); let disp: str = if str_len(txt) > 60 { str_concat(str_slice(txt, 0, 60), "…") } else { txt }; - push(o_text, disp); + push(o_text_mut, disp); }; }; }; let mut child: i32 = node.first_child; while child != -1 { - build_outline(nodes, child, o_idx, o_level, o_text); + build_outline(nodes, child, o_idx_mut, o_level_mut, o_text_mut); child = nodes[child].next_sibling; }; } @@ -2380,17 +2401,17 @@ fn! main() { // If select-all is active, first printable key or backspace replaces all if url_select_all and key > 0 and key != 1001 and key != 1002 and key != 1003 and key != 1004 and key != KEY_ENTER and key != KEY_ESC { if key >= 32 and key <= 126 { - while len(url_buf) > 0 { pop(url_buf); }; + array_clear(url_buf); push(url_buf, key); url_cursor = 1; url_select_all = false; } else if key == 8 or key == 127 { - while len(url_buf) > 0 { pop(url_buf); }; + array_clear(url_buf); url_cursor = 0; url_select_all = false; } else if key == 22 { // Ctrl+V replaces selection - while len(url_buf) > 0 { pop(url_buf); }; + array_clear(url_buf); url_cursor = 0; url_select_all = false; let paste_sel: Result = clipboard_get(); @@ -2568,7 +2589,7 @@ fn! main() { // the render pass find the target. pending_fragment = pending_u.fragment; current_url = nav_url; - while len(url_buf) > 0 { pop(url_buf); }; + array_clear(url_buf); let uc_s: [i32] = str_to_chars(current_url); for ci in 0..len(uc_s) { push(url_buf, uc_s[ci]); }; url_cursor = len(url_buf); @@ -2604,7 +2625,7 @@ fn! main() { if str_len(current_url) > 0 and not str_eq(current_url, nav_url) { push(history_back, current_url); - while len(history_forward) > 0 { pop(history_forward); }; + array_clear(history_forward); }; current_url = nav_url; @@ -2783,13 +2804,13 @@ fn! main() { // status in status_msg so the user knows the request // wasn't a clean 200. let render_status: i32 = resp.status_code; - while len(dom_nodes) > 0 { pop(dom_nodes); }; - while len(dom_styles) > 0 { pop(dom_styles); }; + array_clear(dom_nodes); + array_clear(dom_styles); // Clear image cache for new page img_cache_idx = map_new(); - while len(img_cache_data) > 0 { pop(img_cache_data); }; - while len(img_cache_w) > 0 { pop(img_cache_w); }; - while len(img_cache_h) > 0 { pop(img_cache_h); }; + array_clear(img_cache_data); + array_clear(img_cache_w); + array_clear(img_cache_h); vlog_msg("parse start", ""); dom_root = html_parse(dom_nodes, resp.body); vlog_int("parse nodes", len(dom_nodes)); @@ -2821,12 +2842,12 @@ fn! main() { // picked up. Clearing is in-place (mutating the // same array) to avoid the realloc-on-child-arena // bug documented elsewhere. - while len(page_forms) > 0 { pop(page_forms); }; - while len(page_fields) > 0 { pop(page_fields); }; - while len(form_fill_order) > 0 { pop(form_fill_order); }; + array_clear(page_forms); + array_clear(page_fields); + array_clear(form_fill_order); form_fill_target = -1; form_fill_pos = 0; - while len(form_input_buf) > 0 { pop(form_input_buf); }; + array_clear(form_input_buf); if dom_root >= 0 { forms_collect(dom_nodes, dom_root, page_forms, page_fields); vlog_int("forms", len(page_forms)); @@ -2888,10 +2909,10 @@ fn! main() { // Rebuild outline from the page's headings so `t` // can jump between them. Populated now; y values // are filled in during render_node. - while len(outline_idx) > 0 { pop(outline_idx); }; - while len(outline_y) > 0 { pop(outline_y); }; - while len(outline_text) > 0 { pop(outline_text); }; - while len(outline_level) > 0 { pop(outline_level); }; + array_clear(outline_idx); + array_clear(outline_y); + array_clear(outline_text); + array_clear(outline_level); build_outline(dom_nodes, dom_root, outline_idx, outline_level, outline_text); for _oy in 0..len(outline_idx) { push(outline_y, -1); }; @@ -2944,41 +2965,41 @@ fn! main() { // new URL, which looks like the browser silently "went back" // or hung. if load_failed { - while len(dom_nodes) > 0 { pop(dom_nodes); }; - while len(dom_styles) > 0 { pop(dom_styles); }; + array_clear(dom_nodes); + array_clear(dom_styles); img_cache_idx = map_new(); - while len(img_cache_data) > 0 { pop(img_cache_data); }; - while len(img_cache_w) > 0 { pop(img_cache_w); }; - while len(img_cache_h) > 0 { pop(img_cache_h); }; + array_clear(img_cache_data); + array_clear(img_cache_w); + array_clear(img_cache_h); let err_body: str = "

Failed to load page

URL: {nav_url}

Error: {load_error}

Press H to go back, o to open a new URL, or r to retry.

"; dom_root = html_parse(dom_nodes, err_body); page_loaded = true; scroll_y = 0; - while len(page_forms) > 0 { pop(page_forms); }; - while len(page_fields) > 0 { pop(page_fields); }; - while len(form_fill_order) > 0 { pop(form_fill_order); }; + array_clear(page_forms); + array_clear(page_fields); + array_clear(form_fill_order); form_fill_target = -1; form_fill_pos = 0; - while len(form_input_buf) > 0 { pop(form_input_buf); }; + array_clear(form_input_buf); let mut err_rules: [CssRule] = []; css_compute_all(dom_nodes, dom_root, err_rules, dom_styles); - while len(outline_idx) > 0 { pop(outline_idx); }; - while len(outline_y) > 0 { pop(outline_y); }; - while len(outline_text) > 0 { pop(outline_text); }; - while len(outline_level) > 0 { pop(outline_level); }; + array_clear(outline_idx); + array_clear(outline_y); + array_clear(outline_text); + array_clear(outline_level); build_outline(dom_nodes, dom_root, outline_idx, outline_level, outline_text); for _oy in 0..len(outline_idx) { push(outline_y, -1); }; has_chrome = false; }; - while len(url_buf) > 0 { pop(url_buf); }; + array_clear(url_buf); let uc: [i32] = str_to_chars(current_url); - for ci in 0..len(uc) { push(url_buf, uc[ci]); }; + array_extend(url_buf, uc); url_cursor = len(url_buf); // Force a repaint this frame. The fetch above drew a @@ -3005,10 +3026,8 @@ fn! main() { push(dedup, old); }; }; - while len(persistent_history) > 0 { pop(persistent_history); }; - for ph2 in 0..len(dedup) { - push(persistent_history, dedup[ph2]); - }; + array_clear(persistent_history); + array_extend(persistent_history, dedup); storage_save_lines("oscaweb_history.txt", persistent_history); }; }; // end else (not same-page fragment) @@ -3022,7 +3041,7 @@ fn! main() { show_help = false; search_active = false; search_query = ""; - while len(search_match_y) > 0 { pop(search_match_y); }; + array_clear(search_match_y); } else if key == 106 { scroll_y += LINE_H * ds * 3; if scroll_y > max_scroll { scroll_y = max_scroll; }; @@ -3048,8 +3067,8 @@ fn! main() { } else if key == 102 and last_key != 103 { if page_loaded { mode = BrowserMode::Follow; - while len(follow_input) > 0 { pop(follow_input); }; - while len(follow_links) > 0 { pop(follow_links); }; + array_clear(follow_input); + array_clear(follow_links); let labels: [str] = make_hint_labels(len(page_links)); for li in 0..len(page_links) { let lk: LinkInfo = page_links[li]; @@ -3110,7 +3129,7 @@ fn! main() { render_dirty = true; } else { form_fill_target = 0; - while len(form_fill_order) > 0 { pop(form_fill_order); }; + array_clear(form_fill_order); for ffi in 0..len(page_fields) { let f: FormField = page_fields[ffi]; if f.form_idx == form_fill_target and @@ -3124,7 +3143,7 @@ fn! main() { status_msg = "Form has no editable fields"; } else { form_fill_pos = 0; - while len(form_input_buf) > 0 { pop(form_input_buf); }; + array_clear(form_input_buf); let init_f: FormField = page_fields[form_fill_order[0]]; let init_chars: [i32] = str_to_chars(form_display_value(init_f)); for ici in 0..len(init_chars) { @@ -3176,8 +3195,8 @@ fn! main() { scroll_y = 0; pending_follow_chrome = false; mode = BrowserMode::Normal; - while len(follow_input) > 0 { pop(follow_input); }; - while len(follow_links) > 0 { pop(follow_links); }; + array_clear(follow_input); + array_clear(follow_links); if str_len(current_url) > 0 { nav_url = current_url; needs_navigate = true; @@ -3210,7 +3229,7 @@ fn! main() { // 'o' — open URL (clear bar) mode = BrowserMode::Insert; url_focused = true; - while len(url_buf) > 0 { pop(url_buf); }; + array_clear(url_buf); url_cursor = 0; url_select_all = false; } else if key == 79 { @@ -3221,7 +3240,7 @@ fn! main() { url_select_all = true; } else if key == 47 { mode = BrowserMode::Search; - while len(search_buf) > 0 { pop(search_buf); }; + array_clear(search_buf); search_active = true; } else if key == 63 { show_help = not show_help; @@ -3294,19 +3313,13 @@ fn! main() { // Prepend new bookmark let mut with_new: [str] = []; push(with_new, current_url); - for bi2 in 0..len(new_bm) { - push(with_new, new_bm[bi2]); - }; - while len(bookmarks) > 0 { pop(bookmarks); }; - for bi3 in 0..len(with_new) { - push(bookmarks, with_new[bi3]); - }; + array_extend(with_new, new_bm); + array_clear(bookmarks); + array_extend(bookmarks, with_new); status_msg = "Bookmarked: {current_url}"; } else { - while len(bookmarks) > 0 { pop(bookmarks); }; - for bi4 in 0..len(new_bm) { - push(bookmarks, new_bm[bi4]); - }; + array_clear(bookmarks); + array_extend(bookmarks, new_bm); status_msg = "Removed bookmark: {current_url}"; }; storage_save_lines("oscaweb_bookmarks.txt", bookmarks); @@ -3398,8 +3411,8 @@ fn! main() { // and return to normal. Symmetric with the F toggle // in Normal mode. mode = BrowserMode::Normal; - while len(follow_input) > 0 { pop(follow_input); }; - while len(follow_links) > 0 { pop(follow_links); }; + array_clear(follow_input); + array_clear(follow_links); if show_full { show_full = false; scroll_y = 0; @@ -3439,11 +3452,11 @@ fn! main() { mode = BrowserMode::Normal; search_active = false; search_query = ""; - while len(search_match_y) > 0 { pop(search_match_y); }; + array_clear(search_match_y); } else if key == KEY_ENTER { search_query = str_to_lower(str_from_chars(search_buf)); // Build match positions by scanning DOM text - while len(search_match_y) > 0 { pop(search_match_y); }; + array_clear(search_match_y); search_match_idx = 0; if str_len(search_query) > 0 and dom_root >= 0 { // Collect all text content and find matches by line @@ -3478,7 +3491,7 @@ fn! main() { // values in place (they persist in page_fields), // but drop the in-progress buffer. mode = BrowserMode::Normal; - while len(form_input_buf) > 0 { pop(form_input_buf); }; + array_clear(form_input_buf); status_msg = "Form fill cancelled"; } else if key == KEY_ENTER { // Commit current input buffer into the active field. @@ -3499,7 +3512,7 @@ fn! main() { }; }; form_fill_pos = form_fill_pos + 1; - while len(form_input_buf) > 0 { pop(form_input_buf); }; + array_clear(form_input_buf); if form_fill_pos >= len(form_fill_order) { // Find the first non-disabled submit-style @@ -3567,7 +3580,7 @@ fn! main() { let pick: i32 = key - 97; if pick < len(page_forms) { form_fill_target = pick; - while len(form_fill_order) > 0 { pop(form_fill_order); }; + array_clear(form_fill_order); for fpi in 0..len(page_fields) { let fp: FormField = page_fields[fpi]; if fp.form_idx == form_fill_target and @@ -3582,7 +3595,7 @@ fn! main() { mode = BrowserMode::Normal; } else { form_fill_pos = 0; - while len(form_input_buf) > 0 { pop(form_input_buf); }; + array_clear(form_input_buf); let pinit: FormField = page_fields[form_fill_order[0]]; let pchars: [i32] = str_to_chars(form_display_value(pinit)); for pci in 0..len(pchars) { @@ -3844,12 +3857,12 @@ fn! main() { // Page content if page_loaded and dom_root >= 0 { - while len(page_links) > 0 { pop(page_links); }; - while len(page_clickables) > 0 { pop(page_clickables); }; + array_clear(page_links); + array_clear(page_clickables); let x_ptr: [i32] = [PAD * ds]; let y_ptr: [i32] = [0]; let pg_u: Url = url_parse(current_url); - while len(word_map) > 0 { pop(word_map); }; + array_clear(word_map); // Find the main content subtree. Used by both reader mode // and the default view: when a landmark (
/
) // is present, we render from there and skip all the site @@ -3991,7 +4004,7 @@ fn! main() { // substrings of persistent history. Down/Up cycle; Enter opens // the highlighted entry (or the literal typed text when none is // selected). - while len(ac_matches) > 0 { pop(ac_matches); }; + array_clear(ac_matches); let in_insert: bool = match mode { BrowserMode::Insert => true, _ => false, @@ -4292,8 +4305,8 @@ fn! main() { if len(pfc_chrome) > 0 { pending_follow_chrome = false; mode = BrowserMode::Follow; - while len(follow_input) > 0 { pop(follow_input); }; - while len(follow_links) > 0 { pop(follow_links); }; + array_clear(follow_input); + array_clear(follow_links); let pfc_labels: [str] = make_hint_labels(len(pfc_chrome)); for pfi2 in 0..len(pfc_chrome) { let pfc_lk: LinkInfo = pfc_chrome[pfi2]; diff --git a/build.ps1 b/build.ps1 index b790f2d..c2c01c0 100644 --- a/build.ps1 +++ b/build.ps1 @@ -12,14 +12,15 @@ .\build.ps1 -Clean # Remove build artifacts .\build.ps1 -Test # Compile and run tests .\build.ps1 -Backend c # Deliberate C backend fallback - .\build.ps1 -OptimizeSize # Optimize and strip a native release binary + .\build.ps1 -Backend llvm # Explicit hosted LLVM build + .\build.ps1 -OptimizeSize # Optimize and strip an object-backend release binary .\build.ps1 -V # Verbose build (show oscan/compiler output) #> param( [switch]$Run, [switch]$Clean, [switch]$Test, - [ValidateSet('auto', 'native', 'c')] + [ValidateSet('auto', 'llvm', 'cranelift', 'native', 'c')] [string]$Backend = 'auto', [string]$NativeTarget = 'host', [string]$Output, @@ -59,7 +60,7 @@ if (-not (Get-Command 'oscan' -ErrorAction SilentlyContinue)) { $BuildConfig = Get-OscaWebBuildConfig -Backend $Backend -NativeTarget $NativeTarget -AllowElevatedNativeLink:$AllowElevatedNativeLink Write-Step "Using $($BuildConfig.Backend) backend" -if ($BuildConfig.Backend -eq 'native') { +if ($BuildConfig.Backend -in @('llvm', 'cranelift')) { Write-Host " Native target: $($BuildConfig.NativeTarget), runtime: hosted libc" -ForegroundColor DarkGray } @@ -86,7 +87,7 @@ if ($Test) { $failed = 0 foreach ($tf in $testFiles) { # The legacy C backend on Linux still links QuickJS through the - # freestanding musl path, which is known to crash. Native builds use + # freestanding musl path, which is known to crash. Object builds use # --libc hosted mode and should run this coverage. if ($IsLinux -and $BuildConfig.Backend -eq 'c' -and $tf.Name -eq 'test_js.osc') { Write-Host " Skipping $($tf.Name) on Linux C backend (known freestanding QuickJS issue)" -ForegroundColor Yellow diff --git a/cookies.osc b/cookies.osc index b4f41b6..ee47702 100644 --- a/cookies.osc +++ b/cookies.osc @@ -92,32 +92,26 @@ fn! cookies_find(names: [str], domains: [str], paths: [str], -1 } -// Remove cookie at index idx by shifting subsequent entries down. -// Mirrors http_cache_invalidate's in-place shift (Oscan arrays have -// pop() but no remove-at-index). +// Remove cookie at index idx from every parallel array. fn! cookies_remove_at(names: [str], values: [str], domains: [str], paths: [str], secure: [bool], host_only: [bool], persistent: [bool], idx: i32) { + let mut names_mut: [str] = names; + let mut values_mut: [str] = values; + let mut domains_mut: [str] = domains; + let mut paths_mut: [str] = paths; + let mut secure_mut: [bool] = secure; + let mut host_only_mut: [bool] = host_only; + let mut persistent_mut: [bool] = persistent; let n: i32 = len(names); if idx < 0 or idx >= n { return; }; - let mut i: i32 = idx; - while i < n - 1 { - names[i] = names[i + 1]; - values[i] = values[i + 1]; - domains[i] = domains[i + 1]; - paths[i] = paths[i + 1]; - secure[i] = secure[i + 1]; - host_only[i] = host_only[i + 1]; - persistent[i] = persistent[i + 1]; - i += 1; - }; - pop(names); - pop(values); - pop(domains); - pop(paths); - pop(secure); - pop(host_only); - pop(persistent); + array_remove_at(names_mut, idx); + array_remove_at(values_mut, idx); + array_remove_at(domains_mut, idx); + array_remove_at(paths_mut, idx); + array_remove_at(secure_mut, idx); + array_remove_at(host_only_mut, idx); + array_remove_at(persistent_mut, idx); } // Parse a single `Set-Cookie: ...` header VALUE (the bit after @@ -130,6 +124,13 @@ fn! cookies_parse_set_cookie(names: [str], values: [str], domains: [str], paths: [str], secure: [bool], host_only: [bool], persistent: [bool], req_host: str, line: str) { + let mut names_mut: [str] = names; + let mut values_mut: [str] = values; + let mut domains_mut: [str] = domains; + let mut paths_mut: [str] = paths; + let mut secure_mut: [bool] = secure; + let mut host_only_mut: [bool] = host_only; + let mut persistent_mut: [bool] = persistent; let trimmed: str = str_trim(line); if str_len(trimmed) == 0 { return; }; @@ -213,18 +214,18 @@ fn! cookies_parse_set_cookie(names: [str], values: [str], domains: [str], // Upsert. let existing: i32 = cookies_find(names, domains, paths, name, eff_domain, attr_path); if existing >= 0 { - values[existing] = value; - secure[existing] = attr_secure; - host_only[existing] = eff_host_only; - persistent[existing] = eff_persistent; + values_mut[existing] = value; + secure_mut[existing] = attr_secure; + host_only_mut[existing] = eff_host_only; + persistent_mut[existing] = eff_persistent; } else { - push(names, name); - push(values, value); - push(domains, eff_domain); - push(paths, attr_path); - push(secure, attr_secure); - push(host_only, eff_host_only); - push(persistent, eff_persistent); + push(names_mut, name); + push(values_mut, value); + push(domains_mut, eff_domain); + push(paths_mut, attr_path); + push(secure_mut, attr_secure); + push(host_only_mut, eff_host_only); + push(persistent_mut, eff_persistent); }; } @@ -257,13 +258,20 @@ fn! cookies_build_header(names: [str], values: [str], domains: [str], fn! cookies_clear_all(names: [str], values: [str], domains: [str], paths: [str], secure: [bool], host_only: [bool], persistent: [bool]) { - while len(names) > 0 { pop(names); }; - while len(values) > 0 { pop(values); }; - while len(domains) > 0 { pop(domains); }; - while len(paths) > 0 { pop(paths); }; - while len(secure) > 0 { pop(secure); }; - while len(host_only) > 0 { pop(host_only); }; - while len(persistent) > 0 { pop(persistent); }; + let mut names_mut: [str] = names; + let mut values_mut: [str] = values; + let mut domains_mut: [str] = domains; + let mut paths_mut: [str] = paths; + let mut secure_mut: [bool] = secure; + let mut host_only_mut: [bool] = host_only; + let mut persistent_mut: [bool] = persistent; + array_clear(names_mut); + array_clear(values_mut); + array_clear(domains_mut); + array_clear(paths_mut); + array_clear(secure_mut); + array_clear(host_only_mut); + array_clear(persistent_mut); let empty: [str] = []; storage_save_lines(cookies_filename(), empty); } @@ -294,19 +302,26 @@ fn! cookies_save(names: [str], values: [str], domains: [str], fn! cookies_load(names: [str], values: [str], domains: [str], paths: [str], secure: [bool], host_only: [bool], persistent: [bool]) { + let mut names_mut: [str] = names; + let mut values_mut: [str] = values; + let mut domains_mut: [str] = domains; + let mut paths_mut: [str] = paths; + let mut secure_mut: [bool] = secure; + let mut host_only_mut: [bool] = host_only; + let mut persistent_mut: [bool] = persistent; let lines: [str] = storage_load_lines(cookies_filename()); let n: i32 = len(lines); let mut i: i32 = 0; while i < n { let parts: [str] = str_split(lines[i], "\t"); if len(parts) >= 6 { - push(names, parts[0]); - push(values, parts[1]); - push(domains, parts[2]); - push(paths, parts[3]); - push(secure, str_eq(parts[4], "1")); - push(host_only, str_eq(parts[5], "1")); - push(persistent, true); + push(names_mut, parts[0]); + push(values_mut, parts[1]); + push(domains_mut, parts[2]); + push(paths_mut, parts[3]); + push(secure_mut, str_eq(parts[4], "1")); + push(host_only_mut, str_eq(parts[5], "1")); + push(persistent_mut, true); }; i += 1; }; diff --git a/css.osc b/css.osc index 0e74ccf..84b2edb 100644 --- a/css.osc +++ b/css.osc @@ -282,8 +282,10 @@ let CSS_BASE_LINE_H: i32 = 20; // legacy default; now only used as a bitmap-era // Supported units: "px" and "" (unitless -> treated as px for spacing, // callers are free to reinterpret for line-height). "%" sets the pct path. fn! css_parse_length(raw: str, px_out: [i32], pct_out: [i32]) { - px_out[0] = -1; - pct_out[0] = -1; + let mut px_out_mut: [i32] = px_out; + let mut pct_out_mut: [i32] = pct_out; + px_out_mut[0] = -1; + pct_out_mut[0] = -1; let v: str = str_trim(str_to_lower(raw)); if str_len(v) == 0 { return; }; let chars: [i32] = str_to_chars(v); @@ -330,7 +332,7 @@ fn! css_parse_length(raw: str, px_out: [i32], pct_out: [i32]) { // round to nearest let pct_r: i32 = (value_x100 + 50) / 100; if pct_r > 0 { - pct_out[0] = pct_r; + pct_out_mut[0] = pct_r; }; return; }; @@ -347,7 +349,7 @@ fn! css_parse_length(raw: str, px_out: [i32], pct_out: [i32]) { let scaled: i32 = value_x100 * unit_x100; let px_r: i32 = (scaled + 5000) / 10000; if px_r >= 0 { - px_out[0] = px_r; + px_out_mut[0] = px_r; }; }; } @@ -363,7 +365,7 @@ fn! css_split_values(s: str) -> [str] { if css_is_ws(c) { if len(buf) > 0 { push(out, str_from_chars(buf)); - while len(buf) > 0 { pop(buf); }; + array_clear(buf); }; } else { push(buf, c); @@ -553,6 +555,8 @@ fn! css_selector_list_supported(s: str) -> bool { // the relation between consecutive tokens: 0 = descendant, 1 = child. // combinators has len = len(out) - 1. fn! css_tokenize_compound(s: str, out: [str], combinators: [i32]) { + let mut out_mut: [str] = out; + let mut combinators_mut: [i32] = combinators; let chars: [i32] = str_to_chars(s); let n: i32 = len(chars); let mut buf: [i32] = []; @@ -566,9 +570,9 @@ fn! css_tokenize_compound(s: str, out: [str], combinators: [i32]) { if c == 93 { depth = depth - 1; }; if depth <= 0 and (css_is_ws(c) or c == 62) { if len(buf) > 0 { - if have_token { push(combinators, pending_combinator); }; - push(out, str_from_chars(buf)); - while len(buf) > 0 { pop(buf); }; + if have_token { push(combinators_mut, pending_combinator); }; + push(out_mut, str_from_chars(buf)); + array_clear(buf); have_token = true; pending_combinator = 0; }; @@ -579,8 +583,8 @@ fn! css_tokenize_compound(s: str, out: [str], combinators: [i32]) { i = i + 1; }; if len(buf) > 0 { - if have_token { push(combinators, pending_combinator); }; - push(out, str_from_chars(buf)); + if have_token { push(combinators_mut, pending_combinator); }; + push(out_mut, str_from_chars(buf)); }; } @@ -610,6 +614,7 @@ fn! css_parse_compound(s: str) -> CssCompound { // ─── Declaration parsing ─────────────────────────────────── fn! css_parse_decls(out_decls: [CssDecl], body_text: str) { + let mut out_decls_mut: [CssDecl] = out_decls; let parts: [str] = str_split(body_text, ";"); for pi in 0..len(parts) { let raw: str = str_trim(parts[pi]); @@ -630,7 +635,7 @@ fn! css_parse_decls(out_decls: [CssDecl], body_text: str) { value: val, important: imp, }; - push(out_decls, d); + push(out_decls_mut, d); }; }; }; @@ -654,6 +659,8 @@ fn! css_parse(rules: [CssRule], source: str, start_order: i32) -> i32 { fn! css_parse_block(rules: [CssRule], source: str, start: i32, end: i32, media: i32, order_ptr: [i32]) { + let mut rules_mut: [CssRule] = rules; + let mut order_ptr_mut: [i32] = order_ptr; let chars: [i32] = str_to_chars(source); let n: i32 = if end < len(chars) { end } else { len(chars) }; let mut i: i32 = start; @@ -703,8 +710,8 @@ fn! css_parse_block(rules: [CssRule], source: str, // already 1 or 2, in which case keep the outer (the // outer filter dominates). let effective: i32 = if media == 0 { sub_media } else { media }; - css_parse_block(rules, source, block_start, block_end, - effective, order_ptr); + css_parse_block(rules_mut, source, block_start, block_end, + effective, order_ptr_mut); }; // Unsupported @media queries: drop the whole block. }; @@ -757,11 +764,11 @@ fn! css_parse_block(rules: [CssRule], source: str, let rule: CssRule = CssRule { selectors: sels, decls: decls, - order: order_ptr[0], + order: order_ptr_mut[0], media: media, }; - push(rules, rule); - order_ptr[0] = order_ptr[0] + 1; + push(rules_mut, rule); + order_ptr_mut[0] = order_ptr_mut[0] + 1; }; }; }; @@ -932,60 +939,61 @@ fn! css_compound_matches(sel: CssCompound, node: HtmlNode, // Apply a single declaration to a ComputedStyle in-place. fn! css_apply_decl(styles: [ComputedStyle], idx: i32, d: CssDecl) { + let mut styles_mut: [ComputedStyle] = styles; let prop: str = d.property; let val: str = str_to_lower(str_trim(d.value)); if str_eq(prop, "color") { let c: i32 = css_parse_color(d.value); - if c >= 0 { styles[idx].color = c; }; + if c >= 0 { styles_mut[idx].color = c; }; return; }; if str_eq(prop, "background-color") or str_eq(prop, "background") { if str_eq(val, "transparent") or str_eq(val, "none") { - styles[idx].bg_color = -1; + styles_mut[idx].bg_color = -1; return; }; let c: i32 = css_parse_color(d.value); - if c >= 0 { styles[idx].bg_color = c; }; + if c >= 0 { styles_mut[idx].bg_color = c; }; return; }; if str_eq(prop, "font-weight") { if str_eq(val, "bold") or str_eq(val, "bolder") or str_eq(val, "700") or str_eq(val, "800") or str_eq(val, "900") { - styles[idx].bold = 1; + styles_mut[idx].bold = 1; } else if str_eq(val, "normal") or str_eq(val, "lighter") or str_eq(val, "400") or str_eq(val, "300") or str_eq(val, "200") or str_eq(val, "100") { - styles[idx].bold = 0; + styles_mut[idx].bold = 0; }; return; }; if str_eq(prop, "font-style") { if str_eq(val, "italic") or str_eq(val, "oblique") { - styles[idx].italic = 1; + styles_mut[idx].italic = 1; } else if str_eq(val, "normal") { - styles[idx].italic = 0; + styles_mut[idx].italic = 0; }; return; }; if str_eq(prop, "font-size") { if str_eq(val, "medium") or str_eq(val, "normal") { - styles[idx].font_scale_x100 = 100; + styles_mut[idx].font_scale_x100 = 100; return; }; if str_eq(val, "small") or str_eq(val, "smaller") { - styles[idx].font_scale_x100 = 85; + styles_mut[idx].font_scale_x100 = 85; return; }; if str_eq(val, "large") or str_eq(val, "larger") { - styles[idx].font_scale_x100 = 120; + styles_mut[idx].font_scale_x100 = 120; return; }; let px_p: [i32] = [-1]; let pct_p: [i32] = [-1]; css_parse_length(d.value, px_p, pct_p); if pct_p[0] > 0 { - styles[idx].font_scale_x100 = pct_p[0]; + styles_mut[idx].font_scale_x100 = pct_p[0]; }; return; }; @@ -1002,24 +1010,24 @@ fn! css_apply_decl(styles: [ComputedStyle], idx: i32, d: CssDecl) { if str_eq(t, "line-through") { saw_lt = true; }; }; if saw_none { - styles[idx].underline = 0; - styles[idx].strike = 0; + styles_mut[idx].underline = 0; + styles_mut[idx].strike = 0; } else { - if saw_u { styles[idx].underline = 1; }; - if saw_lt { styles[idx].strike = 1; }; + if saw_u { styles_mut[idx].underline = 1; }; + if saw_lt { styles_mut[idx].strike = 1; }; }; return; }; if str_eq(prop, "text-align") { - if str_eq(val, "left") { styles[idx].text_align = 0; }; - if str_eq(val, "center") { styles[idx].text_align = 1; }; - if str_eq(val, "right") { styles[idx].text_align = 2; }; - if str_eq(val, "justify") { styles[idx].text_align = 0; }; + if str_eq(val, "left") { styles_mut[idx].text_align = 0; }; + if str_eq(val, "center") { styles_mut[idx].text_align = 1; }; + if str_eq(val, "right") { styles_mut[idx].text_align = 2; }; + if str_eq(val, "justify") { styles_mut[idx].text_align = 0; }; return; }; if str_eq(prop, "display") { if str_eq(val, "none") { - styles[idx].display_none = true; + styles_mut[idx].display_none = true; }; return; }; @@ -1041,10 +1049,10 @@ fn! css_apply_decl(styles: [ComputedStyle], idx: i32, d: CssDecl) { let r: i32 = if m >= 2 { vals[1] } else { vals[0] }; let b: i32 = if m >= 3 { vals[2] } else { t }; let l: i32 = if m >= 4 { vals[3] } else { r }; - styles[idx].pad_t = t; - styles[idx].pad_r = r; - styles[idx].pad_b = b; - styles[idx].pad_l = l; + styles_mut[idx].pad_t = t; + styles_mut[idx].pad_r = r; + styles_mut[idx].pad_b = b; + styles_mut[idx].pad_l = l; }; return; }; @@ -1052,47 +1060,47 @@ fn! css_apply_decl(styles: [ComputedStyle], idx: i32, d: CssDecl) { let px_p: [i32] = [-1]; let pct_p: [i32] = [-1]; css_parse_length(d.value, px_p, pct_p); - if px_p[0] >= 0 { styles[idx].pad_t = px_p[0]; }; + if px_p[0] >= 0 { styles_mut[idx].pad_t = px_p[0]; }; return; }; if str_eq(prop, "padding-right") { let px_p: [i32] = [-1]; let pct_p: [i32] = [-1]; css_parse_length(d.value, px_p, pct_p); - if px_p[0] >= 0 { styles[idx].pad_r = px_p[0]; }; + if px_p[0] >= 0 { styles_mut[idx].pad_r = px_p[0]; }; return; }; if str_eq(prop, "padding-bottom") { let px_p: [i32] = [-1]; let pct_p: [i32] = [-1]; css_parse_length(d.value, px_p, pct_p); - if px_p[0] >= 0 { styles[idx].pad_b = px_p[0]; }; + if px_p[0] >= 0 { styles_mut[idx].pad_b = px_p[0]; }; return; }; if str_eq(prop, "padding-left") { let px_p: [i32] = [-1]; let pct_p: [i32] = [-1]; css_parse_length(d.value, px_p, pct_p); - if px_p[0] >= 0 { styles[idx].pad_l = px_p[0]; }; + if px_p[0] >= 0 { styles_mut[idx].pad_l = px_p[0]; }; return; }; // ── Phase 3: max-width ─────────────────────────────────── if str_eq(prop, "max-width") { if str_eq(val, "none") { - styles[idx].max_width_px = -1; - styles[idx].max_width_pct = -1; + styles_mut[idx].max_width_px = -1; + styles_mut[idx].max_width_pct = -1; return; }; let px_p: [i32] = [-1]; let pct_p: [i32] = [-1]; css_parse_length(d.value, px_p, pct_p); if px_p[0] >= 0 { - styles[idx].max_width_px = px_p[0]; - styles[idx].max_width_pct = -1; + styles_mut[idx].max_width_px = px_p[0]; + styles_mut[idx].max_width_pct = -1; } else if pct_p[0] > 0 { - styles[idx].max_width_px = -1; - styles[idx].max_width_pct = pct_p[0]; + styles_mut[idx].max_width_px = -1; + styles_mut[idx].max_width_pct = pct_p[0]; }; return; }; @@ -1104,19 +1112,19 @@ fn! css_apply_decl(styles: [ComputedStyle], idx: i32, d: CssDecl) { // content rarely improves terminal-style readability. if str_eq(prop, "width") { if str_eq(val, "auto") or str_eq(val, "none") { - styles[idx].width_px = -1; - styles[idx].width_pct = -1; + styles_mut[idx].width_px = -1; + styles_mut[idx].width_pct = -1; return; }; let px_p: [i32] = [-1]; let pct_p: [i32] = [-1]; css_parse_length(d.value, px_p, pct_p); if px_p[0] >= 0 { - styles[idx].width_px = px_p[0]; - styles[idx].width_pct = -1; + styles_mut[idx].width_px = px_p[0]; + styles_mut[idx].width_pct = -1; } else if pct_p[0] > 0 { - styles[idx].width_px = -1; - styles[idx].width_pct = pct_p[0]; + styles_mut[idx].width_px = -1; + styles_mut[idx].width_pct = pct_p[0]; }; return; }; @@ -1130,8 +1138,8 @@ fn! css_apply_decl(styles: [ComputedStyle], idx: i32, d: CssDecl) { // Absolute lengths (px / em / rem / pt) remain in line_height_px. if str_eq(prop, "line-height") { if str_eq(val, "normal") { - styles[idx].line_height_mul_x100 = 120; - styles[idx].line_height_px = -1; + styles_mut[idx].line_height_mul_x100 = 120; + styles_mut[idx].line_height_px = -1; return; }; let raw_l: str = str_trim(str_to_lower(d.value)); @@ -1142,8 +1150,8 @@ fn! css_apply_decl(styles: [ComputedStyle], idx: i32, d: CssDecl) { let pct_p: [i32] = [-1]; css_parse_length(d.value, px_p, pct_p); if has_pct and pct_p[0] > 0 { - styles[idx].line_height_mul_x100 = pct_p[0]; - styles[idx].line_height_px = -1; + styles_mut[idx].line_height_mul_x100 = pct_p[0]; + styles_mut[idx].line_height_px = -1; } else if not has_unit and px_p[0] >= 0 { // Unitless: raw number is the multiplier. Re-scan to recover // the fractional part css_parse_length rounded away. @@ -1169,12 +1177,12 @@ fn! css_apply_decl(styles: [ComputedStyle], idx: i32, d: CssDecl) { }; let mul_x100: i32 = int_v * 100 + frac_x100; if mul_x100 > 0 { - styles[idx].line_height_mul_x100 = mul_x100; - styles[idx].line_height_px = -1; + styles_mut[idx].line_height_mul_x100 = mul_x100; + styles_mut[idx].line_height_px = -1; }; } else if px_p[0] >= 0 { - styles[idx].line_height_px = px_p[0]; - styles[idx].line_height_mul_x100 = -1; + styles_mut[idx].line_height_px = px_p[0]; + styles_mut[idx].line_height_mul_x100 = -1; }; return; }; @@ -1218,16 +1226,16 @@ fn! css_apply_decl(styles: [ComputedStyle], idx: i32, d: CssDecl) { if str_eq(r_tok, "auto") { right_auto = true; }; let tpx: [i32] = [-1]; let tpct: [i32] = [-1]; css_parse_length(t_tok, tpx, tpct); - if tpx[0] >= 0 { styles[idx].margin_t = tpx[0]; }; + if tpx[0] >= 0 { styles_mut[idx].margin_t = tpx[0]; }; let rpx: [i32] = [-1]; let rpct: [i32] = [-1]; css_parse_length(r_tok, rpx, rpct); - if rpx[0] >= 0 { styles[idx].margin_r = rpx[0]; }; + if rpx[0] >= 0 { styles_mut[idx].margin_r = rpx[0]; }; let bpx: [i32] = [-1]; let bpct: [i32] = [-1]; css_parse_length(b_tok, bpx, bpct); - if bpx[0] >= 0 { styles[idx].margin_b = bpx[0]; }; + if bpx[0] >= 0 { styles_mut[idx].margin_b = bpx[0]; }; let lpx: [i32] = [-1]; let lpct: [i32] = [-1]; css_parse_length(l_tok, lpx, lpct); - if lpx[0] >= 0 { styles[idx].margin_l = lpx[0]; }; + if lpx[0] >= 0 { styles_mut[idx].margin_l = lpx[0]; }; } else if m >= 1 { let t: str = toks[0]; if str_eq(prop, "margin-left") { @@ -1238,14 +1246,14 @@ fn! css_apply_decl(styles: [ComputedStyle], idx: i32, d: CssDecl) { let pxo: [i32] = [-1]; let pcto: [i32] = [-1]; css_parse_length(t, pxo, pcto); if pxo[0] >= 0 { - if str_eq(prop, "margin-top") { styles[idx].margin_t = pxo[0]; }; - if str_eq(prop, "margin-right") { styles[idx].margin_r = pxo[0]; }; - if str_eq(prop, "margin-bottom") { styles[idx].margin_b = pxo[0]; }; - if str_eq(prop, "margin-left") { styles[idx].margin_l = pxo[0]; }; + if str_eq(prop, "margin-top") { styles_mut[idx].margin_t = pxo[0]; }; + if str_eq(prop, "margin-right") { styles_mut[idx].margin_r = pxo[0]; }; + if str_eq(prop, "margin-bottom") { styles_mut[idx].margin_b = pxo[0]; }; + if str_eq(prop, "margin-left") { styles_mut[idx].margin_l = pxo[0]; }; }; }; if left_auto and right_auto { - styles[idx].center_block = 1; + styles_mut[idx].center_block = 1; }; return; }; @@ -1332,15 +1340,16 @@ fn! css_cascade_apply(styles: [ComputedStyle], idx: i32, // Apply inheritable properties from parent to child when child has unset. fn! css_inherit(styles: [ComputedStyle], idx: i32, parent: ComputedStyle) { - if styles[idx].color == -1 { styles[idx].color = parent.color; }; - if styles[idx].bold == -1 { styles[idx].bold = parent.bold; }; - if styles[idx].italic == -1 { styles[idx].italic = parent.italic; }; - if styles[idx].underline == -1 { styles[idx].underline = parent.underline; }; - if styles[idx].strike == -1 { styles[idx].strike = parent.strike; }; - if styles[idx].text_align == -1 { styles[idx].text_align = parent.text_align; }; - if styles[idx].line_height_px == -1 { styles[idx].line_height_px = parent.line_height_px; }; - if styles[idx].line_height_mul_x100 == -1 { styles[idx].line_height_mul_x100 = parent.line_height_mul_x100; }; - if styles[idx].font_scale_x100 == -1 { styles[idx].font_scale_x100 = parent.font_scale_x100; }; + let mut styles_mut: [ComputedStyle] = styles; + if styles[idx].color == -1 { styles_mut[idx].color = parent.color; }; + if styles[idx].bold == -1 { styles_mut[idx].bold = parent.bold; }; + if styles[idx].italic == -1 { styles_mut[idx].italic = parent.italic; }; + if styles[idx].underline == -1 { styles_mut[idx].underline = parent.underline; }; + if styles[idx].strike == -1 { styles_mut[idx].strike = parent.strike; }; + if styles[idx].text_align == -1 { styles_mut[idx].text_align = parent.text_align; }; + if styles[idx].line_height_px == -1 { styles_mut[idx].line_height_px = parent.line_height_px; }; + if styles[idx].line_height_mul_x100 == -1 { styles_mut[idx].line_height_mul_x100 = parent.line_height_mul_x100; }; + if styles[idx].font_scale_x100 == -1 { styles_mut[idx].font_scale_x100 = parent.font_scale_x100; }; // bg_color, display_none, padding, max_width, center_block do NOT inherit. } @@ -1351,16 +1360,18 @@ fn! css_compute_walk(nodes: [HtmlNode], idx: i32, styles: [ComputedStyle], parent_style: ComputedStyle, ancestors: [i32]) { + let mut styles_mut: [ComputedStyle] = styles; + let mut ancestors_mut: [i32] = ancestors; if idx < 0 or idx >= len(nodes) { return; }; if nodes[idx].is_text { - styles[idx] = parent_style; + styles_mut[idx] = parent_style; return; }; - styles[idx] = css_empty_style(); + styles_mut[idx] = css_empty_style(); // Cascade from stylesheet rules - css_cascade_apply(styles, idx, rules, nodes[idx], nodes, ancestors); + css_cascade_apply(styles_mut, idx, rules, nodes[idx], nodes, ancestors_mut); // Inline style="..." wins over rule cascade (treated as specificity=inf). let inline_style: str = html_get_attr(nodes[idx], "style"); @@ -1368,53 +1379,55 @@ fn! css_compute_walk(nodes: [HtmlNode], idx: i32, let mut inline_decls: [CssDecl] = []; css_parse_decls(inline_decls, inline_style); for ii in 0..len(inline_decls) { - css_apply_decl(styles, idx, inline_decls[ii]); + css_apply_decl(styles_mut, idx, inline_decls[ii]); }; }; // Inherit unset props from parent - css_inherit(styles, idx, parent_style); + css_inherit(styles_mut, idx, parent_style); // Recurse children with this node's computed style as the parent, // and with `idx` pushed onto the ancestors list. - let here: ComputedStyle = styles[idx]; - push(ancestors, idx); + let here: ComputedStyle = styles_mut[idx]; + push(ancestors_mut, idx); let mut child: i32 = nodes[idx].first_child; while child != -1 { - css_compute_walk(nodes, child, rules, styles, here, ancestors); + css_compute_walk(nodes, child, rules, styles_mut, here, ancestors_mut); child = nodes[child].next_sibling; }; - pop(ancestors); + pop(ancestors_mut); } // Fill `styles` (parallel to `nodes`) with computed styles for every node. // Caller should have cleared `styles` first. fn! css_compute_all(nodes: [HtmlNode], root: i32, rules: [CssRule], styles: [ComputedStyle]) { + let mut styles_mut: [ComputedStyle] = styles; // Pre-fill with defaults so every index is valid. - while len(styles) < len(nodes) { - push(styles, css_empty_style()); + while len(styles_mut) < len(nodes) { + push(styles_mut, css_empty_style()); }; if root < 0 { return; }; let root_style: ComputedStyle = css_empty_style(); let mut ancestors: [i32] = []; - css_compute_walk(nodes, root, rules, styles, root_style, ancestors); + css_compute_walk(nodes, root, rules, styles_mut, root_style, ancestors); } // ─── Stylesheet collection from DOM ──────────────────────── // Walk the DOM and append each