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
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 (