Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `&&`/`||`/`!`.
Expand All @@ -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

Expand All @@ -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)

Expand Down Expand Up @@ -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

Expand Down
186 changes: 177 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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).
Expand All @@ -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 }
Loading