feat: macros (multi-rep, hygiene), Drop field-move, lean multi-OS CI
ci / build (ubuntu) (push) Has been cancelled
ci / unit + fmt (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / goldens + tools (push) Has been cancelled
ci / apps (push) Has been cancelled
ci / selfhost smoke (push) Has been cancelled
ci / macos smoke (push) Has been cancelled
ci / windows smoke (push) Has been cancelled
ci / CI gate (push) Has been cancelled
selfhost-loop / bootstrap determinism (push) Has been cancelled
ci / build (ubuntu) (push) Has been cancelled
ci / unit + fmt (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / goldens + tools (push) Has been cancelled
ci / apps (push) Has been cancelled
ci / selfhost smoke (push) Has been cancelled
ci / macos smoke (push) Has been cancelled
ci / windows smoke (push) Has been cancelled
ci / CI gate (push) Has been cancelled
selfhost-loop / bootstrap determinism (push) Has been cancelled
Sessions 56–69: declarative macro! with rep/zip/literal/block and unhygienic var $name binders; partial field-move skip Drop; @[Release] polish; LSP type hierarchy; CI Nim cache + lean macOS + Windows smoke.
This commit is contained in:
+374
-11
@@ -1,4 +1,6 @@
|
|||||||
# Default PR / main CI: full `make test` (not selfhost fixed-point — see selfhost-loop.yml).
|
# Default PR / main CI: split Linux jobs + lean macOS + Windows bootstrap smoke.
|
||||||
|
# Full sequential suite locally: `make test`.
|
||||||
|
# Selfhost fixed-point: see selfhost-loop.yml (not on every PR).
|
||||||
name: ci
|
name: ci
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -11,42 +13,403 @@ concurrency:
|
|||||||
group: ci-${{ github.workflow }}-${{ github.ref }}
|
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Pin patch for stable toolchain cache keys (was 2.0.x).
|
||||||
|
NIM_VERSION: "2.0.8"
|
||||||
|
# setup-nim-action install dir (relative to workspace)
|
||||||
|
NIM_INSTALL_DIR: ".nim_runtime"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
# ── Shared bootstrap build (Linux) ──────────────────────────────────────
|
||||||
name: make test
|
build:
|
||||||
|
name: build (ubuntu)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 90
|
timeout-minutes: 20
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Cache Nim toolchain
|
||||||
|
id: cache-nim
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ env.NIM_INSTALL_DIR }}
|
||||||
|
key: ${{ runner.os }}-nim-${{ env.NIM_VERSION }}-v1
|
||||||
|
|
||||||
- name: Install Nim
|
- name: Install Nim
|
||||||
|
if: steps.cache-nim.outputs.cache-hit != 'true'
|
||||||
uses: jiro4989/setup-nim-action@v2
|
uses: jiro4989/setup-nim-action@v2
|
||||||
with:
|
with:
|
||||||
nim-version: "2.0.x"
|
nim-version: ${{ env.NIM_VERSION }}
|
||||||
|
nim-install-directory: ${{ env.NIM_INSTALL_DIR }}
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: PATH for cached Nim
|
||||||
|
if: steps.cache-nim.outputs.cache-hit == 'true'
|
||||||
|
run: |
|
||||||
|
echo "$PWD/${{ env.NIM_INSTALL_DIR }}/bin" >> "$GITHUB_PATH"
|
||||||
|
echo "$HOME/.nimble/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Cache nimcache (bootstrap)
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: nimcache
|
||||||
|
key: ${{ runner.os }}-nimcache-build-${{ hashFiles('bootstrap/**/*.nim') }}-v1
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-nimcache-build-
|
||||||
|
|
||||||
- name: Install build deps
|
- name: Install build deps
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y --no-install-recommends \
|
sudo apt-get install -y --no-install-recommends \
|
||||||
gcc make binutils libssl-dev python3
|
gcc make binutils libssl-dev python3
|
||||||
|
|
||||||
- name: Bootstrap + full test suite
|
- name: Build buxc
|
||||||
|
run: |
|
||||||
|
nim -v
|
||||||
|
make build
|
||||||
|
|
||||||
|
- name: Upload buxc
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: buxc-linux
|
||||||
|
path: buxc
|
||||||
|
retention-days: 3
|
||||||
|
|
||||||
|
# ── Parallel Linux suites (reuse prebuilt buxc) ─────────────────────────
|
||||||
|
unit:
|
||||||
|
name: unit + fmt
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 25
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Cache Nim toolchain
|
||||||
|
id: cache-nim
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ env.NIM_INSTALL_DIR }}
|
||||||
|
key: ${{ runner.os }}-nim-${{ env.NIM_VERSION }}-v1
|
||||||
|
|
||||||
|
- name: Install Nim
|
||||||
|
if: steps.cache-nim.outputs.cache-hit != 'true'
|
||||||
|
uses: jiro4989/setup-nim-action@v2
|
||||||
|
with:
|
||||||
|
nim-version: ${{ env.NIM_VERSION }}
|
||||||
|
nim-install-directory: ${{ env.NIM_INSTALL_DIR }}
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: PATH for cached Nim
|
||||||
|
if: steps.cache-nim.outputs.cache-hit == 'true'
|
||||||
|
run: |
|
||||||
|
echo "$PWD/${{ env.NIM_INSTALL_DIR }}/bin" >> "$GITHUB_PATH"
|
||||||
|
echo "$HOME/.nimble/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Cache nimcache (unit tests)
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: nimcache
|
||||||
|
key: ${{ runner.os }}-nimcache-unit-${{ hashFiles('bootstrap/**/*.nim', 'tests/**/*.nim') }}-v1
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-nimcache-unit-
|
||||||
|
${{ runner.os }}-nimcache-build-
|
||||||
|
|
||||||
|
- name: Install build deps
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y --no-install-recommends gcc make libssl-dev
|
||||||
|
|
||||||
|
- name: Download buxc
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: buxc-linux
|
||||||
|
|
||||||
|
- name: Prepare buxc
|
||||||
|
run: chmod +x buxc && ./buxc --version
|
||||||
|
|
||||||
|
- name: fmt-check + unit tests
|
||||||
env:
|
env:
|
||||||
# Ensure registry / selfhost smokes see a clean env
|
BUX_SKIP_BUILD: "1"
|
||||||
BUX_NO_LINE: ""
|
run: |
|
||||||
|
unset BUX_DEBUG_FILE || true
|
||||||
|
make fmt-check BUX_SKIP_BUILD=1
|
||||||
|
make test-unit BUX_SKIP_BUILD=1
|
||||||
|
|
||||||
|
examples:
|
||||||
|
name: examples
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 40
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install build deps
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y --no-install-recommends gcc make libssl-dev
|
||||||
|
- name: Download buxc
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: buxc-linux
|
||||||
|
- name: Prepare buxc
|
||||||
|
run: chmod +x buxc && ./buxc --version
|
||||||
|
- name: test-examples
|
||||||
|
env:
|
||||||
|
BUX_SKIP_BUILD: "1"
|
||||||
|
run: |
|
||||||
|
unset BUX_DEBUG_FILE || true
|
||||||
|
make test-examples BUX_SKIP_BUILD=1
|
||||||
|
|
||||||
|
goldens:
|
||||||
|
name: goldens + tools
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install build deps
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y --no-install-recommends \
|
||||||
|
gcc make binutils libssl-dev python3 gdb
|
||||||
|
- name: Download buxc
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: buxc-linux
|
||||||
|
- name: Prepare buxc
|
||||||
|
run: chmod +x buxc && ./buxc --version
|
||||||
|
- name: errors + stdlib + registry + dwarf
|
||||||
|
env:
|
||||||
|
BUX_SKIP_BUILD: "1"
|
||||||
|
run: |
|
||||||
|
unset BUX_DEBUG_FILE || true
|
||||||
|
make test-errors BUX_SKIP_BUILD=1
|
||||||
|
make test-stdlib BUX_SKIP_BUILD=1
|
||||||
|
make test-registry BUX_SKIP_BUILD=1
|
||||||
|
make test-dwarf BUX_SKIP_BUILD=1
|
||||||
|
make test-drop-move BUX_SKIP_BUILD=1
|
||||||
|
|
||||||
|
apps:
|
||||||
|
name: apps
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 25
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install build deps
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y --no-install-recommends gcc make libssl-dev
|
||||||
|
- name: Download buxc
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: buxc-linux
|
||||||
|
- name: Prepare buxc
|
||||||
|
run: chmod +x buxc && ./buxc --version
|
||||||
|
- name: test-apps
|
||||||
|
env:
|
||||||
|
BUX_SKIP_BUILD: "1"
|
||||||
|
run: |
|
||||||
|
unset BUX_DEBUG_FILE || true
|
||||||
|
make test-apps BUX_SKIP_BUILD=1
|
||||||
|
|
||||||
|
selfhost:
|
||||||
|
name: selfhost smoke
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 45
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install build deps
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y --no-install-recommends gcc make libssl-dev
|
||||||
|
- name: Download buxc
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: buxc-linux
|
||||||
|
- name: Prepare buxc
|
||||||
|
run: chmod +x buxc && ./buxc --version
|
||||||
|
- name: test-selfhost-smoke
|
||||||
|
env:
|
||||||
|
BUX_SKIP_BUILD: "1"
|
||||||
run: |
|
run: |
|
||||||
unset BUX_DEBUG_FILE || true
|
unset BUX_DEBUG_FILE || true
|
||||||
unset BUX_SELFHOST_FIXED_POINT || true
|
unset BUX_SELFHOST_FIXED_POINT || true
|
||||||
make test
|
make test-selfhost-smoke BUX_SKIP_BUILD=1
|
||||||
|
|
||||||
- name: Upload selfhost artifacts on failure
|
- name: Upload selfhost artifacts on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ci-failure-logs
|
name: ci-failure-selfhost
|
||||||
path: |
|
path: |
|
||||||
build/selfhost/build/main.c
|
build/selfhost/build/main.c
|
||||||
_test_tmp_pkg/**
|
_test_tmp_pkg/**
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
# ── macOS smoke (platform matrix) — lean: cache Nim + smoke examples ───
|
||||||
|
# Full EXAMPLES / goldens / selfhost stay on Linux. macOS still builds
|
||||||
|
# bootstrap + runs unit tests + a representative example subset.
|
||||||
|
macos:
|
||||||
|
name: macos smoke
|
||||||
|
runs-on: macos-14
|
||||||
|
timeout-minutes: 35
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Cache Nim toolchain
|
||||||
|
id: cache-nim
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ env.NIM_INSTALL_DIR }}
|
||||||
|
# macOS builds Nim from source — cache is the main time win
|
||||||
|
key: ${{ runner.os }}-nim-${{ env.NIM_VERSION }}-v1
|
||||||
|
|
||||||
|
- name: Install Nim
|
||||||
|
if: steps.cache-nim.outputs.cache-hit != 'true'
|
||||||
|
uses: jiro4989/setup-nim-action@v2
|
||||||
|
with:
|
||||||
|
nim-version: ${{ env.NIM_VERSION }}
|
||||||
|
nim-install-directory: ${{ env.NIM_INSTALL_DIR }}
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: PATH for cached Nim
|
||||||
|
if: steps.cache-nim.outputs.cache-hit == 'true'
|
||||||
|
run: |
|
||||||
|
echo "$PWD/${{ env.NIM_INSTALL_DIR }}/bin" >> "$GITHUB_PATH"
|
||||||
|
echo "$HOME/.nimble/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Cache nimcache (bootstrap + unit)
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: nimcache
|
||||||
|
key: ${{ runner.os }}-nimcache-macos-${{ hashFiles('bootstrap/**/*.nim', 'tests/**/*.nim') }}-v1
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-nimcache-macos-
|
||||||
|
|
||||||
|
- name: OpenSSL (Homebrew)
|
||||||
|
run: |
|
||||||
|
# Prefer already-installed openssl@3 (common on GHA images)
|
||||||
|
if ! brew list openssl@3 &>/dev/null; then
|
||||||
|
brew install openssl@3
|
||||||
|
fi
|
||||||
|
OPENSSL_PREFIX="$(brew --prefix openssl@3)"
|
||||||
|
echo "OPENSSL_PREFIX=$OPENSSL_PREFIX" >> "$GITHUB_ENV"
|
||||||
|
# buxc passes BUX_CFLAGS through to cc (needed for Homebrew libcrypto)
|
||||||
|
echo "BUX_CFLAGS=-I${OPENSSL_PREFIX}/include -L${OPENSSL_PREFIX}/lib" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Build buxc
|
||||||
|
run: |
|
||||||
|
nim -v
|
||||||
|
make build
|
||||||
|
|
||||||
|
- name: unit + smoke examples
|
||||||
|
run: |
|
||||||
|
unset BUX_DEBUG_FILE || true
|
||||||
|
# fmt-check is Linux-only in split CI (unit job); skip here to save time
|
||||||
|
make test-unit
|
||||||
|
make test-examples-smoke
|
||||||
|
|
||||||
|
# ── Windows smoke (bootstrap + pure Nim unit tests) ─────────────────────
|
||||||
|
# Full bux→C examples need POSIX runtime (ucontext / pthread sockets in
|
||||||
|
# rt/runtime.c) — not ported yet. This job still catches Nim/bootstrap
|
||||||
|
# regressions on Windows (prebuilt Nim zip = fast install).
|
||||||
|
windows:
|
||||||
|
name: windows smoke
|
||||||
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Cache Nim toolchain
|
||||||
|
id: cache-nim
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ env.NIM_INSTALL_DIR }}
|
||||||
|
key: ${{ runner.os }}-nim-${{ env.NIM_VERSION }}-v1
|
||||||
|
|
||||||
|
- name: Install Nim
|
||||||
|
if: steps.cache-nim.outputs.cache-hit != 'true'
|
||||||
|
uses: jiro4989/setup-nim-action@v2
|
||||||
|
with:
|
||||||
|
nim-version: ${{ env.NIM_VERSION }}
|
||||||
|
nim-install-directory: ${{ env.NIM_INSTALL_DIR }}
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: PATH for cached Nim
|
||||||
|
if: steps.cache-nim.outputs.cache-hit == 'true'
|
||||||
|
run: |
|
||||||
|
echo "$PWD/${{ env.NIM_INSTALL_DIR }}/bin" >> "$GITHUB_PATH"
|
||||||
|
echo "$HOME/.nimble/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Cache nimcache (bootstrap + unit)
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: nimcache
|
||||||
|
key: ${{ runner.os }}-nimcache-win-${{ hashFiles('bootstrap/**/*.nim', 'tests/**/*.nim') }}-v1
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-nimcache-win-
|
||||||
|
|
||||||
|
- name: Build buxc
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
nim -v
|
||||||
|
# Windows produces buxc.exe; keep name predictable for the smoke steps
|
||||||
|
nim c --nimcache:nimcache -o:buxc.exe -d:release --opt:size bootstrap/main.nim
|
||||||
|
./buxc.exe --version
|
||||||
|
|
||||||
|
- name: Pure Nim unit tests + CLI smoke
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
unset BUX_DEBUG_FILE || true
|
||||||
|
export NIMFLAGS=--nimcache:nimcache
|
||||||
|
echo "Running lexer tests..."
|
||||||
|
nim c $NIMFLAGS -r tests/lexer_test.nim
|
||||||
|
echo "Running parser tests..."
|
||||||
|
nim c $NIMFLAGS -r tests/parser_test.nim
|
||||||
|
echo "Running sema tests..."
|
||||||
|
nim c $NIMFLAGS -r tests/sema_test.nim
|
||||||
|
echo "Running HIR tests..."
|
||||||
|
nim c $NIMFLAGS -r tests/hir_test.nim
|
||||||
|
echo "Running borrow checker tests..."
|
||||||
|
nim c $NIMFLAGS -r tests/borrow_test.nim
|
||||||
|
echo "CLI smoke..."
|
||||||
|
rm -rf _test_tmp_pkg
|
||||||
|
./buxc.exe new _test_tmp_pkg
|
||||||
|
./buxc.exe --version
|
||||||
|
echo "windows smoke: PASS (bootstrap + unit + CLI)"
|
||||||
|
|
||||||
|
# ── Single required status for branch protection ────────────────────────
|
||||||
|
ci-gate:
|
||||||
|
name: CI gate
|
||||||
|
if: always()
|
||||||
|
needs: [unit, examples, goldens, apps, selfhost, macos, windows]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: All jobs green?
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
echo "unit=${{ needs.unit.result }}"
|
||||||
|
echo "examples=${{ needs.examples.result }}"
|
||||||
|
echo "goldens=${{ needs.goldens.result }}"
|
||||||
|
echo "apps=${{ needs.apps.result }}"
|
||||||
|
echo "selfhost=${{ needs.selfhost.result }}"
|
||||||
|
echo "macos=${{ needs.macos.result }}"
|
||||||
|
echo "windows=${{ needs.windows.result }}"
|
||||||
|
for r in \
|
||||||
|
"${{ needs.unit.result }}" \
|
||||||
|
"${{ needs.examples.result }}" \
|
||||||
|
"${{ needs.goldens.result }}" \
|
||||||
|
"${{ needs.apps.result }}" \
|
||||||
|
"${{ needs.selfhost.result }}" \
|
||||||
|
"${{ needs.macos.result }}" \
|
||||||
|
"${{ needs.windows.result }}"; do
|
||||||
|
if [ "$r" != "success" ]; then
|
||||||
|
echo "CI gate failed: a required job is $r"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "CI gate: all required jobs passed"
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ concurrency:
|
|||||||
group: selfhost-loop-${{ github.ref }}
|
group: selfhost-loop-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
NIM_VERSION: "2.0.8"
|
||||||
|
NIM_INSTALL_DIR: ".nim_runtime"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
selfhost-loop:
|
selfhost-loop:
|
||||||
name: bootstrap determinism
|
name: bootstrap determinism
|
||||||
@@ -37,12 +41,36 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Cache Nim toolchain
|
||||||
|
id: cache-nim
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ env.NIM_INSTALL_DIR }}
|
||||||
|
key: ${{ runner.os }}-nim-${{ env.NIM_VERSION }}-v1
|
||||||
|
|
||||||
- name: Install Nim
|
- name: Install Nim
|
||||||
|
if: steps.cache-nim.outputs.cache-hit != 'true'
|
||||||
uses: jiro4989/setup-nim-action@v2
|
uses: jiro4989/setup-nim-action@v2
|
||||||
with:
|
with:
|
||||||
nim-version: "2.0.x"
|
nim-version: ${{ env.NIM_VERSION }}
|
||||||
|
nim-install-directory: ${{ env.NIM_INSTALL_DIR }}
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: PATH for cached Nim
|
||||||
|
if: steps.cache-nim.outputs.cache-hit == 'true'
|
||||||
|
run: |
|
||||||
|
echo "$PWD/${{ env.NIM_INSTALL_DIR }}/bin" >> "$GITHUB_PATH"
|
||||||
|
echo "$HOME/.nimble/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Cache nimcache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: nimcache
|
||||||
|
key: ${{ runner.os }}-nimcache-loop-${{ hashFiles('bootstrap/**/*.nim', 'src/**/*.bux') }}-v1
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-nimcache-loop-
|
||||||
|
${{ runner.os }}-nimcache-build-
|
||||||
|
|
||||||
- name: Install build deps
|
- name: Install build deps
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
|
|||||||
@@ -32,3 +32,4 @@ _test_*/
|
|||||||
|
|
||||||
# Log files
|
# Log files
|
||||||
*.log
|
*.log
|
||||||
|
.nim_runtime/
|
||||||
|
|||||||
@@ -2,41 +2,63 @@ NIM := nim
|
|||||||
SRC := bootstrap/main.nim
|
SRC := bootstrap/main.nim
|
||||||
OUT := buxc
|
OUT := buxc
|
||||||
BUILD_DIR := build
|
BUILD_DIR := build
|
||||||
|
# Project-local nimcache so CI can cache compiles (default is ~/.cache/nim).
|
||||||
|
NIMFLAGS ?= --nimcache:nimcache
|
||||||
|
|
||||||
EXAMPLES := hello fibonacci factorial structs enums methods algebraic_enums generics generics_struct generic_infer generic_infer2 extend_generic pattern_matching strings strings2 map result_option try_operator ownership ownership_checked drop_early_return lifetime_elision ctfe async concurrency os_time process json iter trait_bounds channel sync jwt stdlib_ergonomics tuples func_ptr map_remove array_iter_extra string_extra multi_closure iter_hof closure_control match_let string_interp iter_generic generic_infer_hof struct_tuple_pat match_block nested_patterns match_guards pattern_shadow move_field
|
EXAMPLES := hello fibonacci factorial structs enums methods algebraic_enums generics generics_struct generic_infer generic_infer2 extend_generic pattern_matching strings strings2 map result_option try_operator ownership ownership_checked ownership_release drop_early_return lifetime_elision ctfe async concurrency os_time process json iter trait_bounds channel sync jwt stdlib_ergonomics tuples func_ptr map_remove array_iter_extra string_extra multi_closure iter_hof closure_control match_let string_interp iter_generic generic_infer_hof struct_tuple_pat match_block nested_patterns match_guards pattern_shadow move_field move_field_partial c_precedence macro_twice macro_repeat macro_nested macro_hygiene macro_unhygienic
|
||||||
|
|
||||||
.PHONY: all build dev debug test clean clean-all test-examples selfhost test-golden test-errors test-stdlib selfhost-loop lsp fmt-check docs bench test-apps test-dwarf test-selfhost-smoke
|
# Platform smoke (macOS CI): full EXAMPLES still runs on Linux.
|
||||||
|
EXAMPLES_SMOKE := hello ownership ownership_release strings map move_field move_field_partial c_precedence macro_twice macro_repeat macro_nested macro_hygiene macro_unhygienic
|
||||||
|
|
||||||
|
.PHONY: all build dev debug test clean clean-all test-examples test-examples-smoke selfhost test-golden test-errors test-stdlib selfhost-loop lsp fmt-check docs bench test-apps test-dwarf test-selfhost-smoke test-unit ensure-buxc
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
build:
|
# Rebuild only when bootstrap sources change (CI can set BUX_SKIP_BUILD=1
|
||||||
$(NIM) c -o:$(OUT) -d:release --opt:size $(SRC)
|
# after downloading a prebuilt buxc artifact).
|
||||||
|
$(OUT): $(wildcard bootstrap/*.nim)
|
||||||
|
$(NIM) c $(NIMFLAGS) -o:$(OUT) -d:release --opt:size $(SRC)
|
||||||
# strip $(OUT)
|
# strip $(OUT)
|
||||||
|
|
||||||
|
build: $(OUT)
|
||||||
|
|
||||||
|
# CI parallel jobs download buxc and set BUX_SKIP_BUILD=1 to avoid rebuild.
|
||||||
|
ensure-buxc:
|
||||||
|
ifeq ($(BUX_SKIP_BUILD),1)
|
||||||
|
@test -x ./$(OUT) || (echo "error: ./$(OUT) missing (BUX_SKIP_BUILD=1)"; exit 1)
|
||||||
|
else
|
||||||
|
@$(MAKE) $(OUT)
|
||||||
|
endif
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
$(NIM) c -o:buxc_debug -d:debug --stackTrace:on --lineTrace:on $(SRC)
|
$(NIM) c $(NIMFLAGS) -o:buxc_debug -d:debug --stackTrace:on --lineTrace:on $(SRC)
|
||||||
|
|
||||||
debug: dev
|
debug: dev
|
||||||
@echo "Debug binary: buxc_debug"
|
@echo "Debug binary: buxc_debug"
|
||||||
|
|
||||||
test: build fmt-check test-examples test-errors test-stdlib test-registry test-dwarf test-apps test-selfhost-smoke
|
# Full local / sequential suite (same coverage as split CI jobs combined).
|
||||||
|
test: build fmt-check test-examples test-errors test-stdlib test-registry test-dwarf test-drop-move test-apps test-selfhost-smoke test-unit
|
||||||
|
|
||||||
|
# Nim unit tests + tiny CLI smoke (needs Nim + buxc).
|
||||||
|
test-unit: ensure-buxc
|
||||||
@echo "Running lexer tests..."
|
@echo "Running lexer tests..."
|
||||||
$(NIM) c -r tests/lexer_test.nim
|
$(NIM) c $(NIMFLAGS) -r tests/lexer_test.nim
|
||||||
@echo "Running parser tests..."
|
@echo "Running parser tests..."
|
||||||
$(NIM) c -r tests/parser_test.nim
|
$(NIM) c $(NIMFLAGS) -r tests/parser_test.nim
|
||||||
@echo "Running sema tests..."
|
@echo "Running sema tests..."
|
||||||
$(NIM) c -r tests/sema_test.nim
|
$(NIM) c $(NIMFLAGS) -r tests/sema_test.nim
|
||||||
@echo "Running HIR tests..."
|
@echo "Running HIR tests..."
|
||||||
$(NIM) c -r tests/hir_test.nim
|
$(NIM) c $(NIMFLAGS) -r tests/hir_test.nim
|
||||||
@echo "Running borrow checker tests..."
|
@echo "Running borrow checker tests..."
|
||||||
$(NIM) c -r tests/borrow_test.nim
|
$(NIM) c $(NIMFLAGS) -r tests/borrow_test.nim
|
||||||
@echo "Running integration tests..."
|
@echo "Running integration tests..."
|
||||||
rm -rf _test_tmp_pkg
|
rm -rf _test_tmp_pkg
|
||||||
./$(OUT) new _test_tmp_pkg
|
./$(OUT) new _test_tmp_pkg
|
||||||
./$(OUT) --version
|
./$(OUT) --version
|
||||||
|
|
||||||
test-examples: build
|
# Shared loop body for full + smoke example runners.
|
||||||
@for ex in $(EXAMPLES); do \
|
define run-examples
|
||||||
|
@for ex in $(1); do \
|
||||||
echo "=== Testing example: $$ex ==="; \
|
echo "=== Testing example: $$ex ==="; \
|
||||||
mkdir -p examples_pkg/$$ex/src; \
|
mkdir -p examples_pkg/$$ex/src; \
|
||||||
cp examples/$$ex.bux examples_pkg/$$ex/src/Main.bux; \
|
cp examples/$$ex.bux examples_pkg/$$ex/src/Main.bux; \
|
||||||
@@ -49,10 +71,23 @@ test-examples: build
|
|||||||
echo '[Build]' >> examples_pkg/$$ex/bux.toml; \
|
echo '[Build]' >> examples_pkg/$$ex/bux.toml; \
|
||||||
echo 'Output = "Bin"' >> examples_pkg/$$ex/bux.toml; \
|
echo 'Output = "Bin"' >> examples_pkg/$$ex/bux.toml; \
|
||||||
fi; \
|
fi; \
|
||||||
(cd examples_pkg/$$ex && timeout 10 ../../$(OUT) run) || exit 1; \
|
if command -v timeout >/dev/null 2>&1; then \
|
||||||
|
(cd examples_pkg/$$ex && timeout 30 ../../$(OUT) run) || exit 1; \
|
||||||
|
else \
|
||||||
|
(cd examples_pkg/$$ex && ../../$(OUT) run) || exit 1; \
|
||||||
|
fi; \
|
||||||
done
|
done
|
||||||
|
endef
|
||||||
|
|
||||||
|
test-examples: ensure-buxc
|
||||||
|
$(call run-examples,$(EXAMPLES))
|
||||||
@echo "All examples passed!"
|
@echo "All examples passed!"
|
||||||
|
|
||||||
|
# Subset for macOS / quick platform smoke (Linux CI runs full EXAMPLES).
|
||||||
|
test-examples-smoke: ensure-buxc
|
||||||
|
$(call run-examples,$(EXAMPLES_SMOKE))
|
||||||
|
@echo "Smoke examples passed!"
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f $(OUT) buxc_debug
|
rm -f $(OUT) buxc_debug
|
||||||
rm -rf $(BUILD_DIR)
|
rm -rf $(BUILD_DIR)
|
||||||
@@ -65,7 +100,7 @@ clean-all: clean
|
|||||||
rm -rf build/selfhost build/selfhost-loop-a build/selfhost-loop-b build/selfhost-loop-c
|
rm -rf build/selfhost build/selfhost-loop-a build/selfhost-loop-b build/selfhost-loop-c
|
||||||
rm -rf tests/golden/*/build
|
rm -rf tests/golden/*/build
|
||||||
|
|
||||||
selfhost: build
|
selfhost: ensure-buxc
|
||||||
@echo "=== Building self-hosted compiler ==="
|
@echo "=== Building self-hosted compiler ==="
|
||||||
@rm -rf build/selfhost
|
@rm -rf build/selfhost
|
||||||
@mkdir -p build/selfhost/src
|
@mkdir -p build/selfhost/src
|
||||||
@@ -80,7 +115,7 @@ selfhost: build
|
|||||||
|
|
||||||
GOLDEN_TESTS := hello fibonacci structs generics algebraic_enums enums methods strings modern_features
|
GOLDEN_TESTS := hello fibonacci structs generics algebraic_enums enums methods strings modern_features
|
||||||
|
|
||||||
test-golden: build
|
test-golden: ensure-buxc
|
||||||
@echo "=== Golden tests ==="
|
@echo "=== Golden tests ==="
|
||||||
@passed=0; failed=0; \
|
@passed=0; failed=0; \
|
||||||
for test in $(GOLDEN_TESTS); do \
|
for test in $(GOLDEN_TESTS); do \
|
||||||
@@ -100,24 +135,24 @@ test-golden: build
|
|||||||
echo "Golden tests: $$passed passed, $$failed failed"; \
|
echo "Golden tests: $$passed passed, $$failed failed"; \
|
||||||
if [ $$failed -gt 0 ]; then exit 1; fi
|
if [ $$failed -gt 0 ]; then exit 1; fi
|
||||||
|
|
||||||
test-errors: build
|
test-errors: ensure-buxc
|
||||||
@echo "=== Error diagnostic golden tests ==="
|
@echo "=== Error diagnostic golden tests ==="
|
||||||
@chmod +x tests/error_golden/run.sh
|
@chmod +x tests/error_golden/run.sh
|
||||||
@tests/error_golden/run.sh ./$(OUT)
|
@tests/error_golden/run.sh ./$(OUT)
|
||||||
|
|
||||||
test-stdlib: build
|
test-stdlib: ensure-buxc
|
||||||
@echo "=== Stdlib golden tests ==="
|
@echo "=== Stdlib golden tests ==="
|
||||||
@chmod +x tests/stdlib_golden/run.sh
|
@chmod +x tests/stdlib_golden/run.sh
|
||||||
@tests/stdlib_golden/run.sh ./$(OUT)
|
@tests/stdlib_golden/run.sh ./$(OUT)
|
||||||
|
|
||||||
# Generate stdlib API docs from /// comments → docs/api/stdlib.md
|
# Generate stdlib API docs from /// comments → docs/api/stdlib.md
|
||||||
docs: build
|
docs: ensure-buxc
|
||||||
@mkdir -p docs/api
|
@mkdir -p docs/api
|
||||||
@./$(OUT) doc --out docs/api/stdlib.md lib/
|
@./$(OUT) doc --out docs/api/stdlib.md lib/
|
||||||
@echo "docs/api/stdlib.md updated"
|
@echo "docs/api/stdlib.md updated"
|
||||||
|
|
||||||
# CI: full-tree format check (lib / examples / src / tests / apps) + dirty-path smoke.
|
# CI: full-tree format check (lib / examples / src / tests / apps) + dirty-path smoke.
|
||||||
fmt-check: build
|
fmt-check: ensure-buxc
|
||||||
@echo "=== fmt --check (full tree) ==="
|
@echo "=== fmt --check (full tree) ==="
|
||||||
@./$(OUT) fmt --check lib/
|
@./$(OUT) fmt --check lib/
|
||||||
@./$(OUT) fmt --check examples/
|
@./$(OUT) fmt --check examples/
|
||||||
@@ -134,7 +169,7 @@ fmt-check: build
|
|||||||
|
|
||||||
# One-shot reformat of the same trees (run before committing style-only fixes)
|
# One-shot reformat of the same trees (run before committing style-only fixes)
|
||||||
.PHONY: fmt
|
.PHONY: fmt
|
||||||
fmt: build
|
fmt: ensure-buxc
|
||||||
@./$(OUT) fmt lib/
|
@./$(OUT) fmt lib/
|
||||||
@./$(OUT) fmt examples/
|
@./$(OUT) fmt examples/
|
||||||
@./$(OUT) fmt src/
|
@./$(OUT) fmt src/
|
||||||
@@ -144,7 +179,7 @@ fmt: build
|
|||||||
|
|
||||||
# Fixed-point: bootstrap buxc → buxc2 → buxc3 (path-normalized C + stripped ELF).
|
# Fixed-point: bootstrap buxc → buxc2 → buxc3 (path-normalized C + stripped ELF).
|
||||||
# Slow; not part of default `make test`. Optional CI: .github/workflows/selfhost-loop.yml
|
# Slow; not part of default `make test`. Optional CI: .github/workflows/selfhost-loop.yml
|
||||||
selfhost-loop: build
|
selfhost-loop: ensure-buxc
|
||||||
@chmod +x tools/selfhost_loop.sh
|
@chmod +x tools/selfhost_loop.sh
|
||||||
@tools/selfhost_loop.sh
|
@tools/selfhost_loop.sh
|
||||||
|
|
||||||
@@ -194,42 +229,53 @@ test-lsp: lsp
|
|||||||
@echo "==> LSP type hierarchy smoke"
|
@echo "==> LSP type hierarchy smoke"
|
||||||
@chmod +x tools/smoke_lsp_type_hierarchy.sh
|
@chmod +x tools/smoke_lsp_type_hierarchy.sh
|
||||||
@tools/smoke_lsp_type_hierarchy.sh
|
@tools/smoke_lsp_type_hierarchy.sh
|
||||||
|
@echo "==> LSP type hierarchy workspace (closed multi-file)"
|
||||||
|
@chmod +x tools/smoke_lsp_type_hierarchy_ws.sh
|
||||||
|
@tools/smoke_lsp_type_hierarchy_ws.sh
|
||||||
|
|
||||||
.PHONY: test-registry
|
.PHONY: test-registry
|
||||||
test-registry: build
|
test-registry: ensure-buxc
|
||||||
@echo "=== Registry smoke (E.1 + HTTP) ==="
|
@echo "=== Registry smoke (E.1 + HTTP) ==="
|
||||||
@chmod +x tools/smoke_registry.sh
|
@chmod +x tools/smoke_registry.sh
|
||||||
@tools/smoke_registry.sh
|
@tools/smoke_registry.sh
|
||||||
|
|
||||||
# E.2 — build showcase apps + simpledb/jwt CLI smoke
|
# E.2 — build showcase apps + simpledb/jwt CLI smoke
|
||||||
.PHONY: test-apps
|
.PHONY: test-apps
|
||||||
test-apps: build
|
test-apps: ensure-buxc
|
||||||
@echo "=== Apps smoke (E.2) ==="
|
@echo "=== Apps smoke (E.2) ==="
|
||||||
@chmod +x tools/smoke_apps.sh
|
@chmod +x tools/smoke_apps.sh
|
||||||
@tools/smoke_apps.sh
|
@tools/smoke_apps.sh
|
||||||
|
|
||||||
# E.5 — micro-benchmarks (Bux + C/Nim/Zig twins)
|
# E.5 — micro-benchmarks (Bux + C/Nim/Zig twins)
|
||||||
.PHONY: bench
|
.PHONY: bench
|
||||||
bench: build
|
bench: ensure-buxc
|
||||||
@chmod +x tools/bench.sh
|
@chmod +x tools/bench.sh
|
||||||
@tools/bench.sh
|
@tools/bench.sh
|
||||||
|
|
||||||
# E.5 — Nexus HTTP throughput (wrk); optional via BENCH_NEXUS=1 make bench
|
# E.5 — Nexus HTTP throughput (wrk); optional via BENCH_NEXUS=1 make bench
|
||||||
.PHONY: bench-nexus
|
.PHONY: bench-nexus
|
||||||
bench-nexus: build
|
bench-nexus: ensure-buxc
|
||||||
@chmod +x tools/bench_nexus.sh
|
@chmod +x tools/bench_nexus.sh
|
||||||
@tools/bench_nexus.sh
|
@tools/bench_nexus.sh
|
||||||
|
|
||||||
# E.4 — DWARF / #line debugger smoke
|
# E.4 — DWARF / #line debugger smoke
|
||||||
.PHONY: test-dwarf
|
.PHONY: test-dwarf
|
||||||
test-dwarf: build
|
test-dwarf: ensure-buxc
|
||||||
@echo "=== DWARF / #line smoke (E.4) ==="
|
@echo "=== DWARF / #line smoke (E.4) ==="
|
||||||
@chmod +x tools/smoke_dwarf.sh
|
@chmod +x tools/smoke_dwarf.sh
|
||||||
@tools/smoke_dwarf.sh
|
@tools/smoke_dwarf.sh
|
||||||
|
|
||||||
|
# Drop / field-move goldens (whole + partial field move; early-return counts)
|
||||||
|
.PHONY: test-drop-move
|
||||||
|
test-drop-move: ensure-buxc
|
||||||
|
@echo "=== Drop / field-move smoke ==="
|
||||||
|
@chmod +x tools/smoke_drop_move.sh
|
||||||
|
@tools/smoke_drop_move.sh
|
||||||
|
|
||||||
# Selfhost (buxc2): move_field ownership + multi-file #line (session 41/42)
|
# Selfhost (buxc2): move_field ownership + multi-file #line (session 41/42)
|
||||||
|
# When BUX_SKIP_BUILD=1, reuse prebuilt buxc; still builds buxc2 via selfhost.
|
||||||
.PHONY: test-selfhost-smoke
|
.PHONY: test-selfhost-smoke
|
||||||
test-selfhost-smoke: selfhost
|
test-selfhost-smoke: ensure-buxc selfhost
|
||||||
@echo "=== Selfhost smoke (move_field + multi-file #line) ==="
|
@echo "=== Selfhost smoke (move_field + multi-file #line) ==="
|
||||||
@chmod +x tools/smoke_selfhost.sh
|
@chmod +x tools/smoke_selfhost.sh
|
||||||
@tools/smoke_selfhost.sh
|
@tools/smoke_selfhost.sh
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ func Main() -> int {
|
|||||||
| **Backend** | LIR → C transpiler (clean 3-address code, then gcc/clang) |
|
| **Backend** | LIR → C transpiler (clean 3-address code, then gcc/clang) |
|
||||||
| **Strings** | Raw multi-line backticks, `f"..."` interp (bootstrap), `ReplaceAll` / `IsBlank` / `Repeat` |
|
| **Strings** | Raw multi-line backticks, `f"..."` interp (bootstrap), `ReplaceAll` / `IsBlank` / `Repeat` |
|
||||||
| **Gradual Ownership** | `@[Checked]` + `@[Release]` + `@[Shared]` + `borrow &mut` / `borrow &` |
|
| **Gradual Ownership** | `@[Checked]` + `@[Release]` + `@[Shared]` + `borrow &mut` / `borrow &` |
|
||||||
| **Drop Trait** | Auto-drop for `@[Drop]` types (Array, Map, user-defined structs) |
|
| **Drop / RAII** | Auto-drop (`@[Drop]` / `extend … for Drop`); **field-move skips Drop** (no double-free) |
|
||||||
| **Green Threads** | M:N scheduler (ucontext + SIGVTALRM), work-stealing queues |
|
| **Green Threads** | M:N scheduler (ucontext + SIGVTALRM), work-stealing queues |
|
||||||
| **Async/Await** | `async func`, `spawn`, `.await` with stackful coroutines |
|
| **Async/Await** | `async func`, `spawn`, `.await` with stackful coroutines |
|
||||||
| **Concurrency** | `Task`/`Channel`/`Sync` (pthread-based), `bux_async_yield`/`spawn` |
|
| **Concurrency** | `Task`/`Channel`/`Sync` (pthread-based), `bux_async_yield`/`spawn` |
|
||||||
@@ -318,8 +318,11 @@ make test-examples
|
|||||||
# Golden diagnostic tests (Rust-style error format)
|
# Golden diagnostic tests (Rust-style error format)
|
||||||
make test-errors
|
make test-errors
|
||||||
|
|
||||||
# Full unit + example suite (also run on every PR via GitHub Actions `ci.yml`)
|
# Full unit + example suite (local; CI splits the same coverage across jobs)
|
||||||
make test
|
make test
|
||||||
|
# Individual suites (also used by .github/workflows/ci.yml in parallel):
|
||||||
|
# make test-unit / test-examples / test-errors / test-stdlib /
|
||||||
|
# test-registry / test-dwarf / test-apps / test-selfhost-smoke
|
||||||
|
|
||||||
# Full-tree format check (lib/ examples/ src/ tests/ apps/)
|
# Full-tree format check (lib/ examples/ src/ tests/ apps/)
|
||||||
make fmt-check
|
make fmt-check
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ type
|
|||||||
ekMatch
|
ekMatch
|
||||||
ekStringInterp
|
ekStringInterp
|
||||||
ekClosure
|
ekClosure
|
||||||
|
ekMacroCall ## name!(args) — expanded before sema
|
||||||
|
|
||||||
MatchArm* = object
|
MatchArm* = object
|
||||||
loc*: SourceLocation
|
loc*: SourceLocation
|
||||||
@@ -236,6 +237,12 @@ type
|
|||||||
captureCount*: int
|
captureCount*: int
|
||||||
captureNames*: seq[string]
|
captureNames*: seq[string]
|
||||||
captureTypeKinds*: seq[int]
|
captureTypeKinds*: seq[int]
|
||||||
|
of ekMacroCall:
|
||||||
|
exprMacroName*: string
|
||||||
|
exprMacroArgs*: seq[Expr]
|
||||||
|
## Group lengths for multi-rep: `m!(1,2; 3,4)` → @[2, 2].
|
||||||
|
## Empty means a single group of all args.
|
||||||
|
exprMacroGroupLens*: seq[int]
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Statements
|
# Statements
|
||||||
@@ -258,6 +265,7 @@ type
|
|||||||
skDefer
|
skDefer
|
||||||
skSwitch
|
skSwitch
|
||||||
skDecl
|
skDecl
|
||||||
|
skMacroRep ## $( … )* template repetition (macro body only)
|
||||||
|
|
||||||
ElseIf* = object
|
ElseIf* = object
|
||||||
loc*: SourceLocation
|
loc*: SourceLocation
|
||||||
@@ -330,6 +338,8 @@ type
|
|||||||
stmtSwitchDefault*: Block
|
stmtSwitchDefault*: Block
|
||||||
of skDecl:
|
of skDecl:
|
||||||
stmtDecl*: Decl
|
stmtDecl*: Decl
|
||||||
|
of skMacroRep: ## $( stmts… )* in macro templates
|
||||||
|
stmtMacroRepBody*: Block
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Type Parameters (for generics with trait bounds)
|
# Type Parameters (for generics with trait bounds)
|
||||||
@@ -356,6 +366,28 @@ type
|
|||||||
dkExternFunc
|
dkExternFunc
|
||||||
dkExternVar
|
dkExternVar
|
||||||
dkExternBlock
|
dkExternBlock
|
||||||
|
dkMacro ## macro! name { ($x:expr) => { … } }
|
||||||
|
|
||||||
|
## One declarative macro arm: ($a:expr, $($x:expr),*) => { template }
|
||||||
|
MacroFragKind* = enum
|
||||||
|
mfkExpr ## any expression
|
||||||
|
mfkIdent ## bare identifier (after expand must be ekIdent)
|
||||||
|
mfkTt ## token-tree (MVP: same as expr)
|
||||||
|
mfkLiteral ## int/float/string/char/bool literal only
|
||||||
|
mfkBlock ## block expression `{ … }`
|
||||||
|
|
||||||
|
MacroFragment* = object
|
||||||
|
name*: string ## primary / first name (compat)
|
||||||
|
kind*: MacroFragKind ## primary kind (compat)
|
||||||
|
names*: seq[string] ## one or more $names (compound rep: $a,$b)
|
||||||
|
kinds*: seq[MacroFragKind] ## parallel to names
|
||||||
|
isRep*: bool ## true for $( … ),* or $( … )*
|
||||||
|
repSep*: string ## "," if separator was present before *, else ""
|
||||||
|
|
||||||
|
MacroRule* = object
|
||||||
|
loc*: SourceLocation
|
||||||
|
frags*: seq[MacroFragment]
|
||||||
|
body*: Block ## template (substituted, then used as ekBlock)
|
||||||
|
|
||||||
Param* = object
|
Param* = object
|
||||||
loc*: SourceLocation
|
loc*: SourceLocation
|
||||||
@@ -448,6 +480,9 @@ type
|
|||||||
declExtBlockDll*: string
|
declExtBlockDll*: string
|
||||||
declExtBlockCallConv*: CallingConvention
|
declExtBlockCallConv*: CallingConvention
|
||||||
declExtBlockItems*: seq[Decl]
|
declExtBlockItems*: seq[Decl]
|
||||||
|
of dkMacro:
|
||||||
|
declMacroName*: string
|
||||||
|
declMacroRules*: seq[MacroRule]
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Module (AST root)
|
# Module (AST root)
|
||||||
|
|||||||
+19
-1
@@ -4,6 +4,7 @@ import source_location
|
|||||||
import fmt
|
import fmt
|
||||||
import docgen
|
import docgen
|
||||||
import registry
|
import registry
|
||||||
|
import macroexpand
|
||||||
|
|
||||||
type
|
type
|
||||||
ColorMode* = enum
|
ColorMode* = enum
|
||||||
@@ -653,6 +654,12 @@ proc cmdCheck*(args: seq[string], opts: GlobalOptions): int =
|
|||||||
if status != 0:
|
if status != 0:
|
||||||
return status
|
return status
|
||||||
let unifiedModule = mergeProject(pctx)
|
let unifiedModule = mergeProject(pctx)
|
||||||
|
let macRes = expandMacros(unifiedModule)
|
||||||
|
if macRes.diagnostics.len > 0:
|
||||||
|
printError("macro expansion errors", useColor)
|
||||||
|
for d in macRes.diagnostics:
|
||||||
|
printDiagnostic("error", d.message, d.loc, useColor)
|
||||||
|
return 1
|
||||||
let semaRes = analyze(unifiedModule)
|
let semaRes = analyze(unifiedModule)
|
||||||
if semaRes.hasErrors:
|
if semaRes.hasErrors:
|
||||||
printError("type errors in project", useColor)
|
printError("type errors in project", useColor)
|
||||||
@@ -689,6 +696,7 @@ proc getDeclName(d: Decl): string =
|
|||||||
of dkInterface: d.declInterfaceName
|
of dkInterface: d.declInterfaceName
|
||||||
of dkConst: d.declConstName
|
of dkConst: d.declConstName
|
||||||
of dkTypeAlias: d.declAliasName
|
of dkTypeAlias: d.declAliasName
|
||||||
|
of dkMacro: d.declMacroName
|
||||||
else: ""
|
else: ""
|
||||||
|
|
||||||
proc collectDepDecls(lock: Lockfile, root: string, opts: GlobalOptions): seq[Decl] =
|
proc collectDepDecls(lock: Lockfile, root: string, opts: GlobalOptions): seq[Decl] =
|
||||||
@@ -763,6 +771,14 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
|
|||||||
|
|
||||||
let unifiedModule = mergeProject(pctx)
|
let unifiedModule = mergeProject(pctx)
|
||||||
|
|
||||||
|
# Phase 2b: expand declarative macro! / quote! before type checking
|
||||||
|
let macRes = expandMacros(unifiedModule)
|
||||||
|
if macRes.diagnostics.len > 0:
|
||||||
|
printError("macro expansion errors", useColor)
|
||||||
|
for d in macRes.diagnostics:
|
||||||
|
printDiagnostic("error", d.message, d.loc, useColor)
|
||||||
|
return 1
|
||||||
|
|
||||||
# Phase 3: Sema + HIR + C codegen on unified module
|
# Phase 3: Sema + HIR + C codegen on unified module
|
||||||
let (semaRes, semaCtx) = analyzeFull(unifiedModule)
|
let (semaRes, semaCtx) = analyzeFull(unifiedModule)
|
||||||
if semaRes.hasErrors:
|
if semaRes.hasErrors:
|
||||||
@@ -809,7 +825,9 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
|
|||||||
let optFlags = if opts.release: "-O2 -DNDEBUG" else: "-O0 -g"
|
let optFlags = if opts.release: "-O2 -DNDEBUG" else: "-O0 -g"
|
||||||
let extraCflags = getEnv("BUX_CFLAGS")
|
let extraCflags = getEnv("BUX_CFLAGS")
|
||||||
let cflags = if extraCflags.len > 0: optFlags & " " & extraCflags else: optFlags
|
let cflags = if extraCflags.len > 0: optFlags & " " & extraCflags else: optFlags
|
||||||
let ccCmd = &"cc {cflags} -pthread -Wl,--build-id=none -o {outputFile} {cFile} {runtimeDst} {ioDst} -lm -lcrypto 2>&1"
|
# --build-id is GNU ld only (breaks Apple ld). Reproducible selfhost-loop uses Linux CI.
|
||||||
|
let ldStable = when defined(linux): " -Wl,--build-id=none" else: ""
|
||||||
|
let ccCmd = &"cc {cflags} -pthread{ldStable} -o {outputFile} {cFile} {runtimeDst} {ioDst} -lm -lcrypto 2>&1"
|
||||||
if opts.verbose:
|
if opts.verbose:
|
||||||
printInfo(&"running: {ccCmd}", useColor)
|
printInfo(&"running: {ccCmd}", useColor)
|
||||||
let (output, exitCode) = execCmdEx(ccCmd)
|
let (output, exitCode) = execCmdEx(ccCmd)
|
||||||
|
|||||||
+32
-1
@@ -85,12 +85,23 @@ proc markMovedOutLocal(ctx: var LowerCtx, name: string) =
|
|||||||
if name.len > 0 and ctx.hasPendingDrop(name):
|
if name.len > 0 and ctx.hasPendingDrop(name):
|
||||||
ctx.movedOutLocals.incl(name)
|
ctx.movedOutLocals.incl(name)
|
||||||
|
|
||||||
|
# Forward decls (used by markMovedOutFromAst before their full definitions)
|
||||||
|
proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type
|
||||||
|
proc autoDropFuncName(ctx: var LowerCtx, ty: Type): string
|
||||||
|
|
||||||
proc markMovedOutFromAst(ctx: var LowerCtx, expr: Expr) =
|
proc markMovedOutFromAst(ctx: var LowerCtx, expr: Expr) =
|
||||||
## Mark droppable locals used by-value in ownership-taking contexts.
|
## Mark droppable locals used by-value in ownership-taking contexts.
|
||||||
|
## Partial field moves: `return bag.items` / `let x = bag.items` mark `bag`
|
||||||
|
## so auto-Drop of the parent is skipped — **only when the field type itself
|
||||||
|
## is droppable** (not `return bag.tag` for an int field).
|
||||||
if expr == nil: return
|
if expr == nil: return
|
||||||
case expr.kind
|
case expr.kind
|
||||||
of ekIdent:
|
of ekIdent:
|
||||||
ctx.markMovedOutLocal(expr.exprIdent)
|
ctx.markMovedOutLocal(expr.exprIdent)
|
||||||
|
of ekField:
|
||||||
|
let fieldTy = ctx.resolveExprType(expr)
|
||||||
|
if ctx.autoDropFuncName(fieldTy).len > 0:
|
||||||
|
ctx.markMovedOutFromAst(expr.exprFieldObj)
|
||||||
of ekStructInit:
|
of ekStructInit:
|
||||||
for f in expr.exprStructInitFields:
|
for f in expr.exprStructInitFields:
|
||||||
ctx.markMovedOutFromAst(f.value)
|
ctx.markMovedOutFromAst(f.value)
|
||||||
@@ -2131,6 +2142,11 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
|
|||||||
return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc),
|
return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc),
|
||||||
typ: makeVoid(), loc: loc)
|
typ: makeVoid(), loc: loc)
|
||||||
|
|
||||||
|
of skMacroRep:
|
||||||
|
# Expanded before lowering
|
||||||
|
return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc),
|
||||||
|
typ: makeVoid(), loc: loc)
|
||||||
|
|
||||||
proc lowerBlock(ctx: var LowerCtx, blk: Block, asExpr = false): HirNode =
|
proc lowerBlock(ctx: var LowerCtx, blk: Block, asExpr = false): HirNode =
|
||||||
## asExpr=true: block is used as a value (`let x = { ... }`, match arm body).
|
## asExpr=true: block is used as a value (`let x = { ... }`, match arm body).
|
||||||
## Last skExpr becomes the block result. Statement blocks (func body, if/while)
|
## Last skExpr becomes the block result. Statement blocks (func body, if/while)
|
||||||
@@ -2168,15 +2184,30 @@ proc lowerBlock(ctx: var LowerCtx, blk: Block, asExpr = false): HirNode =
|
|||||||
expr = last.blockExpr
|
expr = last.blockExpr
|
||||||
# Scope exit: Drop locals introduced in this block (not outer ones).
|
# Scope exit: Drop locals introduced in this block (not outer ones).
|
||||||
# Skip Drop for block result and any moved-out locals (field / let / return move).
|
# Skip Drop for block result and any moved-out locals (field / let / return move).
|
||||||
|
# If the last statement always returns, drops were already injected on that
|
||||||
|
# path — re-emitting them here produces dead double-Drop after `return`.
|
||||||
|
proc blockAlwaysReturns(n: HirNode): bool =
|
||||||
|
if n == nil: return false
|
||||||
|
if n.kind == hReturn: return true
|
||||||
|
if n.kind == hBlock:
|
||||||
|
if n.blockStmts.len == 0: return false
|
||||||
|
return blockAlwaysReturns(n.blockStmts[^1])
|
||||||
|
false
|
||||||
|
|
||||||
var skipDrop = ""
|
var skipDrop = ""
|
||||||
if expr != nil and expr.kind == hVar:
|
if expr != nil and expr.kind == hVar:
|
||||||
skipDrop = expr.varName
|
skipDrop = expr.varName
|
||||||
ctx.markMovedOutLocal(expr.varName)
|
ctx.markMovedOutLocal(expr.varName)
|
||||||
if ctx.deferStmts.len > deferBase:
|
let lastAlwaysReturns = stmts.len > 0 and blockAlwaysReturns(stmts[^1])
|
||||||
|
if ctx.deferStmts.len > deferBase and not lastAlwaysReturns:
|
||||||
for i in countdown(ctx.deferStmts.len - 1, deferBase):
|
for i in countdown(ctx.deferStmts.len - 1, deferBase):
|
||||||
if not ctx.shouldSkipDrop(ctx.deferStmts[i], skipDrop):
|
if not ctx.shouldSkipDrop(ctx.deferStmts[i], skipDrop):
|
||||||
stmts.add(ctx.deferStmts[i])
|
stmts.add(ctx.deferStmts[i])
|
||||||
ctx.deferStmts.setLen(deferBase)
|
ctx.deferStmts.setLen(deferBase)
|
||||||
|
elif ctx.deferStmts.len > deferBase and lastAlwaysReturns:
|
||||||
|
# Return path already owns these drops; pop so outer scopes don't re-run them
|
||||||
|
# for the same locals when this block is nested. Outer live locals remain.
|
||||||
|
ctx.deferStmts.setLen(deferBase)
|
||||||
let typ = if expr != nil and expr.typ != nil: expr.typ else: makeVoid()
|
let typ = if expr != nil and expr.typ != nil: expr.typ else: makeVoid()
|
||||||
return hirBlock(stmts, expr, typ, blk.loc, isScope = true)
|
return hirBlock(stmts, expr, typ, blk.loc, isScope = true)
|
||||||
|
|
||||||
|
|||||||
@@ -464,6 +464,17 @@ proc scanSymbol(lex: var Lexer, startLoc: SourceLocation): Token =
|
|||||||
return lex.makeToken(tkCaretAssign, startLoc, startPos)
|
return lex.makeToken(tkCaretAssign, startLoc, startPos)
|
||||||
else:
|
else:
|
||||||
return lex.makeToken(tkCaret, startLoc, startPos)
|
return lex.makeToken(tkCaret, startLoc, startPos)
|
||||||
|
of '$':
|
||||||
|
# $name fragment, or bare $ for macro repetition $( ... )*
|
||||||
|
if isIdentStart(lex.peek()):
|
||||||
|
discard lex.advance() # first ident char ( $ already consumed as c1)
|
||||||
|
while not lex.isAtEnd() and isIdentChar(lex.peek()):
|
||||||
|
discard lex.advance()
|
||||||
|
# text includes leading '$'
|
||||||
|
return lex.makeToken(tkIdent, startLoc, startPos)
|
||||||
|
else:
|
||||||
|
# bare $ (c1 already consumed)
|
||||||
|
return lex.makeToken(tkDollar, startLoc, startPos)
|
||||||
of '#':
|
of '#':
|
||||||
# Check for intrinsics: #line, #column, #file, #function, #date, #time, #module
|
# Check for intrinsics: #line, #column, #file, #function, #date, #time, #module
|
||||||
let afterHash = lex.peek()
|
let afterHash = lex.peek()
|
||||||
|
|||||||
@@ -120,14 +120,15 @@ proc emitInstr(be: var LirCBackend, instr: LirInstr) =
|
|||||||
of lirShl: "<<"
|
of lirShl: "<<"
|
||||||
of lirShr: ">>"
|
of lirShr: ">>"
|
||||||
else: "?"
|
else: "?"
|
||||||
be.emitLine(&"{v(instr.dst)} = {v(instr.src)} {op} {v(instr.src2)};")
|
# Parenthesize so future non-temp operands cannot be rewritten by C precedence
|
||||||
|
be.emitLine(&"{v(instr.dst)} = ({v(instr.src)} {op} {v(instr.src2)});")
|
||||||
|
|
||||||
of lirNeg:
|
of lirNeg:
|
||||||
be.emitLine(&"{v(instr.dst)} = -{v(instr.src)};")
|
be.emitLine(&"{v(instr.dst)} = -({v(instr.src)});")
|
||||||
of lirNot:
|
of lirNot:
|
||||||
be.emitLine(&"{v(instr.dst)} = !{v(instr.src)};")
|
be.emitLine(&"{v(instr.dst)} = !({v(instr.src)});")
|
||||||
of lirBNot:
|
of lirBNot:
|
||||||
be.emitLine(&"{v(instr.dst)} = ~{v(instr.src)};")
|
be.emitLine(&"{v(instr.dst)} = ~({v(instr.src)});")
|
||||||
|
|
||||||
# ── Comparison ──
|
# ── Comparison ──
|
||||||
of lirCmpEq, lirCmpNe, lirCmpLt, lirCmpLe, lirCmpGt, lirCmpGe:
|
of lirCmpEq, lirCmpNe, lirCmpLt, lirCmpLe, lirCmpGt, lirCmpGe:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+203
-2
@@ -21,12 +21,14 @@ type
|
|||||||
pos: int
|
pos: int
|
||||||
diagnostics: seq[ParserDiagnostic]
|
diagnostics: seq[ParserDiagnostic]
|
||||||
structInitAllowed: bool ## disabled inside if/while/for/match conditions
|
structInitAllowed: bool ## disabled inside if/while/for/match conditions
|
||||||
|
macroTemplateMode: bool ## true while parsing macro! rule body (allows $(…)*)
|
||||||
|
|
||||||
proc initParser*(tokens: seq[Token], sourceName: string = "<input>"): Parser =
|
proc initParser*(tokens: seq[Token], sourceName: string = "<input>"): Parser =
|
||||||
result.tokens = tokens
|
result.tokens = tokens
|
||||||
result.sourceName = sourceName
|
result.sourceName = sourceName
|
||||||
result.pos = 0
|
result.pos = 0
|
||||||
result.structInitAllowed = true
|
result.structInitAllowed = true
|
||||||
|
result.macroTemplateMode = false
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Token helpers
|
# Token helpers
|
||||||
@@ -152,7 +154,7 @@ proc synchronize(p: var Parser) =
|
|||||||
if p.previous.kind == tkSemicolon: return
|
if p.previous.kind == tkSemicolon: return
|
||||||
case p.peek()
|
case p.peek()
|
||||||
of tkFunc, tkStruct, tkEnum, tkUnion, tkInterface, tkExtend,
|
of tkFunc, tkStruct, tkEnum, tkUnion, tkInterface, tkExtend,
|
||||||
tkModule, tkImport, tkConst, tkType, tkExtern, tkPub:
|
tkModule, tkImport, tkConst, tkType, tkExtern, tkPub, tkMacro:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
discard p.advance()
|
discard p.advance()
|
||||||
@@ -183,7 +185,10 @@ type
|
|||||||
release*: bool ## @[Release] — explicit zero-cost (no borrow checks)
|
release*: bool ## @[Release] — explicit zero-cost (no borrow checks)
|
||||||
|
|
||||||
proc parseAttrs(p: var Parser): ParsedAttrs =
|
proc parseAttrs(p: var Parser): ParsedAttrs =
|
||||||
while p.check(tkAt):
|
while true:
|
||||||
|
p.skipNewlines()
|
||||||
|
if not p.check(tkAt):
|
||||||
|
break
|
||||||
discard p.advance() # @
|
discard p.advance() # @
|
||||||
discard p.expect(tkLBracket, "expected '[' after '@'")
|
discard p.expect(tkLBracket, "expected '[' after '@'")
|
||||||
let name = p.expect(tkIdent, "expected attribute name").text
|
let name = p.expect(tkIdent, "expected attribute name").text
|
||||||
@@ -728,6 +733,42 @@ proc parsePostfix(p: var Parser): Expr =
|
|||||||
left = Expr(kind: ekTry, loc: loc, exprTryOperand: left, exprTryType: nil)
|
left = Expr(kind: ekTry, loc: loc, exprTryOperand: left, exprTryType: nil)
|
||||||
of tkBang:
|
of tkBang:
|
||||||
discard p.advance()
|
discard p.advance()
|
||||||
|
# name!(args) → declarative macro call (not unwrap)
|
||||||
|
if left.kind == ekIdent and p.check(tkLParen):
|
||||||
|
discard p.advance() # (
|
||||||
|
var margs: seq[Expr] = @[]
|
||||||
|
var groupLens: seq[int] = @[]
|
||||||
|
var curGroup = 0
|
||||||
|
while not p.check(tkRParen) and not p.isAtEnd:
|
||||||
|
p.skipNewlines()
|
||||||
|
if p.check(tkRParen): break
|
||||||
|
# `;` starts a new arg group for multi-rep patterns
|
||||||
|
if p.check(tkSemicolon):
|
||||||
|
discard p.advance()
|
||||||
|
groupLens.add(curGroup)
|
||||||
|
curGroup = 0
|
||||||
|
p.skipNewlines()
|
||||||
|
continue
|
||||||
|
margs.add(p.parseExpr())
|
||||||
|
inc curGroup
|
||||||
|
p.skipNewlines()
|
||||||
|
if p.check(tkComma):
|
||||||
|
discard p.advance()
|
||||||
|
elif p.check(tkSemicolon):
|
||||||
|
discard
|
||||||
|
# handled at loop top
|
||||||
|
else:
|
||||||
|
# allow end of args
|
||||||
|
discard
|
||||||
|
if curGroup > 0 or groupLens.len == 0:
|
||||||
|
groupLens.add(curGroup)
|
||||||
|
# single group of all args → empty groupLens means "one group" for expander
|
||||||
|
if groupLens.len == 1:
|
||||||
|
groupLens = @[]
|
||||||
|
discard p.expect(tkRParen, "expected ')' to close macro arguments")
|
||||||
|
left = Expr(kind: ekMacroCall, loc: loc, exprMacroName: left.exprIdent,
|
||||||
|
exprMacroArgs: margs, exprMacroGroupLens: groupLens)
|
||||||
|
else:
|
||||||
left = Expr(kind: ekUnwrap, loc: loc, exprUnwrapOperand: left)
|
left = Expr(kind: ekUnwrap, loc: loc, exprUnwrapOperand: left)
|
||||||
of tkLBrace:
|
of tkLBrace:
|
||||||
if p.structInitAllowed and left.kind in {ekIdent, ekPath, ekGenericCall}:
|
if p.structInitAllowed and left.kind in {ekIdent, ekPath, ekGenericCall}:
|
||||||
@@ -952,7 +993,26 @@ proc parseBlock(p: var Parser): Block =
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
proc parseStmt(p: var Parser): Stmt =
|
proc parseStmt(p: var Parser): Stmt =
|
||||||
|
while p.check(tkNewLine):
|
||||||
|
discard p.advance()
|
||||||
let loc = p.currentLoc
|
let loc = p.currentLoc
|
||||||
|
# Macro template repetition: $( stmts… )*
|
||||||
|
if p.macroTemplateMode and p.check(tkDollar) and p.peek(1) == tkLParen:
|
||||||
|
discard p.advance() # $
|
||||||
|
discard p.advance() # (
|
||||||
|
var stmts: seq[Stmt] = @[]
|
||||||
|
while not p.check(tkRParen) and not p.isAtEnd:
|
||||||
|
while p.check(tkNewLine):
|
||||||
|
discard p.advance()
|
||||||
|
if p.check(tkRParen) or p.isAtEnd:
|
||||||
|
break
|
||||||
|
stmts.add(p.parseStmt())
|
||||||
|
discard p.expect(tkRParen, "expected ')' to close macro repetition")
|
||||||
|
discard p.expect(tkStar, "expected '*' after macro repetition")
|
||||||
|
if p.check(tkSemicolon):
|
||||||
|
discard p.advance()
|
||||||
|
return Stmt(kind: skMacroRep, loc: loc,
|
||||||
|
stmtMacroRepBody: Block(loc: loc, stmts: stmts))
|
||||||
case p.peek()
|
case p.peek()
|
||||||
of tkLet, tkVar:
|
of tkLet, tkVar:
|
||||||
let isMut = p.peek() == tkVar
|
let isMut = p.peek() == tkVar
|
||||||
@@ -1561,6 +1621,145 @@ proc parseExternDecl(p: var Parser, isPublic: bool, attrs: ParsedAttrs): Decl =
|
|||||||
return Decl(kind: dkExternVar, loc: loc, isPublic: isPublic,
|
return Decl(kind: dkExternVar, loc: loc, isPublic: isPublic,
|
||||||
declExtVarName: vName, declExtVarType: vType)
|
declExtVarName: vName, declExtVarType: vType)
|
||||||
|
|
||||||
|
proc parseMacroFragKind(p: var Parser, kindTok: Token): MacroFragKind =
|
||||||
|
case kindTok.text
|
||||||
|
of "expr": mfkExpr
|
||||||
|
of "ident": mfkIdent
|
||||||
|
of "tt": mfkTt
|
||||||
|
of "literal", "lit": mfkLiteral
|
||||||
|
of "block": mfkBlock
|
||||||
|
else:
|
||||||
|
p.emitError(kindTok.loc,
|
||||||
|
"unsupported macro fragment kind '" & kindTok.text &
|
||||||
|
"' (expr|ident|tt|literal|block)")
|
||||||
|
mfkExpr
|
||||||
|
|
||||||
|
proc parseMacroFragment(p: var Parser): MacroFragment =
|
||||||
|
## $name:kind (single non-rep fragment)
|
||||||
|
let fragTok = p.expect(tkIdent, "expected $name fragment in macro pattern")
|
||||||
|
if not fragTok.text.startsWith("$"):
|
||||||
|
p.emitError(fragTok.loc, "macro fragment must start with '$' (e.g. $x:expr)")
|
||||||
|
discard p.expect(tkColon, "expected ':' after macro fragment name")
|
||||||
|
let kindTok = p.expect(tkIdent, "expected fragment kind (expr|ident|tt|literal|block)")
|
||||||
|
let k = p.parseMacroFragKind(kindTok)
|
||||||
|
result = MacroFragment(
|
||||||
|
name: fragTok.text,
|
||||||
|
kind: k,
|
||||||
|
names: @[fragTok.text],
|
||||||
|
kinds: @[k],
|
||||||
|
isRep: false,
|
||||||
|
repSep: "")
|
||||||
|
|
||||||
|
proc parseMacroRepGroup(p: var Parser): MacroFragment =
|
||||||
|
## $( $a:kind , $b:kind , … ) ,* or … )*
|
||||||
|
## Compound: multiple frags inside one rep → parallel lists (zipped).
|
||||||
|
discard p.expect(tkDollar, "expected '$'")
|
||||||
|
discard p.expect(tkLParen, "expected '(' after '$'")
|
||||||
|
p.skipNewlines()
|
||||||
|
var names: seq[string] = @[]
|
||||||
|
var kinds: seq[MacroFragKind] = @[]
|
||||||
|
while not p.check(tkRParen) and not p.isAtEnd:
|
||||||
|
let fragTok = p.expect(tkIdent, "expected $name inside repetition")
|
||||||
|
if not fragTok.text.startsWith("$"):
|
||||||
|
p.emitError(fragTok.loc, "macro fragment must start with '$'")
|
||||||
|
discard p.expect(tkColon, "expected ':' after fragment name")
|
||||||
|
let kindTok = p.expect(tkIdent, "expected fragment kind")
|
||||||
|
names.add(fragTok.text)
|
||||||
|
kinds.add(p.parseMacroFragKind(kindTok))
|
||||||
|
p.skipNewlines()
|
||||||
|
if p.check(tkComma):
|
||||||
|
discard p.advance()
|
||||||
|
p.skipNewlines()
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
if names.len == 0:
|
||||||
|
p.emitError(p.currentLoc, "empty macro repetition group")
|
||||||
|
names.add("$x")
|
||||||
|
kinds.add(mfkExpr)
|
||||||
|
discard p.expect(tkRParen, "expected ')' after repeated fragment(s)")
|
||||||
|
var sep = ""
|
||||||
|
if p.check(tkComma):
|
||||||
|
discard p.advance()
|
||||||
|
sep = ","
|
||||||
|
discard p.expect(tkStar, "expected '*' after macro repetition")
|
||||||
|
result = MacroFragment(
|
||||||
|
name: names[0],
|
||||||
|
kind: kinds[0],
|
||||||
|
names: names,
|
||||||
|
kinds: kinds,
|
||||||
|
isRep: true,
|
||||||
|
repSep: sep)
|
||||||
|
|
||||||
|
proc parseMacroDecl(p: var Parser, isPublic: bool): Decl =
|
||||||
|
## macro! name {
|
||||||
|
## ( $a:ident, $($x:expr),* ) => { … }
|
||||||
|
## ( $($a:expr, $b:expr),* ) => { … } # compound / zipped
|
||||||
|
## ( $($x:expr),* ; $($y:expr),* ) => { … } # multi-rep groups
|
||||||
|
## }
|
||||||
|
let loc = p.currentLoc
|
||||||
|
discard p.expect(tkMacro, "expected 'macro'")
|
||||||
|
discard p.expect(tkBang, "expected '!' after macro")
|
||||||
|
let name = p.expect(tkIdent, "expected macro name").text
|
||||||
|
p.skipNewlines()
|
||||||
|
discard p.expect(tkLBrace, "expected '{' to start macro body")
|
||||||
|
var rules: seq[MacroRule] = @[]
|
||||||
|
while not p.check(tkRBrace) and not p.isAtEnd:
|
||||||
|
p.skipNewlines()
|
||||||
|
if p.check(tkRBrace) or p.isAtEnd:
|
||||||
|
break
|
||||||
|
let rloc = p.currentLoc
|
||||||
|
discard p.expect(tkLParen, "expected '(' to start macro pattern")
|
||||||
|
var frags: seq[MacroFragment] = @[]
|
||||||
|
while not p.check(tkRParen) and not p.isAtEnd:
|
||||||
|
p.skipNewlines()
|
||||||
|
if p.check(tkRParen): break
|
||||||
|
# Group separator for multi-rep: `;` between pattern elements
|
||||||
|
if p.check(tkSemicolon):
|
||||||
|
discard p.advance()
|
||||||
|
p.skipNewlines()
|
||||||
|
continue
|
||||||
|
# $( … ),* compound or single rep
|
||||||
|
if p.check(tkDollar) and p.peek(1) == tkLParen:
|
||||||
|
frags.add(p.parseMacroRepGroup())
|
||||||
|
p.skipNewlines()
|
||||||
|
# optional `;` after rep continues with more elements
|
||||||
|
if p.check(tkSemicolon):
|
||||||
|
discard p.advance()
|
||||||
|
p.skipNewlines()
|
||||||
|
continue
|
||||||
|
if p.check(tkComma):
|
||||||
|
discard p.advance()
|
||||||
|
p.skipNewlines()
|
||||||
|
continue
|
||||||
|
# no more separators → only rparen expected next
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
frags.add(p.parseMacroFragment())
|
||||||
|
p.skipNewlines()
|
||||||
|
if p.check(tkComma):
|
||||||
|
discard p.advance()
|
||||||
|
elif p.check(tkSemicolon):
|
||||||
|
discard p.advance()
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
discard p.expect(tkRParen, "expected ')' to close macro pattern")
|
||||||
|
p.skipNewlines()
|
||||||
|
discard p.expect(tkFatArrow, "expected '=>' after macro pattern")
|
||||||
|
p.skipNewlines()
|
||||||
|
let savedTpl = p.macroTemplateMode
|
||||||
|
p.macroTemplateMode = true
|
||||||
|
let body = p.parseBlock()
|
||||||
|
p.macroTemplateMode = savedTpl
|
||||||
|
rules.add(MacroRule(loc: rloc, frags: frags, body: body))
|
||||||
|
p.skipNewlines()
|
||||||
|
if p.check(tkComma) or p.check(tkSemicolon):
|
||||||
|
discard p.advance()
|
||||||
|
discard p.expect(tkRBrace, "expected '}' to close macro")
|
||||||
|
if rules.len == 0:
|
||||||
|
p.emitError(loc, "macro '" & name & "' has no rules")
|
||||||
|
return Decl(kind: dkMacro, loc: loc, isPublic: isPublic,
|
||||||
|
declMacroName: name, declMacroRules: rules)
|
||||||
|
|
||||||
proc parseDecl(p: var Parser): Decl =
|
proc parseDecl(p: var Parser): Decl =
|
||||||
let loc = p.currentLoc
|
let loc = p.currentLoc
|
||||||
var isPublic = false
|
var isPublic = false
|
||||||
@@ -1606,6 +1805,8 @@ proc parseDecl(p: var Parser): Decl =
|
|||||||
return p.parseTypeAliasDecl(isPublic)
|
return p.parseTypeAliasDecl(isPublic)
|
||||||
of tkExtern:
|
of tkExtern:
|
||||||
return p.parseExternDecl(isPublic, attrs)
|
return p.parseExternDecl(isPublic, attrs)
|
||||||
|
of tkMacro:
|
||||||
|
return p.parseMacroDecl(isPublic)
|
||||||
else:
|
else:
|
||||||
p.emitError(loc, "expected declaration")
|
p.emitError(loc, "expected declaration")
|
||||||
p.synchronize()
|
p.synchronize()
|
||||||
|
|||||||
+15
-2
@@ -41,7 +41,8 @@ type
|
|||||||
# Interface name -> interface decl
|
# Interface name -> interface decl
|
||||||
interfaceTable*: Table[string, Decl]
|
interfaceTable*: Table[string, Decl]
|
||||||
# Borrow checker state
|
# Borrow checker state
|
||||||
checkedFunc*: bool ## true inside @[Checked] function
|
checkedFunc*: bool ## true inside @[Checked] and not @[Release]
|
||||||
|
releaseFunc*: bool ## true inside @[Release] (zero-cost: no borrow checks)
|
||||||
currentFuncIsAsync*: bool ## true inside async func
|
currentFuncIsAsync*: bool ## true inside async func
|
||||||
movedVars*: seq[string] ## variables moved in current checked function
|
movedVars*: seq[string] ## variables moved in current checked function
|
||||||
## Active exclusive borrows: source var → borrow site (let-bound &mut lasts for rest of fn)
|
## Active exclusive borrows: source var → borrow site (let-bound &mut lasts for rest of fn)
|
||||||
@@ -1897,6 +1898,10 @@ proc checkExpr*(sema: var Sema, expr: Expr, scope: Scope): Type =
|
|||||||
for e in expr.exprInterpExprs:
|
for e in expr.exprInterpExprs:
|
||||||
discard sema.checkExpr(e, scope)
|
discard sema.checkExpr(e, scope)
|
||||||
return makeStr()
|
return makeStr()
|
||||||
|
of ekMacroCall:
|
||||||
|
# Should have been expanded before analyze; leftover is a compiler bug
|
||||||
|
sema.emitError(expr.loc, "unexpanded macro call '" & expr.exprMacroName & "!'")
|
||||||
|
return makeUnknown()
|
||||||
of ekClosure:
|
of ekClosure:
|
||||||
let savedRetType = sema.currentRetType
|
let savedRetType = sema.currentRetType
|
||||||
let savedClosureDepth = sema.closureDepth
|
let savedClosureDepth = sema.closureDepth
|
||||||
@@ -2088,6 +2093,10 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type =
|
|||||||
else:
|
else:
|
||||||
discard
|
discard
|
||||||
return makeVoid()
|
return makeVoid()
|
||||||
|
of skMacroRep:
|
||||||
|
# Templates with $(…)* must be expanded before type-check
|
||||||
|
sema.emitError(stmt.loc, "unexpanded macro repetition '$(…)*'")
|
||||||
|
return makeVoid()
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Function body checking
|
# Function body checking
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -2106,8 +2115,11 @@ proc checkFunc(sema: var Sema, decl: Decl) =
|
|||||||
if hasTypeGeneric:
|
if hasTypeGeneric:
|
||||||
return
|
return
|
||||||
let wasChecked = sema.checkedFunc
|
let wasChecked = sema.checkedFunc
|
||||||
|
let wasRelease = sema.releaseFunc
|
||||||
let wasAsync = sema.currentFuncIsAsync
|
let wasAsync = sema.currentFuncIsAsync
|
||||||
sema.checkedFunc = "Checked" in decl.declAttrs
|
# C.4: @[Release] is the zero-cost escape — disables borrow checks even with @[Checked]
|
||||||
|
sema.releaseFunc = "Release" in decl.declAttrs
|
||||||
|
sema.checkedFunc = "Checked" in decl.declAttrs and not sema.releaseFunc
|
||||||
sema.currentFuncIsAsync = decl.declFuncIsAsync
|
sema.currentFuncIsAsync = decl.declFuncIsAsync
|
||||||
if sema.checkedFunc:
|
if sema.checkedFunc:
|
||||||
sema.movedVars = @[]
|
sema.movedVars = @[]
|
||||||
@@ -2139,6 +2151,7 @@ proc checkFunc(sema: var Sema, decl: Decl) =
|
|||||||
for tp in addedTypeParams:
|
for tp in addedTypeParams:
|
||||||
sema.typeTable.del(tp)
|
sema.typeTable.del(tp)
|
||||||
sema.checkedFunc = wasChecked
|
sema.checkedFunc = wasChecked
|
||||||
|
sema.releaseFunc = wasRelease
|
||||||
sema.currentFuncIsAsync = wasAsync
|
sema.currentFuncIsAsync = wasAsync
|
||||||
sema.varRefLifetime = initTable[string, string]()
|
sema.varRefLifetime = initTable[string, string]()
|
||||||
sema.returnLifetime = ""
|
sema.returnLifetime = ""
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ type
|
|||||||
tkDyn # dyn
|
tkDyn # dyn
|
||||||
tkDefer # defer
|
tkDefer # defer
|
||||||
tkLifetime # 'a (lifetime parameter)
|
tkLifetime # 'a (lifetime parameter)
|
||||||
|
tkMacro # macro (declarative macro! definitions)
|
||||||
|
tkDollar # bare $ for macro rep $( ... )*
|
||||||
|
|
||||||
##Punctuation
|
##Punctuation
|
||||||
tkLParen # (
|
tkLParen # (
|
||||||
@@ -224,6 +226,7 @@ proc keywordKind*(text: string): TokenKind =
|
|||||||
of "comptime": tkComptime
|
of "comptime": tkComptime
|
||||||
of "dyn": tkDyn
|
of "dyn": tkDyn
|
||||||
of "defer": tkDefer
|
of "defer": tkDefer
|
||||||
|
of "macro": tkMacro
|
||||||
of "true", "false": tkBoolLiteral
|
of "true", "false": tkBoolLiteral
|
||||||
else: tkIdent
|
else: tkIdent
|
||||||
|
|
||||||
@@ -281,6 +284,8 @@ proc tokenKindName*(kind: TokenKind): string =
|
|||||||
of tkComptime: "'comptime'"
|
of tkComptime: "'comptime'"
|
||||||
of tkDyn: "'dyn'"
|
of tkDyn: "'dyn'"
|
||||||
of tkDefer: "'defer'"
|
of tkDefer: "'defer'"
|
||||||
|
of tkMacro: "'macro'"
|
||||||
|
of tkDollar: "'$'"
|
||||||
of tkLifetime: "lifetime"
|
of tkLifetime: "lifetime"
|
||||||
of tkLParen: "'('"
|
of tkLParen: "'('"
|
||||||
of tkRParen: "')'"
|
of tkRParen: "')'"
|
||||||
|
|||||||
+31
-5
@@ -191,14 +191,40 @@ Use `Std::Test` module for assertions inside test code.
|
|||||||
|
|
||||||
### Continuous integration
|
### Continuous integration
|
||||||
```bash
|
```bash
|
||||||
make test # what PR CI runs
|
make test # full sequential suite (local)
|
||||||
```
|
```
|
||||||
| Workflow | When | Command |
|
| Workflow | When | What runs |
|
||||||
|----------|------|---------|
|
|----------|------|-----------|
|
||||||
| **`.github/workflows/ci.yml`** | every PR + push to `main` | `make test` |
|
| **`.github/workflows/ci.yml`** | every PR + push to `main` | **split jobs** (see below) + macOS smoke |
|
||||||
| **`.github/workflows/selfhost-loop.yml`** | weekly / manual / path-filtered main | `make selfhost-loop` |
|
| **`.github/workflows/selfhost-loop.yml`** | weekly / manual / path-filtered main | `make selfhost-loop` |
|
||||||
|
|
||||||
`make test` includes examples, goldens, registry, apps, DWARF, and selfhost smoke
|
**`ci.yml` layout (faster PR feedback):**
|
||||||
|
|
||||||
|
| Job | OS | Targets |
|
||||||
|
|-----|-----|---------|
|
||||||
|
| `build` | ubuntu | `make build` → upload `buxc` artifact |
|
||||||
|
| `unit` | ubuntu | `fmt-check` + `test-unit` (reuse artifact) |
|
||||||
|
| `examples` | ubuntu | `test-examples` (full list) |
|
||||||
|
| `goldens` | ubuntu | `test-errors` + `test-stdlib` + `test-registry` + `test-dwarf` + `test-drop-move` |
|
||||||
|
| `apps` | ubuntu | `test-apps` |
|
||||||
|
| `selfhost` | ubuntu | `test-selfhost-smoke` |
|
||||||
|
| `macos` | macos-14 | rebuild + `test-unit` + `test-examples-smoke` (subset) |
|
||||||
|
| `windows` | windows-latest | rebuild `buxc.exe` + pure Nim unit tests + CLI smoke |
|
||||||
|
| `ci-gate` | ubuntu | fails if any required job failed (branch protection) |
|
||||||
|
|
||||||
|
**CI speed helpers:**
|
||||||
|
- Pin Nim **2.0.8**; cache `.nim_runtime` (big win on macOS — Nim is built from source there;
|
||||||
|
Windows uses a prebuilt Nim zip)
|
||||||
|
- Project-local `nimcache/` via `NIMFLAGS=--nimcache:nimcache`, cached per job by source hash
|
||||||
|
- macOS skips full EXAMPLES (Linux already runs them) and skips `fmt-check` (Linux unit job)
|
||||||
|
- **Windows** does **not** run `bux run` examples yet: `rt/runtime.c` needs POSIX
|
||||||
|
(`ucontext`, `pthread`, BSD sockets). Smoke still validates bootstrap + unit tests on Win.
|
||||||
|
|
||||||
|
Parallel Linux jobs set `BUX_SKIP_BUILD=1` after downloading the `buxc` artifact.
|
||||||
|
Locally, `make test` still runs the full suite sequentially and builds once.
|
||||||
|
`make test-examples-smoke` runs the macOS-sized subset locally.
|
||||||
|
|
||||||
|
`make test` includes examples, goldens, registry, apps, DWARF, unit tests, and selfhost smoke
|
||||||
(not the slow gen2↔gen3 fixed-point).
|
(not the slow gen2↔gen3 fixed-point).
|
||||||
|
|
||||||
### Selfhost loop (optional CI)
|
### Selfhost loop (optional CI)
|
||||||
|
|||||||
+436
-35
@@ -16,11 +16,13 @@ This document describes the Bux programming language as implemented by the boots
|
|||||||
8. [Pattern Matching](#pattern-matching)
|
8. [Pattern Matching](#pattern-matching)
|
||||||
9. [Methods and Interfaces](#methods-and-interfaces)
|
9. [Methods and Interfaces](#methods-and-interfaces)
|
||||||
10. [Generics](#generics)
|
10. [Generics](#generics)
|
||||||
11. [Error Handling](#error-handling)
|
11. [Gradual Ownership](#gradual-ownership-phase-82--implemented) — Checked / Release / [Drop & RAII](#drop-and-raii)
|
||||||
12. [Modules and Imports](#modules-and-imports)
|
12. [Error Handling](#error-handling)
|
||||||
13. [Async/Await](#asyncawait)
|
13. [Modules and Imports](#modules-and-imports)
|
||||||
14. [Operator Overloading](#operator-overloading)
|
14. [Async/Await](#asyncawait)
|
||||||
15. [Operators](#operators)
|
15. [Operator Overloading](#operator-overloading)
|
||||||
|
16. [Operators](#operators)
|
||||||
|
17. [Macros](#macros)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -522,32 +524,44 @@ func Main() -> int {
|
|||||||
|
|
||||||
## Gradual Ownership (Phase 8.2) ✅ Implemented
|
## Gradual Ownership (Phase 8.2) ✅ Implemented
|
||||||
|
|
||||||
Bux introduces **gradual ownership** — opt-in borrow checking. By default, Bux is permissive like C. With `@[Checked]`, the borrow checker enforces memory safety rules.
|
Bux has **gradual ownership** — opt-in borrow checking. Default is permissive
|
||||||
|
(C-like). Turn safety on where it matters; turn it off on hot paths with zero cost.
|
||||||
|
|
||||||
### Syntax
|
### Three tiers
|
||||||
|
|
||||||
|
| Mode | Attribute | Checks | Cost |
|
||||||
|
|------|-----------|--------|------|
|
||||||
|
| **Default** | (none) | None | Zero — raw `*T`, free aliasing |
|
||||||
|
| **Checked** | `@[Checked]` | Moves, exclusive `&mut`, shared/`&mut` conflicts, dangling returns, elision | Compile-time only |
|
||||||
|
| **Release** | `@[Release]` | **Forced off** (even if also `@[Checked]`) | Zero — same codegen as default |
|
||||||
|
|
||||||
|
**Story:** write most code unchecked for speed of iteration; mark critical APIs
|
||||||
|
`@[Checked]`; mark micro-hotspots `@[Release]` (or both) when you need C-level
|
||||||
|
performance without false positives.
|
||||||
|
|
||||||
```bux
|
```bux
|
||||||
// Default: permissive mode (like C/Nim) — raw pointers, no checks
|
// Tier 1 — default: C-like, no borrow checker
|
||||||
func QuickSort(arr: *int, len: int) {
|
func QuickSort(arr: *int, len: int) {
|
||||||
for i in 0..len {
|
// free to alias, no move tracking
|
||||||
arr[i] = arr[i] * 2;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Opt-in: @[Checked] enables borrow checking
|
// Tier 2 — opt-in safety
|
||||||
@[Checked]
|
@[Checked]
|
||||||
func Scale(val: &mut int) {
|
func Scale(val: &mut int) {
|
||||||
*val = *val * 2; // OK: &mut T allows mutation
|
*val = *val * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@[Checked]
|
// Tier 3 — zero-cost escape (e.g. hot loop helper)
|
||||||
func Read(val: &int) -> int {
|
@[Release]
|
||||||
return *val; // OK: &T allows reading
|
func HotInc(p: *int) {
|
||||||
|
*p = *p + 1; // no checks; same as default, documents intent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Release wins over Checked when both are present
|
||||||
@[Checked]
|
@[Checked]
|
||||||
func BadWrite(val: &int) {
|
@[Release]
|
||||||
*val = 42; // ERROR: cannot write through shared reference '&T'
|
func HotButDocumented(p: &mut int) {
|
||||||
|
*p = *p + 1; // no borrow checks
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -586,37 +600,57 @@ Moves happen in three contexts:
|
|||||||
- **Assignment**: `b = a` moves `a` into `b`
|
- **Assignment**: `b = a` moves `a` into `b`
|
||||||
- **Return**: `return x` moves `x` out of the function
|
- **Return**: `return x` moves `x` out of the function
|
||||||
|
|
||||||
### Rules in @[Checked] functions
|
### Rules in `@[Checked]` functions (not `@[Release]`)
|
||||||
|
|
||||||
- `&T` cannot be used to mutate data (compile-time error)
|
- `&T` cannot be used to mutate data (compile-time error)
|
||||||
- `&mut T` allows mutation
|
- `&mut T` allows mutation
|
||||||
- `*T` pointers are unrestricted (escape hatch)
|
- `*T` pointers are unrestricted (escape hatch)
|
||||||
- `&mut T` coerces to `&T` and `*T`
|
- `&mut T` coerces to `&T` and `*T`
|
||||||
- **Double mutable borrow**: passing `&mut x` twice to the same call is an error
|
- **Double mutable borrow**: two live `&mut` of the same var (call args or let-bound)
|
||||||
```bux
|
```bux
|
||||||
Swap(&mut x, &mut x); // ERROR: double mutable borrow of x
|
Swap(&mut x, &mut x); // ERROR
|
||||||
|
let a: &mut int = &mut x;
|
||||||
|
let b: &mut int = &mut x; // ERROR: exclusive mut already live
|
||||||
```
|
```
|
||||||
- **Use after move**: using a moved `own T` value is an error until reassigned
|
- **Use while mutably borrowed**: assign/use of `x` while a let-bound `&mut x` is live
|
||||||
```bux
|
- **Shared while mut**: cannot form `&x` while `&mut x` is live
|
||||||
let msg: own String = "hello";
|
- **Use after move**: using a moved `own T` until reassigned
|
||||||
Process(msg); // move
|
- **No dangling returns**: cannot return a reference to a local
|
||||||
PrintLine(msg); // ERROR: use of moved value
|
|
||||||
msg = "reassigned"; // OK: reinitialization
|
|
||||||
PrintLine(msg);
|
|
||||||
```
|
|
||||||
- **No dangling returns**: cannot return a reference to a local (or by-value parameter)
|
|
||||||
```bux
|
```bux
|
||||||
@[Checked]
|
@[Checked]
|
||||||
func Bad(p: &int) -> &int {
|
func Bad(p: &int) -> &int {
|
||||||
var x: int = 1;
|
var x: int = 1;
|
||||||
return &x; // ERROR: cannot return reference to local variable
|
return &x; // ERROR
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `@[Release]` (C.4 zero-cost path)
|
||||||
|
|
||||||
|
Use when a function must stay check-free:
|
||||||
|
|
||||||
|
1. **Documented hot path** — same IR as unchecked, but the attribute states intent.
|
||||||
|
2. **Override Checked** — `@[Checked] @[Release]` on a method that would otherwise inherit team-wide Checked defaults.
|
||||||
|
|
||||||
|
There is **no runtime cost**: the attribute only disables the checker for that function body. Prefer `@[Release]` on the smallest possible surface; keep call boundaries `@[Checked]` when you still want API-level safety.
|
||||||
|
|
||||||
|
```bux
|
||||||
|
@[Checked]
|
||||||
|
func SafeApi(buf: &mut int) {
|
||||||
|
// checked here
|
||||||
|
HotPath(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@[Release]
|
||||||
|
func HotPath(p: &mut int) {
|
||||||
|
// no move / borrow tracking — write like C
|
||||||
|
*p = *p + 1;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Lifetime elision (C.1)
|
### Lifetime elision (C.1)
|
||||||
|
|
||||||
In `@[Checked]` functions, most reference signatures need **no** lifetime annotations.
|
In `@[Checked]` functions (and not `@[Release]`), most reference signatures need
|
||||||
Elision applies the usual single-input rules:
|
**no** lifetime annotations. Elision applies the usual single-input rules:
|
||||||
|
|
||||||
1. Each elided input `&T` / `&mut T` parameter gets a distinct lifetime.
|
1. Each elided input `&T` / `&mut T` parameter gets a distinct lifetime.
|
||||||
2. If there is **exactly one** input lifetime, it is assigned to all elided outputs.
|
2. If there is **exactly one** input lifetime, it is assigned to all elided outputs.
|
||||||
@@ -640,8 +674,180 @@ func Pick<'a>(a: &'a int, b: &'a int) -> &'a int {
|
|||||||
// Type parameters: func F<'a, T>(...)
|
// Type parameters: func F<'a, T>(...)
|
||||||
```
|
```
|
||||||
|
|
||||||
Unchecked functions ignore lifetime rules (C-like). Explicit `'a` is optional
|
Default and `@[Release]` functions ignore lifetime rules (C-like). Explicit `'a`
|
||||||
documentation when a single input would already elide correctly.
|
is optional documentation when a single input would already elide correctly.
|
||||||
|
|
||||||
|
### Drop and RAII
|
||||||
|
|
||||||
|
Bux uses **static destructors** (no GC): when a value goes out of scope, the
|
||||||
|
compiler may emit `TypeName_Drop(&local)`. That is the RAII story — resources
|
||||||
|
are released at every exit path without manual `defer` on every return.
|
||||||
|
|
||||||
|
#### Declaring cleanup
|
||||||
|
|
||||||
|
Two equivalent ways to opt a type into auto-drop:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
// 1) Attribute — compiler looks up TypeName_Drop
|
||||||
|
@[Drop]
|
||||||
|
struct Token {
|
||||||
|
id: int,
|
||||||
|
counter: *int,
|
||||||
|
}
|
||||||
|
|
||||||
|
func Token_Drop(self: *Token) {
|
||||||
|
// free / close / decrement …
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Interface (stdlib `lib/Drop.bux`) — same static call, no vtable
|
||||||
|
import Drop;
|
||||||
|
|
||||||
|
extend Buffer for Drop {
|
||||||
|
func Drop(self: *Buffer) {
|
||||||
|
Mem_Free(self.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Stdlib collections implement Drop (`Array_Drop`, `Map_Drop`, …). Calling
|
||||||
|
`Array_Drop` is the same cleanup as `Array_Free` for `Array<T>`.
|
||||||
|
|
||||||
|
#### When auto-drop runs
|
||||||
|
|
||||||
|
Auto-drop is **not** gated on `@[Checked]`. Any function can receive injected
|
||||||
|
`Type_Drop` at:
|
||||||
|
|
||||||
|
| Exit | Behavior |
|
||||||
|
|------|----------|
|
||||||
|
| End of block / function | Drop locals still owned |
|
||||||
|
| Early `return` | Drop all live locals **after** materializing the return value |
|
||||||
|
| Branch scope end | Only locals from the taken branch |
|
||||||
|
| Nested scopes | Drop in reverse order of declaration |
|
||||||
|
|
||||||
|
```bux
|
||||||
|
@[Drop]
|
||||||
|
struct Token { id: int, counter: *int }
|
||||||
|
func Token_Drop(self: *Token) { /* … */ }
|
||||||
|
|
||||||
|
func Early(flag: int, counter: *int) -> int {
|
||||||
|
let t: Token = Token { id: 1, counter: counter };
|
||||||
|
if flag == 0 {
|
||||||
|
return 0; // still runs Token_Drop(&t)
|
||||||
|
}
|
||||||
|
return 1; // Token_Drop(&t) here too
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See `examples/drop_early_return.bux` for branch-local vs fallthrough counts.
|
||||||
|
|
||||||
|
#### Field-move: skip Drop of the source (critical)
|
||||||
|
|
||||||
|
**Problem:** a local is moved **by value** into a struct field (or another local).
|
||||||
|
If the compiler still auto-dropped the source, you get a **double free** — the
|
||||||
|
field and the original local would both run `Array_Drop` on the same buffer.
|
||||||
|
|
||||||
|
**Rule:** after a **value move** out of a local, that local is **not** dropped.
|
||||||
|
|
||||||
|
```bux
|
||||||
|
struct Box {
|
||||||
|
items: Array<int>;
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeBox() -> Box {
|
||||||
|
var items: Array<int> = Array_New<int>(4);
|
||||||
|
Array_Push<int>(&items, 10);
|
||||||
|
Array_Push<int>(&items, 20);
|
||||||
|
// Move `items` into the field — compiler skips Drop of `items`
|
||||||
|
let b: Box = Box { items: items };
|
||||||
|
return b; // also: return-by-value skips Drop of `b` (caller owns it)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
What the C backend does for `MakeBox` (simplified):
|
||||||
|
|
||||||
|
```c
|
||||||
|
Box MakeBox(void) {
|
||||||
|
Array_int items = Array_New_int(4);
|
||||||
|
Array_Push_int(&items, 10);
|
||||||
|
Array_Push_int(&items, 20);
|
||||||
|
Box b = (Box){ .items = items };
|
||||||
|
return b;
|
||||||
|
/* no Array_Drop_int(&items); — moved into b.items */
|
||||||
|
/* no Array_Drop on b; — moved to caller via return */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ownership after `MakeBox`:
|
||||||
|
|
||||||
|
1. Heap buffer lives inside `b.items` (and later the caller's `Box`).
|
||||||
|
2. `items` is **moved-out** → skip auto-Drop.
|
||||||
|
3. `b` is **returned by value** → skip auto-Drop at the return site; the caller
|
||||||
|
(or the next owner) is responsible.
|
||||||
|
|
||||||
|
The same skip applies to:
|
||||||
|
|
||||||
|
- **Struct field init** — `S { field: local }` (field-move)
|
||||||
|
- **Assignment** — `a = b` when `b` is moved (value types with Drop)
|
||||||
|
- **Call argument** by value into a consuming parameter
|
||||||
|
- **`return x`** — move-on-return
|
||||||
|
|
||||||
|
Live, unmoved Drop locals still clean up on error paths (e.g. early `return`
|
||||||
|
before the move). That is intentional: only the **successful transfer** path
|
||||||
|
skips Drop.
|
||||||
|
|
||||||
|
Runnable check: `examples/move_field.bux` (also covered by
|
||||||
|
`make test-selfhost-smoke` on buxc2).
|
||||||
|
|
||||||
|
#### Partial field moves
|
||||||
|
|
||||||
|
Moving a **droppable field** out of a local (return or `let`) also skips Drop
|
||||||
|
of the **parent** local:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
@[Drop]
|
||||||
|
struct Bag {
|
||||||
|
items: Array<int>,
|
||||||
|
tag: int,
|
||||||
|
}
|
||||||
|
func Bag_Drop(self: *Bag) {
|
||||||
|
Array_Drop<int>(&self.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
func TakeItems() -> Array<int> {
|
||||||
|
var items: Array<int> = Array_New<int>(4);
|
||||||
|
Array_Push<int>(&items, 42);
|
||||||
|
let bag: Bag = Bag { items: items, tag: 7 };
|
||||||
|
return bag.items; // Bag_Drop skipped — items ownership transferred
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- Applies only when the **field type** is droppable (`Array_*`, `@[Drop]` types,
|
||||||
|
etc.). Reading `bag.tag` (`int`) does **not** mark `bag` moved.
|
||||||
|
- After `let moved = bag.items`, `Bag_Drop(&bag)` is skipped; `moved` owns the
|
||||||
|
array and is auto-dropped at scope end.
|
||||||
|
- Avoid using other droppable fields of the parent after a partial move (they
|
||||||
|
may be left in a moved-from state without per-field Drop).
|
||||||
|
|
||||||
|
Golden smoke: `make test-drop-move` / `examples/move_field_partial.bux`.
|
||||||
|
|
||||||
|
#### Manual Drop and non-Drop types
|
||||||
|
|
||||||
|
- Types **without** `@[Drop]` / `Drop` impl are never auto-dropped (plain C layout).
|
||||||
|
- You can still call `Type_Drop(&x)` or use `defer` for explicit cleanup.
|
||||||
|
- `@[Release]` / default functions still get auto-drop for Drop types — Release
|
||||||
|
only turns off the **borrow checker**, not RAII.
|
||||||
|
|
||||||
|
#### Limits (honest)
|
||||||
|
|
||||||
|
- Partial field moves mark the **whole parent local** as moved for Drop purposes
|
||||||
|
(not per-field Drop of remaining fields).
|
||||||
|
- Nested `a.b.c` path moves and moving through pointers are limited.
|
||||||
|
- Interface Drop uses a static `TypeName_Drop` symbol (zero cost), not dynamic
|
||||||
|
dispatch through a vtable.
|
||||||
|
- Double-free bugs in **unchecked** code that manually free *and* auto-drop are
|
||||||
|
still possible if you free without invalidating the value — prefer one owner.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -893,3 +1099,198 @@ Overloadable operators use the naming convention `TypeName_operator_<op>`:
|
|||||||
- `..` — Range (exclusive): `0..10`
|
- `..` — Range (exclusive): `0..10`
|
||||||
- `..=` — Range (inclusive): `0..=10`
|
- `..=` — Range (inclusive): `0..=10`
|
||||||
- `sizeof` — Size of type: `sizeof(Type)`
|
- `sizeof` — Size of type: `sizeof(Type)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Macros
|
||||||
|
|
||||||
|
Bux supports **declarative macros**. Expansion runs after parse and before
|
||||||
|
type-checking. Expanded AST uses **call-site** source locations (quote hygiene).
|
||||||
|
Both bootstrap and selfhost (`buxc2`) expand macros.
|
||||||
|
|
||||||
|
### Definition
|
||||||
|
|
||||||
|
```bux
|
||||||
|
macro! twice {
|
||||||
|
($x:expr) => {
|
||||||
|
($x) + ($x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trailing repetition
|
||||||
|
macro! sum_n {
|
||||||
|
( $($x:expr),* ) => {
|
||||||
|
var acc: int = 0;
|
||||||
|
$( acc = acc + $x; )*
|
||||||
|
acc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compound / zip: parallel lists from interleaved args
|
||||||
|
macro! add_pairs {
|
||||||
|
( $($a:expr, $b:expr),* ) => {
|
||||||
|
var acc: int = 0;
|
||||||
|
$( acc = acc + ($a + $b); )*
|
||||||
|
acc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-rep groups: `;` separates arg groups at the call site
|
||||||
|
macro! sum_groups {
|
||||||
|
( $($x:expr),* ; $($y:expr),* ) => {
|
||||||
|
var s: int = 0;
|
||||||
|
$( s = s + $x; )*
|
||||||
|
$( s = s + $y; )*
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nested template repetition (outer list → inner expands once per item)
|
||||||
|
macro! double_each_sum {
|
||||||
|
( $($x:expr),* ) => {
|
||||||
|
var t: int = 0;
|
||||||
|
$(
|
||||||
|
$( t = t + $x; )*
|
||||||
|
$( t = t + $x; )*
|
||||||
|
)*
|
||||||
|
t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ident fragment: bare identifier at the call site
|
||||||
|
macro! call0 {
|
||||||
|
( $f:ident ) => {
|
||||||
|
$f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// literal (alias: lit) — only int/float/string/char/bool literals
|
||||||
|
macro! only_lit {
|
||||||
|
( $x:literal ) => { $x }
|
||||||
|
}
|
||||||
|
|
||||||
|
// block — only `{ … }` block expressions
|
||||||
|
macro! wrap_block {
|
||||||
|
( $b:block ) => { $b }
|
||||||
|
}
|
||||||
|
|
||||||
|
// gensym: template locals renamed per expansion
|
||||||
|
macro! with_acc {
|
||||||
|
( $start:literal ) => {
|
||||||
|
var n: int = $start;
|
||||||
|
n = n + 1;
|
||||||
|
n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Introduced with the `macro!` keyword.
|
||||||
|
- Each **rule** is `( pattern ) => { template }`.
|
||||||
|
- **Fragment kinds:**
|
||||||
|
|
||||||
|
| Kind | Matches |
|
||||||
|
|------|---------|
|
||||||
|
| `expr` | any expression |
|
||||||
|
| `ident` | bare identifier (`ekIdent`) |
|
||||||
|
| `tt` | token-tree (MVP: same as `expr`) |
|
||||||
|
| `literal` / `lit` | int/float/string/char/bool literal only |
|
||||||
|
| `block` | block expression `{ … }` |
|
||||||
|
|
||||||
|
- Fragment names start with `$` (lexer `$ident`).
|
||||||
|
- **Repetition:** `$( $x:expr ),*` / `$( $x:expr )*` — one or more rep fragments per pattern.
|
||||||
|
- **Compound rep:** `$( $a:expr, $b:expr ),*` — interleaved args zip into parallel lists.
|
||||||
|
- **Multi-rep:** two (or more) `$(…)*` in one pattern; call site uses `;` between groups:
|
||||||
|
`sum_groups!(1, 2; 10, 20, 30)`.
|
||||||
|
- Template `$( stmt; … )*` expands once per list item (zip when multiple lists used).
|
||||||
|
- Nested `$( $(…)* )*`: after outer binds list items as singles, inner expands once.
|
||||||
|
|
||||||
|
### Invocation
|
||||||
|
|
||||||
|
```bux
|
||||||
|
let n = twice!(21);
|
||||||
|
let s = sum_n!(1, 2, 3); // 6
|
||||||
|
let z = sum_n!(); // 0
|
||||||
|
let p = add_pairs!(1, 10, 2, 20); // (1+10)+(2+20) = 33
|
||||||
|
let g = sum_groups!(1, 2; 10, 20, 30); // 63
|
||||||
|
let d = double_each_sum!(3, 4); // 14
|
||||||
|
call0!(SomeFunc);
|
||||||
|
let a = with_acc!(10); // 11
|
||||||
|
let b = with_acc!(20); // 21 — different gensym'd `n`
|
||||||
|
let c = only_lit!(7);
|
||||||
|
// only_lit!(1 + 2); // ERROR: no matching rule
|
||||||
|
let w = wrap_block!({ 1 + 2 }); // 3
|
||||||
|
```
|
||||||
|
|
||||||
|
- Syntax: `name!( arg, … )` (not unwrap: unwrap is `expr!` without `(`).
|
||||||
|
- Matching: fixed-arity by count; kind constraints; rep by groups / remaining args / chunk.
|
||||||
|
|
||||||
|
### Built-in `quote!`
|
||||||
|
|
||||||
|
```bux
|
||||||
|
let x = quote!(1 + 2); // identity expand; locations grafted to call site
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hygiene
|
||||||
|
|
||||||
|
Two layers (both bootstrap + selfhost):
|
||||||
|
|
||||||
|
1. **Call-site graft** — expanded AST uses the call site’s line/col/`sourceFile`
|
||||||
|
(so diagnostics and `#line` point at the user call, not the macro definition).
|
||||||
|
2. **Gensym of template binders** — each expansion renames:
|
||||||
|
- `let` / `var` locals introduced by the template
|
||||||
|
- `for` loop binders in the template
|
||||||
|
- Nested scopes (if/while/for bodies, MacroRep bodies)
|
||||||
|
|
||||||
|
so two expansions of the same macro in one function do not collide under the
|
||||||
|
C backend’s **function-scoped** locals (e.g. `__m1_n` and `__m2_n`).
|
||||||
|
|
||||||
|
Spliced `$frags` in expression positions are **not** gensym’d — they keep
|
||||||
|
call-site names/values.
|
||||||
|
|
||||||
|
#### Unhygienic binders (`var $name`)
|
||||||
|
|
||||||
|
To **introduce a binder whose name comes from the call site**, use a `$frag`
|
||||||
|
as the binder itself. That name is **not** gensym’d:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
macro! let_mut {
|
||||||
|
( $name:ident, $init:literal ) => {
|
||||||
|
var $name: int = $init; // unhygienic: becomes `counter`, not __m1_…
|
||||||
|
$name = $name + 1;
|
||||||
|
$name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// expands with local `counter` (and hygienic locals still unique)
|
||||||
|
let a = let_mut!(counter, 10); // 11
|
||||||
|
let b = let_mut!(other, 20); // 21
|
||||||
|
|
||||||
|
macro! double_acc {
|
||||||
|
( $start:literal ) => {
|
||||||
|
var acc: int = $start; // hygienic → __m1_acc / __m2_acc
|
||||||
|
acc = acc + acc;
|
||||||
|
acc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Binder form | After expand | Gensym? |
|
||||||
|
|-------------|--------------|---------|
|
||||||
|
| `var acc = …` (plain name in template) | `__mN_acc` | yes |
|
||||||
|
| `var $name = …` with `$name:ident` | call-site ident | **no** |
|
||||||
|
| `for $i in …` with `$i:ident` | call-site ident | **no** |
|
||||||
|
|
||||||
|
The binder must be a **`:ident` fragment** bound to a bare identifier. A plain
|
||||||
|
template name is always hygienic.
|
||||||
|
|
||||||
|
Examples: `examples/macro_hygiene.bux`, `examples/macro_unhygienic.bux`.
|
||||||
|
|
||||||
|
### Limits
|
||||||
|
|
||||||
|
- Up to two named rep lists per rule on selfhost (enough for zip + multi-rep).
|
||||||
|
- Compound chunk size currently 1 or 2.
|
||||||
|
- Nested macro *calls* expanded recursively (depth limit 32).
|
||||||
|
- Unhygienic binders only rename `let`/`var`/`for` binders — not full
|
||||||
|
Scheme/Rust colored identifiers or `stmt`/`pat` token trees.
|
||||||
|
- Macro expansion still yields a **block expression**; unhygienic names are
|
||||||
|
scoped to that block (not automatically injected into the caller scope).
|
||||||
|
|||||||
+259
-7
@@ -1,7 +1,7 @@
|
|||||||
# Bux — План към „добър“ език (v0.5 → v1.0)
|
# Bux — План към „добър“ език (v0.5 → v1.0)
|
||||||
|
|
||||||
> **Дата:** 2026-07-19
|
> **Дата:** 2026-07-20
|
||||||
> **Текущо:** v0.5.x — quote/graft hygiene, LSP 0.15, CI, fixed-point
|
> **Текущо:** v0.5.x — macros (unhygienic binders + multi-rep), partial field-move, lean CI
|
||||||
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
|
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
| C.1 | Lifetime elision за common cases | Без `'a` в 90% от API-тата | ✅ bootstrap + selfhost |
|
| C.1 | Lifetime elision за common cases | Без `'a` в 90% от API-тата | ✅ bootstrap + selfhost |
|
||||||
| C.2 | Exclusive `&mut` vs shared `&` data-flow | По-малко false negatives | ✅ let-bound + use-while + call conflict |
|
| C.2 | Exclusive `&mut` vs shared `&` data-flow | По-малко false negatives | ✅ let-bound + use-while + call conflict |
|
||||||
| C.3 | Auto-drop edge cases (early return, branches) | RAII да е надежден | ✅ bootstrap + selfhost |
|
| C.3 | Auto-drop edge cases (early return, branches) | RAII да е надежден | ✅ bootstrap + selfhost |
|
||||||
| C.4 | `@[Release]` zero-cost path документация + golden tests | Killer story: safe default, free hot path | ✅ partial (unchecked path + goldens) |
|
| C.4 | `@[Release]` zero-cost path документация + golden tests | Killer story: safe default, free hot path | ✅ full (docs + Release wins + tests + example) |
|
||||||
|
|
||||||
### D — Tooling (P1)
|
### D — Tooling (P1)
|
||||||
|
|
||||||
@@ -864,9 +864,261 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Сесия 56 (CBE binary parentheses — C precedence safety)
|
||||||
|
|
||||||
|
1. **Bug (selfhost):** tree HIR→C emitted nested binaries **without** parens.
|
||||||
|
- `return (a + b) * c` → C `return a + b * c;` → **7** instead of **9**
|
||||||
|
- `return (a - b) / c` → `a - b / c` → **8** instead of **3**
|
||||||
|
2. **Fix selfhost** (`src/c_backend.bux`):
|
||||||
|
- `hBinary` always emits `(left op right)` (same policy as bootstrap HIR CBE)
|
||||||
|
- Unary operand parens (session 52) kept: `!(a && b)`
|
||||||
|
3. **Bootstrap LIR** (`bootstrap/lir_c_backend.nim`): defensive parens on
|
||||||
|
arith/bitwise and unary `!`/`-`/`~` (operands are temps today; future-proof)
|
||||||
|
4. **Example:** `examples/c_precedence.bux` — MulSum/SubDiv/ShiftSum/Mix
|
||||||
|
5. **Smoke:** `tools/smoke_selfhost.sh` checks run values **and** generated C
|
||||||
|
contains `(a + b) * c` / `(a - b) / c`
|
||||||
|
6. Wired into `EXAMPLES` + `make test-selfhost-smoke`
|
||||||
|
7. Verified: bootstrap + **buxc2** → `9/3/6/7` + `PASS c_precedence`;
|
||||||
|
smoke PASS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сесия 57 (CI split jobs + macOS smoke)
|
||||||
|
|
||||||
|
1. **`.github/workflows/ci.yml`** — no longer one 90m monolithic `make test`:
|
||||||
|
- **`build`** (ubuntu): `make build` → artifact `buxc-linux`
|
||||||
|
- **Parallel** (reuse artifact, `BUX_SKIP_BUILD=1`):
|
||||||
|
- `unit` — `fmt-check` + `test-unit`
|
||||||
|
- `examples` — `test-examples`
|
||||||
|
- `goldens` — errors + stdlib + registry + dwarf
|
||||||
|
- `apps` — `test-apps`
|
||||||
|
- `selfhost` — `test-selfhost-smoke`
|
||||||
|
- **`macos`**: Homebrew OpenSSL + rebuild + fmt/unit/examples
|
||||||
|
- **`ci-gate`**: single required status (all of the above must succeed)
|
||||||
|
2. **Makefile:**
|
||||||
|
- `test-unit` extracted from `test`
|
||||||
|
- `ensure-buxc` + `BUX_SKIP_BUILD=1` for CI artifact reuse
|
||||||
|
- `$(OUT)` rebuild only when `bootstrap/*.nim` changes
|
||||||
|
- portable examples runner (optional `timeout`; macOS without coreutils OK)
|
||||||
|
3. **macOS / non-GNU ld:**
|
||||||
|
- bootstrap: `-Wl,--build-id=none` only `when defined(linux)`
|
||||||
|
- selfhost: `bux_cc_ld_stable()` in `rt/runtime.c` (Linux-only build-id)
|
||||||
|
- CI sets `BUX_CFLAGS=-I… -L…` for Homebrew `libcrypto`
|
||||||
|
4. Docs: BuildAndTest + README CI table; local `make test` still full sequential
|
||||||
|
5. Verified locally: `BUX_SKIP_BUILD=1 make fmt-check test-unit test-errors`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сесия 58 (LSP 0.16 workspace type hierarchy index)
|
||||||
|
|
||||||
|
1. **Gap:** type hierarchy subtypes/supertypes only saw open-doc `impls` or
|
||||||
|
method-keyed `workspaceImpls`. Closed files with **empty**
|
||||||
|
`extend T for I {}` (no methods) returned **[]**.
|
||||||
|
2. **`workspaceTypeRels`** (`tools/lsp_server.nim`):
|
||||||
|
- URI → `seq[(typeName, iface, line)]` from every `analyzeFile`
|
||||||
|
- `registerWorkspaceTypeRels` replaces per-URI (re-open / re-scan safe)
|
||||||
|
- filled on `scanWorkspace` + open/edit — **no open doc required**
|
||||||
|
3. **Consumers:**
|
||||||
|
- `collectSubtypeItems` / `collectSupertypeItems` prefer type-rel index
|
||||||
|
- `collectTypeImplementorLocs` (textDocument/implementation) same
|
||||||
|
4. **Smoke:** `tools/smoke_lsp_type_hierarchy_ws.sh`
|
||||||
|
- open only Main; Drawable.bux + Shapes.bux closed
|
||||||
|
- empty extends → Drawable subtypes Circle+Square; Circle supers Drawable+Named
|
||||||
|
5. Version **bux-lsp 0.16.0**; wired into `make test-lsp`
|
||||||
|
6. Verified: single-file hierarchy + workspace smoke PASS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сесия 59 (user-facing `macro!` / `quote!` — declarative MVP)
|
||||||
|
|
||||||
|
1. **Syntax (bootstrap):**
|
||||||
|
- `macro! name { ($x:expr, …) => { template } }`
|
||||||
|
- Invoke: `name!(args…)` — distinct from unwrap `expr!` via following `(`
|
||||||
|
- Built-in **`quote!(e)`** — identity expand + call-site graft
|
||||||
|
2. **Lexer:** keyword `macro`; `$ident` fragment tokens (`$x`)
|
||||||
|
3. **AST:** `dkMacro` + `MacroRule`/`MacroFragment`; `ekMacroCall`
|
||||||
|
4. **Expansion** (`bootstrap/macroexpand.nim`) before sema:
|
||||||
|
- Collect macro decls; match rule by arity
|
||||||
|
- Deep clone + substitute `$frags` + graft call-site `SourceLocation`
|
||||||
|
- Nested expand (depth ≤ 32)
|
||||||
|
5. **CLI:** `build` / `check` / `run` call `expandMacros` after merge
|
||||||
|
6. **Example:** `examples/macro_twice.bux` → 42 / 42 / 43 + PASS
|
||||||
|
7. **LanguageRef:** Macros section (limits documented)
|
||||||
|
8. Verified: `./buxc run macro_twice`; hello + lexer/parser tests green
|
||||||
|
9. **Not yet:** selfhost expand parity; `$(…)*` repetition; more frag kinds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сесия 60 (selfhost `macro!` / `quote!` expand parity)
|
||||||
|
|
||||||
|
1. **Lexer/token:** `tkMacro`, keyword `macro`, `$ident` fragments
|
||||||
|
2. **AST:** `dkMacro` (rules in `childDecl1` chain), `ekMacroCall`
|
||||||
|
3. **Parser:** `macro! name { ($x:expr) => {…} }`, invoke `name!(…)`
|
||||||
|
4. **`src/macroexpand.bux`:**
|
||||||
|
- collect macros → match rule by arity → clone+subst `$frags`
|
||||||
|
- call-site graft (line/col/`sourceFile`)
|
||||||
|
- built-in `quote!(e)`
|
||||||
|
5. **CLI:** expand before sema (project / check / compile paths)
|
||||||
|
6. **Sema fixes** (needed for block templates):
|
||||||
|
- `ekBlock` value = last `skExpr` type (was always `tyVoid`)
|
||||||
|
- `let x: T = …` sets `sym.typeKind` from annotation (not only init)
|
||||||
|
7. **Smoke:** `tools/smoke_selfhost.sh` runs `examples/macro_twice.bux` via buxc2
|
||||||
|
8. Verified: **buxc2** + bootstrap → `42/42/43` + `PASS macro_twice`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сесия 61 (macro `$(…)*` + `ident`/`tt` fragments)
|
||||||
|
|
||||||
|
1. **Fragment kinds:** `expr` | `ident` | `tt` (tt ≡ expr for now)
|
||||||
|
2. **Pattern rep (trailing):** `$( $x:expr ),*` / `$( $x:expr )*`
|
||||||
|
3. **Template rep:** `$( stmts… )*` → `skMacroRep`, expanded per list item
|
||||||
|
4. **Lexer:** bare `tkDollar` for `$(…)` (vs `$ident`)
|
||||||
|
5. **Bootstrap** `macroexpand.nim`: list bindings, match rules, gensym locals
|
||||||
|
6. **Selfhost** parity: `useNames` encodes kinds/`rep:`, `Subst_Block_Flat`, gensym
|
||||||
|
7. **Sema:** block-as-expr checks last value **inside** child scope (no UAF of locals)
|
||||||
|
8. **Example:** `examples/macro_repeat.bux` — sum_n / empty / call0 / id_tt
|
||||||
|
9. Verified: bootstrap + **buxc2** → `6/0/42/7` + `PASS macro_repeat`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сесия 62 (C.4 `@[Release]` polish + Checked docs)
|
||||||
|
|
||||||
|
1. **Three-tier model** documented in LanguageRef:
|
||||||
|
- default (no checks) → `@[Checked]` → `@[Release]` (force off)
|
||||||
|
2. **Bootstrap:** `releaseFunc`; `checkedFunc = Checked ∧ ¬Release`
|
||||||
|
3. **Selfhost:** same rule; **stacked attrs** loop (`@[Checked]` + `@[Release]`)
|
||||||
|
4. **Parser:** multi-line stacked `@[…]` (skip newlines between attrs)
|
||||||
|
5. **Tests** (`borrow_test`): Release alone; Checked+Release wins; Checked still errors
|
||||||
|
6. **Example:** `examples/ownership_release.bux` — Unchecked / Safe / Hot / HotDangle
|
||||||
|
7. Verified: 27/27 borrow tests; example PASS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сесия 63 (nested `$(…)*` / multi-rep / compound zip)
|
||||||
|
|
||||||
|
1. **Bootstrap** (`macroexpand.nim` + parser):
|
||||||
|
- Compound rep: `$( $a:expr, $b:expr ),*` → parallel lists, zip in template
|
||||||
|
- Multi-rep: `$(…)* ; $(…)*` with call-site `;` groups (`exprMacroGroupLens`)
|
||||||
|
- Nested template: outer binds list → inner `$(…)*` expands once (no list names left)
|
||||||
|
- `MacroFragment.names` / `.kinds` for multi-name frags
|
||||||
|
2. **Selfhost** parity (`src/macroexpand.bux`, `parser.bux`):
|
||||||
|
- Two named rep lists + zip in `Subst_Block_Flat`
|
||||||
|
- Kinds encoding `rep:expr+expr,@2` / multi-seg `rep:…;rep:…`
|
||||||
|
- Macro call `;` groups → `genericCallee` group-length string
|
||||||
|
3. **Example:** `examples/macro_nested.bux`
|
||||||
|
- `add_pairs` → 33, `sum_groups` → 63, `double_each_sum` → 14, `named_sum` → 18
|
||||||
|
4. **LanguageRef:** multi-rep / compound / nested docs; limits updated
|
||||||
|
5. Verified: bootstrap + **buxc2** → PASS macro_nested / macro_repeat / macro_twice
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сесия 64 (CI Nim cache + faster macOS)
|
||||||
|
|
||||||
|
1. **Nim pin + toolchain cache:**
|
||||||
|
- `NIM_VERSION: 2.0.8` (stable cache keys; was `2.0.x`)
|
||||||
|
- Cache `.nim_runtime` on `build` / `unit` / `macos` / `selfhost-loop`
|
||||||
|
- Skip `setup-nim-action` on cache hit; restore `PATH` only
|
||||||
|
2. **`nimcache` project-local:**
|
||||||
|
- `Makefile` `NIMFLAGS ?= --nimcache:nimcache` for bootstrap + unit tests
|
||||||
|
- `actions/cache` keyed on `bootstrap/**/*.nim` (+ tests for unit)
|
||||||
|
3. **Leaner macOS job:**
|
||||||
|
- Runner `macos-14`; timeout 35m
|
||||||
|
- `make test-unit` + `make test-examples-smoke` (not full EXAMPLES / not fmt)
|
||||||
|
- `EXAMPLES_SMOKE`: hello, ownership*, strings, map, c_precedence, macro_*
|
||||||
|
- OpenSSL: install only if missing (`brew list`)
|
||||||
|
4. **Docs:** BuildAndTest CI table; `.gitignore` `.nim_runtime/`
|
||||||
|
5. Verified locally: `make build` uses `nimcache/`; `test-examples-smoke` PASS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сесия 65 (Drop / RAII docs — field-move story)
|
||||||
|
|
||||||
|
1. **LanguageRef — Drop and RAII** (under Gradual Ownership):
|
||||||
|
- `@[Drop]` vs `extend T for Drop` (static `Type_Drop`, no vtable)
|
||||||
|
- When auto-drop runs (block end, early return, branches) — not gated on Checked
|
||||||
|
- **Field-move skip Drop** with `MakeBox` + simplified C (no `Array_Drop(&items)`)
|
||||||
|
- Move-on-return, assignment, call-arg transfers; error-path still Drops
|
||||||
|
- Limits: whole-local moves, static dispatch, manual free pitfalls
|
||||||
|
2. **TOC** links Ownership + Drop; **Stdlib** `Array_Drop` + `Std::Drop` section
|
||||||
|
3. **README** Drop line mentions field-move
|
||||||
|
4. Cross-refs: `examples/move_field.bux`, `examples/drop_early_return.bux`,
|
||||||
|
selfhost smoke
|
||||||
|
5. Verified: `move_field` C has no `Array_Drop` on moved `items`; example PASS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сесия 66 (macro hygiene + frag kinds `literal` / `block`)
|
||||||
|
|
||||||
|
1. **Fragment kinds** (bootstrap + selfhost):
|
||||||
|
- `literal` / `lit` — only `ekLiteral` (rejects `1 + 2`)
|
||||||
|
- `block` — only `ekBlock` `{ … }`
|
||||||
|
- Shared `fragMatches` / `Macro_FragMatches` at match time
|
||||||
|
2. **Hygiene gensym:**
|
||||||
|
- Bootstrap: also rename **`for` binders**; walk for bodies in collect
|
||||||
|
- Selfhost: gensym `skFor` + recurse if/while/for/MacroRep bodies
|
||||||
|
- CBE: two `with_acc!` → `__m1_n` / `__m2_n` (no collision)
|
||||||
|
3. **Example:** `examples/macro_hygiene.bux` → 11/21/7/3/42 + PASS
|
||||||
|
4. **LanguageRef:** kind table + hygiene layers (graft + gensym)
|
||||||
|
5. **Makefile:** `macro_hygiene` in EXAMPLES + EXAMPLES_SMOKE
|
||||||
|
6. Verified: bootstrap + **buxc2**; negative `only_lit!(1+2)` → no matching rule
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сесия 67 (CI Windows smoke)
|
||||||
|
|
||||||
|
1. **`.github/workflows/ci.yml` — `windows` job** (`windows-latest`, bash shell):
|
||||||
|
- Cache Nim **2.0.8** (prebuilt zip — fast) + `nimcache`
|
||||||
|
- `nim c -o:buxc.exe` bootstrap
|
||||||
|
- Pure Nim unit tests: lexer / parser / sema / hir / borrow
|
||||||
|
- CLI smoke: `buxc.exe new` + `--version`
|
||||||
|
2. **Scope (honest):** no `bux run` examples on Windows yet —
|
||||||
|
`rt/runtime.c` is POSIX (`ucontext`, `pthread`, sockets, OpenSSL link).
|
||||||
|
Job still gates bootstrap regressions on Win.
|
||||||
|
3. **`ci-gate`:** `windows` is a required job
|
||||||
|
4. **Docs:** BuildAndTest CI table + Windows note
|
||||||
|
5. Locally: YAML validated; full Win run is on GHA only
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сесия 68 (partial field moves + Drop goldens)
|
||||||
|
|
||||||
|
1. **Bug:** `return bag.items` still ran `Bag_Drop(&bag)` → double-free /
|
||||||
|
corrupt Array (ASSERT fail). Also dead double-Drop after terminal `return`.
|
||||||
|
2. **Bootstrap** (`hir_lower.nim`):
|
||||||
|
- `markMovedOutFromAst` handles `ekField` when **field type is droppable**
|
||||||
|
(`autoDropFuncName`) — not for `return a.id` (int)
|
||||||
|
- Scope exit: skip re-emitting drops when last stmt always-returns; pop defers
|
||||||
|
3. **Selfhost** (`c_backend.bux`):
|
||||||
|
- `CBE_MarkMovedFromNodeHint` + droppable type check; return uses `currentRetType`
|
||||||
|
- Store/let rhs walks field access for partial moves
|
||||||
|
4. **Example + golden smoke:**
|
||||||
|
- `examples/move_field_partial.bux`
|
||||||
|
- `tools/smoke_drop_move.sh` + `make test-drop-move` (CI goldens job)
|
||||||
|
5. Verified: partial PASS; `TakeItems` has **no** `Bag_Drop`; drop_early_return 5;
|
||||||
|
move_field PASS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сесия 69 (macro unhygienic binders)
|
||||||
|
|
||||||
|
1. **Problem:** gensym renamed *all* template `let`/`var` binders, so
|
||||||
|
`var $name: int = …` with `$name:ident` could not introduce a call-site name.
|
||||||
|
2. **Bootstrap** (`macroexpand.nim`):
|
||||||
|
- `binderIdentFromFrag` + `expandUnhygienic` set
|
||||||
|
- `substStmt`: rewrite `skLet`/`skFor` binder when name is `$frag` → ekIdent
|
||||||
|
- `collectLetNames` skips unhygienic names
|
||||||
|
3. **Selfhost** (`macroexpand.bux`):
|
||||||
|
- `Env_AddUnhy` / `Env_IsUnhy` / `Env_BinderFromFrag`
|
||||||
|
- `Subst_Stmt` rewrites binders; `Macro_GensymBlock(ex, body, env)` skips them
|
||||||
|
4. **Example:** `examples/macro_unhygienic.bux` → 11/21/6/10/1 + PASS
|
||||||
|
- C: `counter` / `other` / `n` kept; `acc` / `scratch` → `__mN_*`
|
||||||
|
5. **LanguageRef:** unhygienic binder table; EXAMPLES + EXAMPLES_SMOKE
|
||||||
|
6. Verified: bootstrap + **buxc2**; macro_hygiene still PASS
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Следващи стъпки
|
## Следващи стъпки
|
||||||
|
|
||||||
1. Parenthesize binary ops in CBE for full C precedence safety
|
1. Windows: MinGW + runtime stubs for `hello` smoke (stretch)
|
||||||
2. CI matrix (macOS) or split jobs for faster PR feedback
|
2. Per-field Drop after partial move (stretch)
|
||||||
3. Type hierarchy for multi-file closed docs without open (workspace type index)
|
3. Macro: true `stmt`/`pat` token-tree frags (stretch)
|
||||||
4. User-facing `macro!` / `quote` syntax on top of graft/clone
|
|
||||||
|
|||||||
+25
-1
@@ -95,6 +95,12 @@ struct Array<T> {
|
|||||||
| `Array_Clear<T>` | `func Array_Clear<T>(arr: *Array<T>)` | Set length to 0 (keeps capacity) |
|
| `Array_Clear<T>` | `func Array_Clear<T>(arr: *Array<T>)` | Set length to 0 (keeps capacity) |
|
||||||
| `Array_Reserve<T>` | `func Array_Reserve<T>(arr: *Array<T>, minCap: uint)` | Grow capacity if needed |
|
| `Array_Reserve<T>` | `func Array_Reserve<T>(arr: *Array<T>, minCap: uint)` | Grow capacity if needed |
|
||||||
| `Array_Free<T>` | `func Array_Free<T>(arr: *Array<T>)` | Free memory |
|
| `Array_Free<T>` | `func Array_Free<T>(arr: *Array<T>)` | Free memory |
|
||||||
|
| `Array_Drop<T>` | `func Array_Drop<T>(self: *Array<T>)` | Drop trait entry (same as `Array_Free`) |
|
||||||
|
|
||||||
|
**RAII:** `Array<T>` is auto-dropped at scope exit. Prefer letting the compiler call
|
||||||
|
`Array_Drop` over manual `Array_Free` when ownership is clear. If you move an
|
||||||
|
array into a struct field, the **source local is not dropped** (see LanguageRef
|
||||||
|
[Drop and RAII](LanguageRef.md#drop-and-raii) / `examples/move_field.bux`).
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
```bux
|
```bux
|
||||||
@@ -105,13 +111,31 @@ func Main() -> int {
|
|||||||
Array_Push<int>(&arr, 10);
|
Array_Push<int>(&arr, 10);
|
||||||
Array_Push<int>(&arr, 20);
|
Array_Push<int>(&arr, 20);
|
||||||
PrintInt(Array_Get<int>(&arr, 0)); // 10
|
PrintInt(Array_Get<int>(&arr, 0)); // 10
|
||||||
Array_Free<int>(&arr);
|
// Array_Drop runs at end of Main (or call Array_Free manually)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Std::Drop
|
||||||
|
|
||||||
|
Trait for automatic cleanup (RAII). Defined in `lib/Drop.bux`:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
interface Drop {
|
||||||
|
func Drop(self: *Self);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Implement with `extend Type for Drop { func Drop(self: *Type) { … } }` or mark
|
||||||
|
the type `@[Drop]` and provide `Type_Drop`. Full rules (early return, field-move
|
||||||
|
skip Drop, move-on-return): **LanguageRef → Drop and RAII**.
|
||||||
|
|
||||||
|
Examples: `examples/drop_early_return.bux`, `examples/move_field.bux`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Std::Iter
|
## Std::Iter
|
||||||
|
|
||||||
Lightweight iterator over `Array<T>` (index-based, no allocation).
|
Lightweight iterator over `Array<T>` (index-based, no allocation).
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// C precedence safety: Bux AST must survive C codegen without rewrite.
|
||||||
|
// Mul(Add(a,b), c) must stay (a+b)*c, not a+b*c.
|
||||||
|
import Std::Io::{PrintLine};
|
||||||
|
import Std::String::{String_FromInt};
|
||||||
|
|
||||||
|
func MulSum(a: int, b: int, c: int) -> int {
|
||||||
|
// AST: (a+b)*c → 9 for (1,2,3); wrong C emit gives 1+2*3=7
|
||||||
|
return (a + b) * c;
|
||||||
|
}
|
||||||
|
|
||||||
|
func SubDiv(a: int, b: int, c: int) -> int {
|
||||||
|
// AST: (a-b)/c → 3 for (10,4,2); wrong C emit gives 10-4/2=8
|
||||||
|
return (a - b) / c;
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShiftSum(a: int, b: int) -> int {
|
||||||
|
// (a+b)<<1 → 6 for (1,2)
|
||||||
|
return (a + b) << 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Mix(a: int, b: int, c: int, d: int) -> int {
|
||||||
|
// ((a+b)*c)-d → 7 for (1,2,3,2)
|
||||||
|
return (a + b) * c - d;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
let m: int = MulSum(1, 2, 3);
|
||||||
|
let s: int = SubDiv(10, 4, 2);
|
||||||
|
let sh: int = ShiftSum(1, 2);
|
||||||
|
let x: int = Mix(1, 2, 3, 2);
|
||||||
|
PrintLine(String_FromInt(m));
|
||||||
|
PrintLine(String_FromInt(s));
|
||||||
|
PrintLine(String_FromInt(sh));
|
||||||
|
PrintLine(String_FromInt(x));
|
||||||
|
if m != 9 {
|
||||||
|
PrintLine("FAIL MulSum");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if s != 3 {
|
||||||
|
PrintLine("FAIL SubDiv");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if sh != 6 {
|
||||||
|
PrintLine("FAIL ShiftSum");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if x != 7 {
|
||||||
|
PrintLine("FAIL Mix");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
PrintLine("PASS c_precedence");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// Session 66 — gensym hygiene + fragment kinds literal / block
|
||||||
|
import Std::Io::{PrintLine};
|
||||||
|
import Std::String::{String_FromInt};
|
||||||
|
|
||||||
|
// Template locals are gensym'd per expansion — two uses of `n` in one Main
|
||||||
|
// must not collide under the C backend's function-scoped locals.
|
||||||
|
macro! with_acc {
|
||||||
|
( $start:literal ) => {
|
||||||
|
var n: int = $start;
|
||||||
|
n = n + 1;
|
||||||
|
n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// literal: only int/float/string/char/bool literals (not 1+2)
|
||||||
|
macro! only_lit {
|
||||||
|
( $x:literal ) => {
|
||||||
|
{
|
||||||
|
$x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// block: only `{ … }` block expressions
|
||||||
|
macro! wrap_block {
|
||||||
|
( $b:block ) => {
|
||||||
|
$b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// lit alias for literal
|
||||||
|
macro! double_lit {
|
||||||
|
( $n:lit ) => {
|
||||||
|
($n) + ($n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
// gensym: two expansions both introduce `n`
|
||||||
|
let a: int = with_acc!(10);
|
||||||
|
let b: int = with_acc!(20);
|
||||||
|
// 11, 21
|
||||||
|
let c: int = only_lit!(7);
|
||||||
|
let d: int = wrap_block!({ 1 + 2 });
|
||||||
|
let e: int = double_lit!(21);
|
||||||
|
PrintLine(String_FromInt(a));
|
||||||
|
PrintLine(String_FromInt(b));
|
||||||
|
PrintLine(String_FromInt(c));
|
||||||
|
PrintLine(String_FromInt(d));
|
||||||
|
PrintLine(String_FromInt(e));
|
||||||
|
if a != 11 {
|
||||||
|
PrintLine("FAIL with_acc gensym a");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if b != 21 {
|
||||||
|
PrintLine("FAIL with_acc gensym b");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if c != 7 {
|
||||||
|
PrintLine("FAIL only_lit");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if d != 3 {
|
||||||
|
PrintLine("FAIL wrap_block");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if e != 42 {
|
||||||
|
PrintLine("FAIL double_lit");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
PrintLine("PASS macro_hygiene");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
// Session 63 — multi-rep patterns, compound/zipped rep, nested template $(…)*
|
||||||
|
import Std::Io::{PrintLine};
|
||||||
|
import Std::String::{String_FromInt};
|
||||||
|
|
||||||
|
// Compound rep: $( $a:expr , $b:expr ),* → parallel lists, zip in template
|
||||||
|
macro! add_pairs {
|
||||||
|
( $($a:expr, $b:expr),* ) => {
|
||||||
|
var __acc: int = 0;
|
||||||
|
$( __acc = __acc + ($a + $b); )*
|
||||||
|
__acc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-rep groups separated by `;` in the call
|
||||||
|
// sum_n!(1,2; 10,20,30) → sum first group + sum second group
|
||||||
|
macro! sum_groups {
|
||||||
|
( $($x:expr),* ; $($y:expr),* ) => {
|
||||||
|
var __s: int = 0;
|
||||||
|
$( __s = __s + $x; )*
|
||||||
|
$( __s = __s + $y; )*
|
||||||
|
__s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nested template: outer over $x, inner body uses current $x once
|
||||||
|
// (same-list nested MacroRep expands once when bound as single)
|
||||||
|
macro! double_each_sum {
|
||||||
|
( $($x:expr),* ) => {
|
||||||
|
var __t: int = 0;
|
||||||
|
$(
|
||||||
|
$( __t = __t + $x; )*
|
||||||
|
$( __t = __t + $x; )*
|
||||||
|
)*
|
||||||
|
__t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefix fixed + trailing rep still works
|
||||||
|
macro! named_sum {
|
||||||
|
( $label:ident, $($n:expr),* ) => {
|
||||||
|
var __u: int = 0;
|
||||||
|
$( __u = __u + $n; )*
|
||||||
|
__u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
let p: int = add_pairs!(1, 10, 2, 20);
|
||||||
|
// (1+10)+(2+20) = 33
|
||||||
|
let g: int = sum_groups!(1, 2; 10, 20, 30);
|
||||||
|
// 1+2+10+20+30 = 63
|
||||||
|
let d: int = double_each_sum!(3, 4);
|
||||||
|
// (3+3)+(4+4) = 14
|
||||||
|
let n: int = named_sum!(ignored, 5, 6, 7);
|
||||||
|
// 18
|
||||||
|
PrintLine(String_FromInt(p));
|
||||||
|
PrintLine(String_FromInt(g));
|
||||||
|
PrintLine(String_FromInt(d));
|
||||||
|
PrintLine(String_FromInt(n));
|
||||||
|
if p != 33 {
|
||||||
|
PrintLine("FAIL add_pairs");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if g != 63 {
|
||||||
|
PrintLine("FAIL sum_groups");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if d != 14 {
|
||||||
|
PrintLine("FAIL double_each_sum");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if n != 18 {
|
||||||
|
PrintLine("FAIL named_sum");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
PrintLine("PASS macro_nested");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// Macro repetition $(…)* + fragment kinds expr/ident/tt (session 61)
|
||||||
|
import Std::Io::{PrintLine};
|
||||||
|
import Std::String::{String_FromInt};
|
||||||
|
|
||||||
|
// Sum any number of int exprs
|
||||||
|
// Template body is a single block (lets + $(…)* + result expr).
|
||||||
|
macro! sum_n {
|
||||||
|
( $($x:expr),* ) => {
|
||||||
|
var __sum_acc: int = 0;
|
||||||
|
$( __sum_acc = __sum_acc + $x; )*
|
||||||
|
__sum_acc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind an identifier name and call it as a zero-arg func via expr wrap
|
||||||
|
// (ident fragment must be a bare identifier at the call site)
|
||||||
|
macro! call0 {
|
||||||
|
( $f:ident ) => {
|
||||||
|
{
|
||||||
|
$f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tt is accepted like expr (token-tree MVP)
|
||||||
|
macro! id_tt {
|
||||||
|
( $t:tt ) => {
|
||||||
|
{
|
||||||
|
$t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FortyTwo() -> int {
|
||||||
|
return 42;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
let a: int = sum_n!(1, 2, 3);
|
||||||
|
let b: int = sum_n!();
|
||||||
|
let c: int = call0!(FortyTwo);
|
||||||
|
let d: int = id_tt!(7);
|
||||||
|
PrintLine(String_FromInt(a));
|
||||||
|
PrintLine(String_FromInt(b));
|
||||||
|
PrintLine(String_FromInt(c));
|
||||||
|
PrintLine(String_FromInt(d));
|
||||||
|
if a != 6 {
|
||||||
|
PrintLine("FAIL sum_n 1+2+3");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if b != 0 {
|
||||||
|
PrintLine("FAIL sum_n empty");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if c != 42 {
|
||||||
|
PrintLine("FAIL call0 ident");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if d != 7 {
|
||||||
|
PrintLine("FAIL id_tt");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
PrintLine("PASS macro_repeat");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// Declarative macro! + built-in quote! (session 59)
|
||||||
|
// Expands before type-check; $frags splice with call-site hygiene.
|
||||||
|
import Std::Io::{PrintLine};
|
||||||
|
import Std::String::{String_FromInt};
|
||||||
|
|
||||||
|
macro! twice {
|
||||||
|
($x:expr) => {
|
||||||
|
($x) + ($x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro! add2 {
|
||||||
|
($a:expr, $b:expr) => {
|
||||||
|
($a) + ($b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
let a: int = twice!(21);
|
||||||
|
let b: int = add2!(10, 32);
|
||||||
|
// quote! is a built-in identity expand (call-site graft)
|
||||||
|
let c: int = quote!(a + 1);
|
||||||
|
PrintLine(String_FromInt(a));
|
||||||
|
PrintLine(String_FromInt(b));
|
||||||
|
PrintLine(String_FromInt(c));
|
||||||
|
if a != 42 {
|
||||||
|
PrintLine("FAIL twice");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if b != 42 {
|
||||||
|
PrintLine("FAIL add2");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if c != 43 {
|
||||||
|
PrintLine("FAIL quote");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
PrintLine("PASS macro_twice");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// Session 69 — unhygienic binders: `var $name` keeps the call-site identifier
|
||||||
|
// (not gensym'd). Hygienic template locals (`acc`) still get unique names.
|
||||||
|
import Std::Io::{PrintLine};
|
||||||
|
import Std::String::{String_FromInt};
|
||||||
|
|
||||||
|
// Introduce a binder named by the call-site ident; keep that name (unhygienic)
|
||||||
|
macro! let_mut {
|
||||||
|
( $name:ident, $init:literal ) => {
|
||||||
|
var $name: int = $init;
|
||||||
|
$name = $name + 1;
|
||||||
|
$name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hygienic local `acc` must not collide across two expansions
|
||||||
|
macro! double_acc {
|
||||||
|
( $start:literal ) => {
|
||||||
|
var acc: int = $start;
|
||||||
|
acc = acc + acc;
|
||||||
|
acc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mixed: unhygienic $name + hygienic scratch
|
||||||
|
macro! bump_named {
|
||||||
|
( $name:ident ) => {
|
||||||
|
var $name: int = 0;
|
||||||
|
var scratch: int = 1;
|
||||||
|
$name = $name + scratch;
|
||||||
|
$name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
// Unhygienic: binder becomes `counter` inside the expansion block
|
||||||
|
let a: int = let_mut!(counter, 10);
|
||||||
|
// 11
|
||||||
|
let b: int = let_mut!(other, 20);
|
||||||
|
// 21
|
||||||
|
// Hygienic: two expansions with local `acc`
|
||||||
|
let c: int = double_acc!(3);
|
||||||
|
let d: int = double_acc!(5);
|
||||||
|
// 6, 10
|
||||||
|
let e: int = bump_named!(n);
|
||||||
|
// 1
|
||||||
|
PrintLine(String_FromInt(a));
|
||||||
|
PrintLine(String_FromInt(b));
|
||||||
|
PrintLine(String_FromInt(c));
|
||||||
|
PrintLine(String_FromInt(d));
|
||||||
|
PrintLine(String_FromInt(e));
|
||||||
|
if a != 11 {
|
||||||
|
PrintLine("FAIL let_mut counter");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if b != 21 {
|
||||||
|
PrintLine("FAIL let_mut other");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if c != 6 {
|
||||||
|
PrintLine("FAIL double_acc 3");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if d != 10 {
|
||||||
|
PrintLine("FAIL double_acc 5");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if e != 1 {
|
||||||
|
PrintLine("FAIL bump_named");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
PrintLine("PASS macro_unhygienic");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// Session 68 — partial field moves out of @[Drop] parents
|
||||||
|
// `return bag.items` / `let x = bag.items` must skip Bag_Drop (no double-free).
|
||||||
|
// Non-droppable fields (`bag.tag`) do not mark the parent moved.
|
||||||
|
import Std::Io::{PrintLine};
|
||||||
|
import Std::Array::{Array, Array_New, Array_Push, Array_Len, Array_Get};
|
||||||
|
import Std::String::{String_FromInt, String_Concat};
|
||||||
|
import Std::Test::{Test_AssertTrue, Test_Pass};
|
||||||
|
|
||||||
|
@[Drop]
|
||||||
|
struct Bag {
|
||||||
|
items: Array<int>,
|
||||||
|
tag: int
|
||||||
|
}
|
||||||
|
|
||||||
|
func Bag_Drop(self: *Bag) {
|
||||||
|
Array_Drop<int>(&self.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move droppable field out via return
|
||||||
|
func TakeItems() -> Array<int> {
|
||||||
|
var items: Array<int> = Array_New<int>(4);
|
||||||
|
Array_Push<int>(&items, 42);
|
||||||
|
let bag: Bag = Bag { items: items, tag: 7 };
|
||||||
|
return bag.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move droppable field via let; read non-droppable tag after
|
||||||
|
func PeekTagAndTake() -> int {
|
||||||
|
var items: Array<int> = Array_New<int>(2);
|
||||||
|
Array_Push<int>(&items, 1);
|
||||||
|
let bag: Bag = Bag { items: items, tag: 99 };
|
||||||
|
let moved: Array<int> = bag.items;
|
||||||
|
let t: int = bag.tag;
|
||||||
|
discard Array_Len<int>(&moved);
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whole-struct return still moves bag (existing path)
|
||||||
|
func MakeBag() -> Bag {
|
||||||
|
var items: Array<int> = Array_New<int>(2);
|
||||||
|
Array_Push<int>(&items, 10);
|
||||||
|
Array_Push<int>(&items, 20);
|
||||||
|
let bag: Bag = Bag { items: items, tag: 3 };
|
||||||
|
return bag;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
let taken: Array<int> = TakeItems();
|
||||||
|
Test_AssertTrue(Array_Len<int>(&taken) == 1);
|
||||||
|
Test_AssertTrue(Array_Get<int>(&taken, 0) == 42);
|
||||||
|
|
||||||
|
let tag: int = PeekTagAndTake();
|
||||||
|
Test_AssertTrue(tag == 99);
|
||||||
|
|
||||||
|
let b: Bag = MakeBag();
|
||||||
|
Test_AssertTrue(Array_Len<int>(&b.items) == 2);
|
||||||
|
Test_AssertTrue(Array_Get<int>(&b.items, 0) == 10);
|
||||||
|
Test_AssertTrue(b.tag == 3);
|
||||||
|
|
||||||
|
PrintLine(String_Concat("ok=", String_FromInt(tag as int64)));
|
||||||
|
Test_Pass("move_field_partial");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// C.4 — @[Release] zero-cost path vs @[Checked]
|
||||||
|
// Default / Release: free; Checked: catches double-mut and dangling returns.
|
||||||
|
import Std::Io::{PrintLine, PrintInt};
|
||||||
|
import Std::Test::{Test_AssertEqInt, Test_Pass};
|
||||||
|
|
||||||
|
// Tier 1 — unchecked (default)
|
||||||
|
func UncheckedInc(p: *int) {
|
||||||
|
*p = *p + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 2 — borrow checked API surface
|
||||||
|
@[Checked]
|
||||||
|
func SafeInc(p: &mut int) {
|
||||||
|
*p = *p + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@[Checked]
|
||||||
|
func SafeGet(p: &int) -> int {
|
||||||
|
return *p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 3 — explicit zero-cost hot path (Release wins over Checked)
|
||||||
|
@[Checked]
|
||||||
|
@[Release]
|
||||||
|
func HotInc(p: &mut int) {
|
||||||
|
// Would be fine either way; attribute documents "no checker cost here"
|
||||||
|
*p = *p + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@[Release]
|
||||||
|
func HotDangle() -> &int {
|
||||||
|
// Allowed: Release disables dangling-return checks
|
||||||
|
var x: int = 99;
|
||||||
|
return &x;
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
var n: int = 10;
|
||||||
|
UncheckedInc(&n);
|
||||||
|
Test_AssertEqInt(n, 11);
|
||||||
|
|
||||||
|
SafeInc(&n);
|
||||||
|
Test_AssertEqInt(n, 12);
|
||||||
|
|
||||||
|
HotInc(&n);
|
||||||
|
Test_AssertEqInt(n, 13);
|
||||||
|
|
||||||
|
let v: int = SafeGet(&n);
|
||||||
|
Test_AssertEqInt(v, 13);
|
||||||
|
|
||||||
|
// HotDangle is only used to prove Release compiles; do not dereference
|
||||||
|
discard HotDangle();
|
||||||
|
|
||||||
|
PrintInt(n);
|
||||||
|
PrintLine("");
|
||||||
|
Test_Pass("ownership_release");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -1478,6 +1478,15 @@ const char* bux_getenv(const char* name) {
|
|||||||
return val ? val : "";
|
return val ? val : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Extra cc/ld flags for host. --build-id is GNU ld only (Apple ld rejects it). */
|
||||||
|
const char* bux_cc_ld_stable(void) {
|
||||||
|
#if defined(__linux__)
|
||||||
|
return " -Wl,--build-id=none";
|
||||||
|
#else
|
||||||
|
return "";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
int bux_setenv(const char* name, const char* value) {
|
int bux_setenv(const char* name, const char* value) {
|
||||||
if (!name || !value) return -1;
|
if (!name || !value) return -1;
|
||||||
return setenv(name, value, 1);
|
return setenv(name, value, 1);
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ module Ast {
|
|||||||
const ekAwait: int = 25;
|
const ekAwait: int = 25;
|
||||||
const ekStringInterp: int = 26;
|
const ekStringInterp: int = 26;
|
||||||
const ekClosure: int = 27;
|
const ekClosure: int = 27;
|
||||||
|
const ekMacroCall: int = 28; // name!(args) — expanded before sema
|
||||||
|
|
||||||
struct ExprList {
|
struct ExprList {
|
||||||
expr: *Expr,
|
expr: *Expr,
|
||||||
@@ -218,6 +219,7 @@ module Ast {
|
|||||||
const skDecl: int = 11;
|
const skDecl: int = 11;
|
||||||
const skDefer: int = 12;
|
const skDefer: int = 12;
|
||||||
const skSwitch: int = 13;
|
const skSwitch: int = 13;
|
||||||
|
const skMacroRep: int = 14; // $( stmts… )* in macro templates
|
||||||
|
|
||||||
struct ElseIf {
|
struct ElseIf {
|
||||||
line: uint32;
|
line: uint32;
|
||||||
@@ -265,6 +267,7 @@ module Ast {
|
|||||||
const dkTypeAlias: int = 9;
|
const dkTypeAlias: int = 9;
|
||||||
const dkExternFunc: int = 10;
|
const dkExternFunc: int = 10;
|
||||||
const dkExternVar: int = 11;
|
const dkExternVar: int = 11;
|
||||||
|
const dkMacro: int = 12; // macro! name { rules }; rules in childDecl1
|
||||||
|
|
||||||
struct Param {
|
struct Param {
|
||||||
line: uint32;
|
line: uint32;
|
||||||
|
|||||||
+62
-10
@@ -113,25 +113,64 @@ module CBackend {
|
|||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mark droppable locals moved by-value (struct fields / nested).
|
/// True when a C type name owns heap / has auto-Drop (not primitives).
|
||||||
|
func CBE_IsDroppableTypeName(tn: String) -> bool {
|
||||||
|
if tn == null as String || String_Eq(tn, "") { return false; }
|
||||||
|
if String_Eq(tn, "void") || String_Eq(tn, "int") || String_Eq(tn, "bool") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if String_Eq(tn, "int64") || String_Eq(tn, "uint") || String_Eq(tn, "uint64") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if String_Eq(tn, "float") || String_Eq(tn, "float64") || String_Eq(tn, "char8") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if String_Eq(tn, "String") || String_Eq(tn, "cstr") { return false; }
|
||||||
|
// Array_int, Map_*, user @[Drop] structs (Bag, Token, …)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark droppable locals moved by-value (struct fields / nested / partial field).
|
||||||
|
/// `valueTypeHint`: when non-empty (e.g. function return type), used to decide
|
||||||
|
/// whether a field access is an ownership move (`return bag.items` vs `return bag.tag`).
|
||||||
func CBE_MarkMovedFromNode(cbe: *CEmitter, node: *HirNode) {
|
func CBE_MarkMovedFromNode(cbe: *CEmitter, node: *HirNode) {
|
||||||
|
CBE_MarkMovedFromNodeHint(cbe, node, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
func CBE_MarkMovedFromNodeHint(cbe: *CEmitter, node: *HirNode, valueTypeHint: String) {
|
||||||
if node == null as *HirNode { return; }
|
if node == null as *HirNode { return; }
|
||||||
if node.kind == hVar {
|
if node.kind == hVar {
|
||||||
CBE_AddMoved(cbe, node.strValue);
|
CBE_AddMoved(cbe, node.strValue);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Partial field move: only when the *value* type is droppable
|
||||||
|
if node.kind == hFieldPtr || node.kind == hFieldAccess {
|
||||||
|
var vty: String = valueTypeHint;
|
||||||
|
if String_Eq(vty, "") {
|
||||||
|
vty = node.typeName;
|
||||||
|
}
|
||||||
|
// Field HIR often stores the *base* struct typeName — prefer hint
|
||||||
|
if CBE_IsDroppableTypeName(vty) {
|
||||||
|
// Walk to base local (hVar / load / nested)
|
||||||
|
CBE_MarkMovedFromNodeHint(cbe, node.child1, "");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if node.kind == hLoad {
|
||||||
|
CBE_MarkMovedFromNodeHint(cbe, node.child1, valueTypeHint);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if node.kind == hStructInit {
|
if node.kind == hStructInit {
|
||||||
var field: *HirNode = node.child1;
|
var field: *HirNode = node.child1;
|
||||||
while field != null as *HirNode {
|
while field != null as *HirNode {
|
||||||
CBE_MarkMovedFromNode(cbe, field.child1);
|
CBE_MarkMovedFromNodeHint(cbe, field.child1, "");
|
||||||
field = field.child3;
|
field = field.child3;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if node.kind == hTupleInit {
|
if node.kind == hTupleInit {
|
||||||
// child1/child2 + linked extras if any
|
CBE_MarkMovedFromNodeHint(cbe, node.child1, "");
|
||||||
CBE_MarkMovedFromNode(cbe, node.child1);
|
CBE_MarkMovedFromNodeHint(cbe, node.child2, "");
|
||||||
CBE_MarkMovedFromNode(cbe, node.child2);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -375,13 +414,16 @@ module CBackend {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Binary
|
// Binary — always parenthesize so C precedence cannot rewrite the AST.
|
||||||
|
// Without parens, Mul(Add(a,b), c) emits `a + b * c` (= a+(b*c)) instead of (a+b)*c.
|
||||||
if kind == hBinary {
|
if kind == hBinary {
|
||||||
|
StringBuilder_Append(&cbe.sb, "(");
|
||||||
CBE_EmitExpr(cbe, node.child1);
|
CBE_EmitExpr(cbe, node.child1);
|
||||||
StringBuilder_Append(&cbe.sb, " ");
|
StringBuilder_Append(&cbe.sb, " ");
|
||||||
StringBuilder_Append(&cbe.sb, CBackend_OpToC(node.intValue));
|
StringBuilder_Append(&cbe.sb, CBackend_OpToC(node.intValue));
|
||||||
StringBuilder_Append(&cbe.sb, " ");
|
StringBuilder_Append(&cbe.sb, " ");
|
||||||
CBE_EmitExpr(cbe, node.child2);
|
CBE_EmitExpr(cbe, node.child2);
|
||||||
|
StringBuilder_Append(&cbe.sb, ")");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,9 +536,15 @@ module CBackend {
|
|||||||
// (Emitting Drop before the value used to use-after-drop on `return a.id`.)
|
// (Emitting Drop before the value used to use-after-drop on `return a.id`.)
|
||||||
if kind == hReturn {
|
if kind == hReturn {
|
||||||
CBE_EmitDebugLine(cbe, node);
|
CBE_EmitDebugLine(cbe, node);
|
||||||
// Track moved variables via return / field-move into returned struct
|
// Track moved variables via return / field-move into returned struct.
|
||||||
|
// Pass currentRetType so `return bag.items` (Array) marks bag, but
|
||||||
|
// `return bag.tag` (int) does not.
|
||||||
if node.child1 != null as *HirNode {
|
if node.child1 != null as *HirNode {
|
||||||
CBE_MarkMovedFromNode(cbe, node.child1);
|
var retHint: String = "";
|
||||||
|
if cbe.currentRetType != null as String {
|
||||||
|
retHint = cbe.currentRetType;
|
||||||
|
}
|
||||||
|
CBE_MarkMovedFromNodeHint(cbe, node.child1, retHint);
|
||||||
}
|
}
|
||||||
if node.child1 != null as *HirNode && cbe.deferCount > 0 {
|
if node.child1 != null as *HirNode && cbe.deferCount > 0 {
|
||||||
// Materialize into a temp so Drop cannot clobber the returned value.
|
// Materialize into a temp so Drop cannot clobber the returned value.
|
||||||
@@ -554,9 +602,13 @@ module CBackend {
|
|||||||
|
|
||||||
// Store: combine alloca + value into single declaration
|
// Store: combine alloca + value into single declaration
|
||||||
if kind == hStore {
|
if kind == hStore {
|
||||||
// Track moved variables via assignment/let
|
// Track moved variables via assignment/let (incl. partial field rhs)
|
||||||
if node.child2 != null as *HirNode && node.child2.kind == hVar {
|
if node.child2 != null as *HirNode {
|
||||||
|
if node.child2.kind == hVar {
|
||||||
CBE_AddMoved(cbe, node.child2.strValue);
|
CBE_AddMoved(cbe, node.child2.strValue);
|
||||||
|
} else {
|
||||||
|
CBE_MarkMovedFromNode(cbe, node.child2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Reinitialization removes moved status
|
// Reinitialization removes moved status
|
||||||
if node.child1 != null as *HirNode && node.child1.kind == hVar {
|
if node.child1 != null as *HirNode && node.child1.kind == hVar {
|
||||||
|
|||||||
+77
-4
@@ -17,6 +17,7 @@ module Cli {
|
|||||||
extern func bux_system(cmd: String) -> int;
|
extern func bux_system(cmd: String) -> int;
|
||||||
extern func bux_getenv(name: String) -> String;
|
extern func bux_getenv(name: String) -> String;
|
||||||
extern func bux_setenv(name: String, value: String) -> int;
|
extern func bux_setenv(name: String, value: String) -> int;
|
||||||
|
extern func bux_cc_ld_stable() -> String;
|
||||||
extern func bux_strlen(s: String) -> uint;
|
extern func bux_strlen(s: String) -> uint;
|
||||||
extern func bux_str_slice(s: String, start: uint, len: uint) -> String;
|
extern func bux_str_slice(s: String, start: uint, len: uint) -> String;
|
||||||
|
|
||||||
@@ -226,6 +227,25 @@ func Cli_Compile(source: String, sourceName: String, targetTriple: String) -> St
|
|||||||
decl = decl.childDecl2;
|
decl = decl.childDecl2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 2b: declarative macro! / quote! expansion
|
||||||
|
PrintLine(" Macro expand...");
|
||||||
|
let macEx: *MacroExpander = MacroExpand_ExpandModule(mod);
|
||||||
|
if MacroExpand_DiagCount(macEx) > 0 {
|
||||||
|
var mi: int = 0;
|
||||||
|
while mi < MacroExpand_DiagCount(macEx) {
|
||||||
|
let md: MacroDiag = MacroExpand_GetDiag(macEx, mi);
|
||||||
|
let diag: Diagnostic = Diagnostic {
|
||||||
|
message: md.message,
|
||||||
|
line: md.line,
|
||||||
|
column: md.column,
|
||||||
|
severity: 0,
|
||||||
|
};
|
||||||
|
Diagnostic_Print(&diag, sourceName);
|
||||||
|
mi = mi + 1;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 3: Semantic analysis
|
// Phase 3: Semantic analysis
|
||||||
PrintLine(" Sema...");
|
PrintLine(" Sema...");
|
||||||
let sema: *Sema = Sema_Analyze(mod);
|
let sema: *Sema = Sema_Analyze(mod);
|
||||||
@@ -340,13 +360,17 @@ func Cli_Build(srcPath: String, outPath: String, targetTriple: String, isRelease
|
|||||||
if !String_Eq(targetTriple, "") {
|
if !String_Eq(targetTriple, "") {
|
||||||
StringBuilder_Append(&cmdBuf, "clang ");
|
StringBuilder_Append(&cmdBuf, "clang ");
|
||||||
StringBuilder_Append(&cmdBuf, optFlags);
|
StringBuilder_Append(&cmdBuf, optFlags);
|
||||||
StringBuilder_Append(&cmdBuf, " -pthread -Wl,--build-id=none -target ");
|
StringBuilder_Append(&cmdBuf, " -pthread");
|
||||||
|
StringBuilder_Append(&cmdBuf, bux_cc_ld_stable());
|
||||||
|
StringBuilder_Append(&cmdBuf, " -target ");
|
||||||
StringBuilder_Append(&cmdBuf, targetTriple);
|
StringBuilder_Append(&cmdBuf, targetTriple);
|
||||||
StringBuilder_Append(&cmdBuf, " ");
|
StringBuilder_Append(&cmdBuf, " ");
|
||||||
} else {
|
} else {
|
||||||
StringBuilder_Append(&cmdBuf, "cc ");
|
StringBuilder_Append(&cmdBuf, "cc ");
|
||||||
StringBuilder_Append(&cmdBuf, optFlags);
|
StringBuilder_Append(&cmdBuf, optFlags);
|
||||||
StringBuilder_Append(&cmdBuf, " -pthread -Wl,--build-id=none ");
|
StringBuilder_Append(&cmdBuf, " -pthread");
|
||||||
|
StringBuilder_Append(&cmdBuf, bux_cc_ld_stable());
|
||||||
|
StringBuilder_Append(&cmdBuf, " ");
|
||||||
}
|
}
|
||||||
StringBuilder_Append(&cmdBuf, "-o ");
|
StringBuilder_Append(&cmdBuf, "-o ");
|
||||||
StringBuilder_Append(&cmdBuf, outPath);
|
StringBuilder_Append(&cmdBuf, outPath);
|
||||||
@@ -424,6 +448,24 @@ func Cli_Check(srcPath: String) -> int {
|
|||||||
decl2 = decl2.childDecl2;
|
decl2 = decl2.childDecl2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 2b: macro expand
|
||||||
|
let macEx2: *MacroExpander = MacroExpand_ExpandModule(mod);
|
||||||
|
if MacroExpand_DiagCount(macEx2) > 0 {
|
||||||
|
var mi2: int = 0;
|
||||||
|
while mi2 < MacroExpand_DiagCount(macEx2) {
|
||||||
|
let md2: MacroDiag = MacroExpand_GetDiag(macEx2, mi2);
|
||||||
|
let diag: Diagnostic = Diagnostic {
|
||||||
|
message: md2.message,
|
||||||
|
line: md2.line,
|
||||||
|
column: md2.column,
|
||||||
|
severity: 0,
|
||||||
|
};
|
||||||
|
Diagnostic_Print(&diag, srcPath);
|
||||||
|
mi2 = mi2 + 1;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 3: Sema
|
// Phase 3: Sema
|
||||||
let sema: *Sema = Sema_Analyze(mod);
|
let sema: *Sema = Sema_Analyze(mod);
|
||||||
if Sema_HasError(sema) {
|
if Sema_HasError(sema) {
|
||||||
@@ -479,6 +521,14 @@ func Cli_CompileSource(source: String, sourceName: String) -> *HirModule {
|
|||||||
return null as *HirModule;
|
return null as *HirModule;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 2b: macro expand
|
||||||
|
let macEx3: *MacroExpander = MacroExpand_ExpandModule(mod);
|
||||||
|
if MacroExpand_DiagCount(macEx3) > 0 {
|
||||||
|
Print("Macro errors in ");
|
||||||
|
PrintLine(sourceName);
|
||||||
|
return null as *HirModule;
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 3: Semantic analysis
|
// Phase 3: Semantic analysis
|
||||||
let sema: *Sema = Sema_Analyze(mod);
|
let sema: *Sema = Sema_Analyze(mod);
|
||||||
if Sema_HasError(sema) {
|
if Sema_HasError(sema) {
|
||||||
@@ -1623,6 +1673,25 @@ func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool)
|
|||||||
PrintInt(merged.itemCount);
|
PrintInt(merged.itemCount);
|
||||||
PrintLine(" declarations");
|
PrintLine(" declarations");
|
||||||
|
|
||||||
|
// Declarative macro! / quote! expansion (before type-check)
|
||||||
|
PrintLine("Expanding macros...");
|
||||||
|
let macEx: *MacroExpander = MacroExpand_ExpandModule(merged);
|
||||||
|
if MacroExpand_DiagCount(macEx) > 0 {
|
||||||
|
var mi: int = 0;
|
||||||
|
while mi < MacroExpand_DiagCount(macEx) {
|
||||||
|
let md: MacroDiag = MacroExpand_GetDiag(macEx, mi);
|
||||||
|
let diag: Diagnostic = Diagnostic {
|
||||||
|
message: md.message,
|
||||||
|
line: md.line,
|
||||||
|
column: md.column,
|
||||||
|
severity: 0,
|
||||||
|
};
|
||||||
|
Diagnostic_Print(&diag, "<macro>");
|
||||||
|
mi = mi + 1;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Semantic analysis
|
// Semantic analysis
|
||||||
PrintLine("Running sema...");
|
PrintLine("Running sema...");
|
||||||
let sema: *Sema = Sema_Analyze(merged);
|
let sema: *Sema = Sema_Analyze(merged);
|
||||||
@@ -1705,13 +1774,17 @@ func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool)
|
|||||||
if !String_Eq(targetTriple, "") {
|
if !String_Eq(targetTriple, "") {
|
||||||
StringBuilder_Append(&ccBuf, "clang ");
|
StringBuilder_Append(&ccBuf, "clang ");
|
||||||
StringBuilder_Append(&ccBuf, optFlags2);
|
StringBuilder_Append(&ccBuf, optFlags2);
|
||||||
StringBuilder_Append(&ccBuf, " -pthread -Wl,--build-id=none -target ");
|
StringBuilder_Append(&ccBuf, " -pthread");
|
||||||
|
StringBuilder_Append(&ccBuf, bux_cc_ld_stable());
|
||||||
|
StringBuilder_Append(&ccBuf, " -target ");
|
||||||
StringBuilder_Append(&ccBuf, targetTriple);
|
StringBuilder_Append(&ccBuf, targetTriple);
|
||||||
StringBuilder_Append(&ccBuf, " ");
|
StringBuilder_Append(&ccBuf, " ");
|
||||||
} else {
|
} else {
|
||||||
StringBuilder_Append(&ccBuf, "cc ");
|
StringBuilder_Append(&ccBuf, "cc ");
|
||||||
StringBuilder_Append(&ccBuf, optFlags2);
|
StringBuilder_Append(&ccBuf, optFlags2);
|
||||||
StringBuilder_Append(&ccBuf, " -pthread -Wl,--build-id=none ");
|
StringBuilder_Append(&ccBuf, " -pthread");
|
||||||
|
StringBuilder_Append(&ccBuf, bux_cc_ld_stable());
|
||||||
|
StringBuilder_Append(&ccBuf, " ");
|
||||||
}
|
}
|
||||||
StringBuilder_Append(&ccBuf, "-o ");
|
StringBuilder_Append(&ccBuf, "-o ");
|
||||||
StringBuilder_Append(&ccBuf, outBin);
|
StringBuilder_Append(&ccBuf, outBin);
|
||||||
|
|||||||
@@ -282,6 +282,7 @@ module Lexer {
|
|||||||
if String_Eq(text, "async") { return tkAsync; }
|
if String_Eq(text, "async") { return tkAsync; }
|
||||||
if String_Eq(text, "await") { return tkAwait; }
|
if String_Eq(text, "await") { return tkAwait; }
|
||||||
if String_Eq(text, "spawn") { return tkSpawn; }
|
if String_Eq(text, "spawn") { return tkSpawn; }
|
||||||
|
if String_Eq(text, "macro") { return tkMacro; }
|
||||||
return tkIdent;
|
return tkIdent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,6 +667,19 @@ module Lexer {
|
|||||||
lexEmitToken(lex, tkHash); return;
|
lexEmitToken(lex, tkHash); return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Macro fragment $name, or bare $ for $(…)*
|
||||||
|
if c == 36 { // '$'
|
||||||
|
if Lex_IsIdentStart(lexPeek(lex, 0)) {
|
||||||
|
while !lexIsAtEnd(lex) && Lex_IsIdentChar(lexPeek(lex, 0)) {
|
||||||
|
discard lexAdvance(lex);
|
||||||
|
}
|
||||||
|
lexEmitToken(lex, tkIdent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lexEmitToken(lex, tkDollar);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
lexEmitDiag(lex, "unexpected character");
|
lexEmitDiag(lex, "unexpected character");
|
||||||
lexEmitToken(lex, tkUnknown);
|
lexEmitToken(lex, tkUnknown);
|
||||||
}
|
}
|
||||||
|
|||||||
+1105
File diff suppressed because it is too large
Load Diff
+292
-5
@@ -27,6 +27,7 @@ module Parser {
|
|||||||
diagCount: int,
|
diagCount: int,
|
||||||
diags: *ParserDiag,
|
diags: *ParserDiag,
|
||||||
structInitAllowed: bool,
|
structInitAllowed: bool,
|
||||||
|
macroTemplateMode: bool, // allows $(…)* in macro! bodies
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ParserDiag {
|
struct ParserDiag {
|
||||||
@@ -1252,11 +1253,81 @@ module Parser {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ! (unwrap operator)
|
// ! — macro call name!(args) or unwrap
|
||||||
if kind == tkBang {
|
if kind == tkBang {
|
||||||
discard parserAdvance(p);
|
discard parserAdvance(p);
|
||||||
let line: uint32 = parserCurToken(p).line;
|
let line: uint32 = parserCurToken(p).line;
|
||||||
let col: uint32 = parserCurToken(p).column;
|
let col: uint32 = parserCurToken(p).column;
|
||||||
|
if left.kind == ekIdent && parserCheck(p, tkLParen) {
|
||||||
|
discard parserAdvance(p); // (
|
||||||
|
let e: *Expr = parserMakeExpr(ekMacroCall, line, col);
|
||||||
|
e.strValue = left.strValue;
|
||||||
|
var argCount: int = 0;
|
||||||
|
var firstArg: *ExprList = null as *ExprList;
|
||||||
|
var lastArg: *ExprList = null as *ExprList;
|
||||||
|
// Multi-rep groups: m!(a,b; c,d) — lengths encoded in genericCallee
|
||||||
|
var groupLens: String = "";
|
||||||
|
var curGroup: int = 0;
|
||||||
|
var nGroups: int = 0;
|
||||||
|
while !parserCheck(p, tkRParen) && parserPeek(p, 0) != tkEndOfFile {
|
||||||
|
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
|
||||||
|
if parserCheck(p, tkRParen) { break; }
|
||||||
|
// `;` starts a new arg group
|
||||||
|
if parserMatch(p, tkSemicolon) {
|
||||||
|
if String_Eq(groupLens, "") {
|
||||||
|
groupLens = String_FromInt(curGroup as int64);
|
||||||
|
} else {
|
||||||
|
groupLens = String_Concat(groupLens, String_Concat(";", String_FromInt(curGroup as int64)));
|
||||||
|
}
|
||||||
|
nGroups = nGroups + 1;
|
||||||
|
curGroup = 0;
|
||||||
|
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let argExpr: *Expr = parserParseExpr(p);
|
||||||
|
let argNode: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
|
||||||
|
argNode.expr = argExpr;
|
||||||
|
argNode.next = null as *ExprList;
|
||||||
|
argNode.argName = "";
|
||||||
|
if firstArg == null as *ExprList {
|
||||||
|
firstArg = argNode;
|
||||||
|
lastArg = argNode;
|
||||||
|
} else {
|
||||||
|
lastArg.next = argNode;
|
||||||
|
lastArg = argNode;
|
||||||
|
}
|
||||||
|
argCount = argCount + 1;
|
||||||
|
curGroup = curGroup + 1;
|
||||||
|
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
|
||||||
|
if parserMatch(p, tkComma) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// allow `;` at loop top; otherwise end of args
|
||||||
|
if !parserCheck(p, tkSemicolon) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// finalize last group
|
||||||
|
if curGroup > 0 || nGroups == 0 {
|
||||||
|
if String_Eq(groupLens, "") {
|
||||||
|
groupLens = String_FromInt(curGroup as int64);
|
||||||
|
} else {
|
||||||
|
groupLens = String_Concat(groupLens, String_Concat(";", String_FromInt(curGroup as int64)));
|
||||||
|
}
|
||||||
|
nGroups = nGroups + 1;
|
||||||
|
}
|
||||||
|
// single group → empty genericCallee (flat match)
|
||||||
|
if nGroups <= 1 {
|
||||||
|
e.genericCallee = "";
|
||||||
|
} else {
|
||||||
|
e.genericCallee = groupLens;
|
||||||
|
}
|
||||||
|
e.callArgs = firstArg;
|
||||||
|
e.callArgCount = argCount;
|
||||||
|
discard parserExpect(p, tkRParen, "expected ')' to close macro arguments");
|
||||||
|
left = e;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let e: *Expr = parserMakeExpr(ekUnwrap, line, col);
|
let e: *Expr = parserMakeExpr(ekUnwrap, line, col);
|
||||||
e.child1 = left;
|
e.child1 = left;
|
||||||
left = e;
|
left = e;
|
||||||
@@ -1466,11 +1537,50 @@ module Parser {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func parserParseStmt(p: *Parser) -> *Stmt {
|
func parserParseStmt(p: *Parser) -> *Stmt {
|
||||||
|
while parserCheck(p, tkNewLine) {
|
||||||
|
discard parserAdvance(p);
|
||||||
|
}
|
||||||
let tok: LexToken = parserCurToken(p);
|
let tok: LexToken = parserCurToken(p);
|
||||||
let line: uint32 = tok.line;
|
let line: uint32 = tok.line;
|
||||||
let col: uint32 = tok.column;
|
let col: uint32 = tok.column;
|
||||||
let kind: int = tok.kind;
|
let kind: int = tok.kind;
|
||||||
|
|
||||||
|
// Macro template: $( stmts… )*
|
||||||
|
if p.macroTemplateMode && kind == tkDollar && parserPeek(p, 1) == tkLParen {
|
||||||
|
discard parserAdvance(p); // $
|
||||||
|
discard parserAdvance(p); // (
|
||||||
|
let body: *Block = bux_alloc(sizeof(Block)) as *Block;
|
||||||
|
body.line = line;
|
||||||
|
body.column = col;
|
||||||
|
body.sourceFile = "";
|
||||||
|
body.stmtCount = 0;
|
||||||
|
body.firstStmt = null as *Stmt;
|
||||||
|
body.lastStmt = null as *Stmt;
|
||||||
|
while !parserCheck(p, tkRParen) && parserPeek(p, 0) != tkEndOfFile {
|
||||||
|
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
|
||||||
|
if parserCheck(p, tkRParen) { break; }
|
||||||
|
let inner: *Stmt = parserParseStmt(p);
|
||||||
|
if body.firstStmt == null as *Stmt {
|
||||||
|
body.firstStmt = inner;
|
||||||
|
body.lastStmt = inner;
|
||||||
|
} else {
|
||||||
|
body.lastStmt.nextStmt = inner;
|
||||||
|
body.lastStmt = inner;
|
||||||
|
}
|
||||||
|
body.stmtCount = body.stmtCount + 1;
|
||||||
|
}
|
||||||
|
discard parserExpect(p, tkRParen, "expected ')' to close macro repetition");
|
||||||
|
discard parserExpect(p, tkStar, "expected '*' after macro repetition");
|
||||||
|
parserMatch(p, tkSemicolon);
|
||||||
|
let s: *Stmt = bux_alloc(sizeof(Stmt)) as *Stmt;
|
||||||
|
s.kind = skMacroRep;
|
||||||
|
s.line = line;
|
||||||
|
s.column = col;
|
||||||
|
s.refStmtBlock = body;
|
||||||
|
s.nextStmt = null as *Stmt;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
// let / var
|
// let / var
|
||||||
if kind == tkLet || kind == tkVar {
|
if kind == tkLet || kind == tkVar {
|
||||||
let isVar: bool = (kind == tkVar);
|
let isVar: bool = (kind == tkVar);
|
||||||
@@ -2197,6 +2307,178 @@ module Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
// macro! name { ($x:expr, …) => { template } … }
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func parserSetMacroFragName(d: *Decl, idx: int, name: String) {
|
||||||
|
if idx == 0 { d.param0.name = name; }
|
||||||
|
else if idx == 1 { d.param1.name = name; }
|
||||||
|
else if idx == 2 { d.param2.name = name; }
|
||||||
|
else if idx == 3 { d.param3.name = name; }
|
||||||
|
else if idx == 4 { d.param4.name = name; }
|
||||||
|
else if idx == 5 { d.param5.name = name; }
|
||||||
|
else if idx == 6 { d.param6.name = name; }
|
||||||
|
else if idx == 7 { d.param7.name = name; }
|
||||||
|
else if idx == 8 { d.param8.name = name; }
|
||||||
|
}
|
||||||
|
|
||||||
|
func parserParseMacroDecl(p: *Parser, isPublic: bool) -> *Decl {
|
||||||
|
let line: uint32 = parserCurToken(p).line;
|
||||||
|
let col: uint32 = parserCurToken(p).column;
|
||||||
|
discard parserExpect(p, tkMacro, "expected 'macro'");
|
||||||
|
discard parserExpect(p, tkBang, "expected '!' after macro");
|
||||||
|
let nameTok: LexToken = parserExpect(p, tkIdent, "expected macro name");
|
||||||
|
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
|
||||||
|
discard parserExpect(p, tkLBrace, "expected '{' to start macro body");
|
||||||
|
|
||||||
|
let d: *Decl = bux_alloc(sizeof(Decl)) as *Decl;
|
||||||
|
d.kind = dkMacro;
|
||||||
|
d.line = line;
|
||||||
|
d.column = col;
|
||||||
|
d.isPublic = isPublic;
|
||||||
|
d.strValue = nameTok.text;
|
||||||
|
d.childDecl1 = null as *Decl;
|
||||||
|
d.childDecl2 = null as *Decl;
|
||||||
|
|
||||||
|
var firstRule: *Decl = null as *Decl;
|
||||||
|
var lastRule: *Decl = null as *Decl;
|
||||||
|
var ruleCount: int = 0;
|
||||||
|
|
||||||
|
while !parserCheck(p, tkRBrace) && parserPeek(p, 0) != tkEndOfFile {
|
||||||
|
while parserCheck(p, tkNewLine) || parserCheck(p, tkSemicolon) {
|
||||||
|
discard parserAdvance(p);
|
||||||
|
}
|
||||||
|
if parserCheck(p, tkRBrace) { break; }
|
||||||
|
|
||||||
|
let rline: uint32 = parserCurToken(p).line;
|
||||||
|
let rcol: uint32 = parserCurToken(p).column;
|
||||||
|
discard parserExpect(p, tkLParen, "expected '(' to start macro pattern");
|
||||||
|
|
||||||
|
let rule: *Decl = bux_alloc(sizeof(Decl)) as *Decl;
|
||||||
|
rule.kind = dkMacro;
|
||||||
|
rule.line = rline;
|
||||||
|
rule.column = rcol;
|
||||||
|
rule.strValue = "";
|
||||||
|
rule.paramCount = 0;
|
||||||
|
rule.childDecl2 = null as *Decl;
|
||||||
|
|
||||||
|
// useNames encodes: "expr" | "ident" | "tt" | "rep:expr," | "rep:expr+expr," (compound)
|
||||||
|
// Multiple pattern elements joined by `;` → multi-rep groups at call site
|
||||||
|
var kindsEnc: String = "";
|
||||||
|
while !parserCheck(p, tkRParen) && parserPeek(p, 0) != tkEndOfFile {
|
||||||
|
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
|
||||||
|
if parserCheck(p, tkRParen) { break; }
|
||||||
|
if parserMatch(p, tkSemicolon) { continue; }
|
||||||
|
// $( $a:kind , $b:kind ),*
|
||||||
|
if parserCheck(p, tkDollar) && parserPeek(p, 1) == tkLParen {
|
||||||
|
discard parserAdvance(p); // $
|
||||||
|
discard parserAdvance(p); // (
|
||||||
|
var repNames: String = "";
|
||||||
|
var repKinds: String = "";
|
||||||
|
var nIn: int = 0;
|
||||||
|
while !parserCheck(p, tkRParen) && parserPeek(p, 0) != tkEndOfFile {
|
||||||
|
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
|
||||||
|
if parserCheck(p, tkRParen) { break; }
|
||||||
|
let fragTok: LexToken = parserExpect(p, tkIdent, "expected $name fragment");
|
||||||
|
if !String_StartsWith(fragTok.text, "$") {
|
||||||
|
parserEmitDiag(p, fragTok.line, fragTok.column, "macro fragment must start with '$'");
|
||||||
|
}
|
||||||
|
discard parserExpect(p, tkColon, "expected ':' after fragment name");
|
||||||
|
let kindTok: LexToken = parserExpect(p, tkIdent, "expected fragment kind");
|
||||||
|
var kname: String = kindTok.text;
|
||||||
|
if String_Eq(kname, "lit") { kname = "literal"; }
|
||||||
|
if !(String_Eq(kname, "expr") || String_Eq(kname, "ident") || String_Eq(kname, "tt")
|
||||||
|
|| String_Eq(kname, "literal") || String_Eq(kname, "block")) {
|
||||||
|
kname = "expr";
|
||||||
|
}
|
||||||
|
if nIn == 0 {
|
||||||
|
repNames = fragTok.text;
|
||||||
|
repKinds = kname;
|
||||||
|
} else {
|
||||||
|
repNames = String_Concat(repNames, String_Concat("+", fragTok.text));
|
||||||
|
repKinds = String_Concat(repKinds, String_Concat("+", kname));
|
||||||
|
}
|
||||||
|
if rule.paramCount < 9 {
|
||||||
|
parserSetMacroFragName(rule, rule.paramCount, fragTok.text);
|
||||||
|
rule.paramCount = rule.paramCount + 1;
|
||||||
|
}
|
||||||
|
nIn = nIn + 1;
|
||||||
|
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
|
||||||
|
if !parserMatch(p, tkComma) { break; }
|
||||||
|
}
|
||||||
|
discard parserExpect(p, tkRParen, "expected ')' after repeated fragment");
|
||||||
|
var sep: String = "";
|
||||||
|
if parserMatch(p, tkComma) { sep = ","; }
|
||||||
|
discard parserExpect(p, tkStar, "expected '*' after macro repetition");
|
||||||
|
let enc: String = String_Concat("rep:", String_Concat(repKinds, sep));
|
||||||
|
// mark compound count via leading digit in typeParam0 of rule (hack: use isDrop)
|
||||||
|
if rule.paramCount > 0 {
|
||||||
|
// store chunk size on last param via isVariadic false; use methodCount as chunk
|
||||||
|
// Encode: kindsEnc entry includes chunk after @
|
||||||
|
let enc2: String = String_Concat(enc, String_Concat("@", String_FromInt(nIn)));
|
||||||
|
if String_Eq(kindsEnc, "") { kindsEnc = enc2; }
|
||||||
|
else { kindsEnc = String_Concat(kindsEnc, String_Concat(";", enc2)); }
|
||||||
|
}
|
||||||
|
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
|
||||||
|
if parserMatch(p, tkSemicolon) { continue; }
|
||||||
|
if parserMatch(p, tkComma) { continue; }
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
let fragTok: LexToken = parserExpect(p, tkIdent, "expected $name fragment");
|
||||||
|
if !String_StartsWith(fragTok.text, "$") {
|
||||||
|
parserEmitDiag(p, fragTok.line, fragTok.column, "macro fragment must start with '$'");
|
||||||
|
}
|
||||||
|
discard parserExpect(p, tkColon, "expected ':' after fragment name");
|
||||||
|
let kindTok: LexToken = parserExpect(p, tkIdent, "expected fragment kind");
|
||||||
|
var kname: String = kindTok.text;
|
||||||
|
if String_Eq(kname, "lit") { kname = "literal"; }
|
||||||
|
if !(String_Eq(kname, "expr") || String_Eq(kname, "ident") || String_Eq(kname, "tt")
|
||||||
|
|| String_Eq(kname, "literal") || String_Eq(kname, "block")) {
|
||||||
|
kname = "expr";
|
||||||
|
}
|
||||||
|
if rule.paramCount < 9 {
|
||||||
|
parserSetMacroFragName(rule, rule.paramCount, fragTok.text);
|
||||||
|
rule.paramCount = rule.paramCount + 1;
|
||||||
|
}
|
||||||
|
if String_Eq(kindsEnc, "") { kindsEnc = kname; }
|
||||||
|
else { kindsEnc = String_Concat(kindsEnc, String_Concat(";", kname)); }
|
||||||
|
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
|
||||||
|
if parserMatch(p, tkComma) { continue; }
|
||||||
|
if parserMatch(p, tkSemicolon) { continue; }
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rule.useNames = kindsEnc;
|
||||||
|
discard parserExpect(p, tkRParen, "expected ')' to close macro pattern");
|
||||||
|
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
|
||||||
|
discard parserExpect(p, tkFatArrow, "expected '=>' after macro pattern");
|
||||||
|
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
|
||||||
|
let savedTpl: bool = p.macroTemplateMode;
|
||||||
|
p.macroTemplateMode = true;
|
||||||
|
rule.refBody = parserParseBlock(p);
|
||||||
|
p.macroTemplateMode = savedTpl;
|
||||||
|
|
||||||
|
if firstRule == null as *Decl {
|
||||||
|
firstRule = rule;
|
||||||
|
lastRule = rule;
|
||||||
|
} else {
|
||||||
|
lastRule.childDecl2 = rule;
|
||||||
|
lastRule = rule;
|
||||||
|
}
|
||||||
|
ruleCount = ruleCount + 1;
|
||||||
|
while parserCheck(p, tkNewLine) || parserCheck(p, tkComma) || parserCheck(p, tkSemicolon) {
|
||||||
|
discard parserAdvance(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
discard parserExpect(p, tkRBrace, "expected '}' to close macro");
|
||||||
|
d.childDecl1 = firstRule;
|
||||||
|
d.methodCount = ruleCount;
|
||||||
|
if ruleCount == 0 {
|
||||||
|
parserEmitDiag(p, line, col, "macro has no rules");
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
// Top-level declaration
|
// Top-level declaration
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -2207,11 +2489,15 @@ module Parser {
|
|||||||
}
|
}
|
||||||
let isPublic: bool = parserMatch(p, tkPub);
|
let isPublic: bool = parserMatch(p, tkPub);
|
||||||
|
|
||||||
// Parse @[Checked] / @[Drop] / @[Release] attribute
|
// Parse stacked @[Checked] / @[Drop] / @[Release] attributes
|
||||||
var isChecked: int = 0;
|
var isChecked: int = 0;
|
||||||
var isDrop: int = 0;
|
var isDrop: int = 0;
|
||||||
var isRelease: int = 0;
|
var isRelease: int = 0;
|
||||||
if parserCheck(p, tkAt) {
|
while true {
|
||||||
|
while parserCheck(p, tkNewLine) || parserCheck(p, tkSemicolon) {
|
||||||
|
discard parserAdvance(p);
|
||||||
|
}
|
||||||
|
if !parserCheck(p, tkAt) { break; }
|
||||||
discard parserAdvance(p); // @
|
discard parserAdvance(p); // @
|
||||||
if parserCheck(p, tkLBracket) {
|
if parserCheck(p, tkLBracket) {
|
||||||
discard parserAdvance(p); // [
|
discard parserAdvance(p); // [
|
||||||
@@ -2232,11 +2518,10 @@ module Parser {
|
|||||||
discard parserAdvance(p); // ]
|
discard parserAdvance(p); // ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Skip newlines after attribute before the declaration
|
}
|
||||||
while parserCheck(p, tkNewLine) || parserCheck(p, tkSemicolon) {
|
while parserCheck(p, tkNewLine) || parserCheck(p, tkSemicolon) {
|
||||||
discard parserAdvance(p);
|
discard parserAdvance(p);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let kind: int = parserPeek(p, 0);
|
let kind: int = parserPeek(p, 0);
|
||||||
|
|
||||||
@@ -2270,6 +2555,7 @@ module Parser {
|
|||||||
if kind == tkImport { return parserParseImportDecl(p, isPublic); }
|
if kind == tkImport { return parserParseImportDecl(p, isPublic); }
|
||||||
if kind == tkExtern { return parserParseExternDecl(p, isPublic); }
|
if kind == tkExtern { return parserParseExternDecl(p, isPublic); }
|
||||||
if kind == tkInterface { return parserParseInterfaceDecl(p, isPublic); }
|
if kind == tkInterface { return parserParseInterfaceDecl(p, isPublic); }
|
||||||
|
if kind == tkMacro { return parserParseMacroDecl(p, isPublic); }
|
||||||
|
|
||||||
if kind == tkExtend {
|
if kind == tkExtend {
|
||||||
discard parserAdvance(p);
|
discard parserAdvance(p);
|
||||||
@@ -2396,6 +2682,7 @@ module Parser {
|
|||||||
p.tokenCount = tokenCount;
|
p.tokenCount = tokenCount;
|
||||||
p.pos = 0;
|
p.pos = 0;
|
||||||
p.structInitAllowed = true;
|
p.structInitAllowed = true;
|
||||||
|
p.macroTemplateMode = false;
|
||||||
let diagBuf: *ParserDiag = bux_alloc(256 as uint * sizeof(ParserDiag)) as *ParserDiag;
|
let diagBuf: *ParserDiag = bux_alloc(256 as uint * sizeof(ParserDiag)) as *ParserDiag;
|
||||||
p.diags = diagBuf;
|
p.diags = diagBuf;
|
||||||
p.diagCount = 0;
|
p.diagCount = 0;
|
||||||
|
|||||||
+28
-8
@@ -1202,16 +1202,34 @@ module Sema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Block expression (boolValue = true means unsafe block)
|
// Block expression (boolValue = true means unsafe block)
|
||||||
|
// Value is the last skExpr (macro! templates and `{ e }` as expr).
|
||||||
|
// Check stmts inside a child scope and return last expr type WITHOUT
|
||||||
|
// re-checking outside that scope (locals must stay visible).
|
||||||
if kind == ekBlock {
|
if kind == ekBlock {
|
||||||
if expr.refBlock != null as *Block {
|
if expr.refBlock != null as *Block {
|
||||||
if expr.boolValue {
|
|
||||||
let prevChecked: bool = sema.checkedFunc;
|
let prevChecked: bool = sema.checkedFunc;
|
||||||
|
if expr.boolValue {
|
||||||
sema.checkedFunc = false;
|
sema.checkedFunc = false;
|
||||||
Sema_CheckBlock(sema, expr.refBlock);
|
|
||||||
sema.checkedFunc = prevChecked;
|
|
||||||
} else {
|
|
||||||
Sema_CheckBlock(sema, expr.refBlock);
|
|
||||||
}
|
}
|
||||||
|
var blockScope: Scope = Scope_NewChild(sema.scope);
|
||||||
|
let prevScope: *Scope = sema.scope;
|
||||||
|
sema.scope = &blockScope;
|
||||||
|
var lastType: int = tyVoid;
|
||||||
|
var blkWalk: *Stmt = expr.refBlock.firstStmt;
|
||||||
|
while blkWalk != null as *Stmt {
|
||||||
|
Sema_CheckStmt(sema, blkWalk);
|
||||||
|
if blkWalk.kind == skExpr && blkWalk.child1 != null as *Expr {
|
||||||
|
// CheckStmt already typed the expr; re-read via CheckExpr in-scope
|
||||||
|
lastType = Sema_CheckExpr(sema, blkWalk.child1);
|
||||||
|
if blkWalk.child1.refType != null as *TypeExpr {
|
||||||
|
expr.refType = blkWalk.child1.refType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blkWalk = blkWalk.nextStmt;
|
||||||
|
}
|
||||||
|
sema.scope = prevScope;
|
||||||
|
sema.checkedFunc = prevChecked;
|
||||||
|
return lastType;
|
||||||
}
|
}
|
||||||
return tyVoid;
|
return tyVoid;
|
||||||
}
|
}
|
||||||
@@ -1424,6 +1442,8 @@ module Sema {
|
|||||||
sym.refType = null as *TypeExpr;
|
sym.refType = null as *TypeExpr;
|
||||||
if stmt.refStmtType != null as *TypeExpr {
|
if stmt.refStmtType != null as *TypeExpr {
|
||||||
sym.refType = stmt.refStmtType;
|
sym.refType = stmt.refStmtType;
|
||||||
|
// Prefer annotation typeKind (block inits previously left tyVoid)
|
||||||
|
sym.typeKind = Sema_ResolveType(sema, stmt.refStmtType);
|
||||||
if stmt.refStmtType.kind == tekPointer && stmt.refStmtType.pointerPointee != null as *TypeExpr {
|
if stmt.refStmtType.kind == tekPointer && stmt.refStmtType.pointerPointee != null as *TypeExpr {
|
||||||
sym.typeName = String_Concat(stmt.refStmtType.pointerPointee.typeName, "*");
|
sym.typeName = String_Concat(stmt.refStmtType.pointerPointee.typeName, "*");
|
||||||
} else {
|
} else {
|
||||||
@@ -2329,13 +2349,13 @@ module Sema {
|
|||||||
s.currentRetType = tyVoid;
|
s.currentRetType = tyVoid;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable borrow checking for @[Checked] functions
|
// @[Checked] enables borrow checks; @[Release] forces zero-cost (C.4)
|
||||||
let wasChecked: bool = s.checkedFunc;
|
let wasChecked: bool = s.checkedFunc;
|
||||||
s.checkedFunc = decl.isChecked != 0;
|
|
||||||
let wasRelease: bool = s.releaseFunc;
|
let wasRelease: bool = s.releaseFunc;
|
||||||
s.releaseFunc = decl.isRelease != 0;
|
s.releaseFunc = decl.isRelease != 0;
|
||||||
|
s.checkedFunc = (decl.isChecked != 0) && !s.releaseFunc;
|
||||||
s.movedCount = 0;
|
s.movedCount = 0;
|
||||||
// C.1: lifetime elision before walking the body
|
// C.1: lifetime elision before walking the body (no-op if not checked)
|
||||||
Sema_ApplyLifetimeElision(s, decl);
|
Sema_ApplyLifetimeElision(s, decl);
|
||||||
|
|
||||||
// Check body statements
|
// Check body statements
|
||||||
|
|||||||
@@ -147,6 +147,10 @@ module Token {
|
|||||||
// Lifetime parameter token: 'a, 'b, ... (not a char literal)
|
// Lifetime parameter token: 'a, 'b, ... (not a char literal)
|
||||||
const tkLifetime: int = 111;
|
const tkLifetime: int = 111;
|
||||||
|
|
||||||
|
// Declarative macros (session 60 — selfhost parity)
|
||||||
|
const tkMacro: int = 112;
|
||||||
|
const tkDollar: int = 113; // bare $ for $(…)*
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Token struct
|
// Token struct
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -229,6 +233,7 @@ module Token {
|
|||||||
if String_Eq(text, "async") { return tkAsync; }
|
if String_Eq(text, "async") { return tkAsync; }
|
||||||
if String_Eq(text, "await") { return tkAwait; }
|
if String_Eq(text, "await") { return tkAwait; }
|
||||||
if String_Eq(text, "spawn") { return tkSpawn; }
|
if String_Eq(text, "spawn") { return tkSpawn; }
|
||||||
|
if String_Eq(text, "macro") { return tkMacro; }
|
||||||
if String_Eq(text, "true") { return tkBoolLiteral; }
|
if String_Eq(text, "true") { return tkBoolLiteral; }
|
||||||
if String_Eq(text, "false") { return tkBoolLiteral; }
|
if String_Eq(text, "false") { return tkBoolLiteral; }
|
||||||
return tkIdent;
|
return tkIdent;
|
||||||
@@ -318,6 +323,8 @@ module Token {
|
|||||||
if kind == tkAmpAmp { return "&&"; }
|
if kind == tkAmpAmp { return "&&"; }
|
||||||
if kind == tkPipePipe { return "||"; }
|
if kind == tkPipePipe { return "||"; }
|
||||||
if kind == tkBang { return "!"; }
|
if kind == tkBang { return "!"; }
|
||||||
|
if kind == tkMacro { return "macro"; }
|
||||||
|
if kind == tkDollar { return "$"; }
|
||||||
if kind == tkEq { return "=="; }
|
if kind == tkEq { return "=="; }
|
||||||
if kind == tkNe { return "!="; }
|
if kind == tkNe { return "!="; }
|
||||||
if kind == tkLt { return "<"; }
|
if kind == tkLt { return "<"; }
|
||||||
|
|||||||
@@ -371,3 +371,46 @@ func Main() -> int {
|
|||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
check(not res.hasErrors)
|
check(not res.hasErrors)
|
||||||
|
|
||||||
|
test "@[Release] alone disables checks (zero-cost path)":
|
||||||
|
let res = checkSource("""
|
||||||
|
@[Release]
|
||||||
|
func Dangle() -> &int {
|
||||||
|
var x: int = 1;
|
||||||
|
return &x;
|
||||||
|
}
|
||||||
|
func Main() -> int {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
check(not res.hasErrors)
|
||||||
|
|
||||||
|
test "@[Checked] @[Release] — Release wins, no use-after-move error":
|
||||||
|
let res = checkSource("""
|
||||||
|
@[Checked]
|
||||||
|
@[Release]
|
||||||
|
func Consume(s: own String) {
|
||||||
|
// move then use — allowed because Release disables checker
|
||||||
|
let t: own String = s;
|
||||||
|
let u: own String = s;
|
||||||
|
}
|
||||||
|
func Main() -> int {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
check(not res.hasErrors)
|
||||||
|
|
||||||
|
test "@[Checked] still errors without Release":
|
||||||
|
let res = checkSource("""
|
||||||
|
@[Checked]
|
||||||
|
func Bad() -> &int {
|
||||||
|
var x: int = 1;
|
||||||
|
return &x;
|
||||||
|
}
|
||||||
|
func Main() -> int {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
check(res.hasErrors)
|
||||||
|
check(res.diagnostics[0].message.contains("local") or
|
||||||
|
res.diagnostics[0].message.contains("reference"))
|
||||||
+65
-4
@@ -17,6 +17,8 @@
|
|||||||
# v0.13.0: textDocument/implementation (interface → types / methods).
|
# v0.13.0: textDocument/implementation (interface → types / methods).
|
||||||
# v0.14.0: workspace-wide import path index (no open-doc required).
|
# v0.14.0: workspace-wide import path index (no open-doc required).
|
||||||
# v0.15.0: type hierarchy (prepare / supertypes / subtypes via extend for).
|
# v0.15.0: type hierarchy (prepare / supertypes / subtypes via extend for).
|
||||||
|
# v0.16.0: workspace type-impl index — hierarchy works for closed multi-file
|
||||||
|
# docs even when `extend T for I` has no methods (no open required).
|
||||||
|
|
||||||
import std/[json, os, strutils, streams, tables, osproc, sequtils, sets]
|
import std/[json, os, strutils, streams, tables, osproc, sequtils, sets]
|
||||||
import lexer, parser, ast, sema, types, scope, source_location
|
import lexer, parser, ast, sema, types, scope, source_location
|
||||||
@@ -154,6 +156,10 @@ var
|
|||||||
## Import paths by file URI (from scanWorkspace + open docs) — v0.14
|
## Import paths by file URI (from scanWorkspace + open docs) — v0.14
|
||||||
## Each entry is a full path like @["Std", "Io"] (not open-doc dependent).
|
## Each entry is a full path like @["Std", "Io"] (not open-doc dependent).
|
||||||
workspaceImportPaths = initTable[string, seq[seq[string]]]()
|
workspaceImportPaths = initTable[string, seq[seq[string]]]()
|
||||||
|
## Type ↔ interface relations from `extend Type for Iface` (v0.16).
|
||||||
|
## Keyed by file URI so re-analyze replaces stale entries (open or closed).
|
||||||
|
## Does not require methods in the extend body (unlike workspaceImpls).
|
||||||
|
workspaceTypeRels = initTable[string, seq[tuple[typeName, iface: string, line: int]]]()
|
||||||
cachedStdlibDir = ""
|
cachedStdlibDir = ""
|
||||||
cachedStdlibDecls: seq[Decl] = @[]
|
cachedStdlibDecls: seq[Decl] = @[]
|
||||||
stdlibLoaded = false
|
stdlibLoaded = false
|
||||||
@@ -170,6 +176,11 @@ proc registerWorkspaceImports(uri: string, segs: seq[PathSegInfo]) =
|
|||||||
paths.add(s.path)
|
paths.add(s.path)
|
||||||
workspaceImportPaths[uri] = paths
|
workspaceImportPaths[uri] = paths
|
||||||
|
|
||||||
|
proc registerWorkspaceTypeRels(uri: string, impls: seq[tuple[typeName, iface: string, line: int]]) =
|
||||||
|
## Replace type↔interface relations for this URI (from analyzeFile `impls`).
|
||||||
|
## Empty impls clears prior entries so deleted extends disappear from hierarchy.
|
||||||
|
workspaceTypeRels[uri] = impls
|
||||||
|
|
||||||
proc getDoc(uri: string): DocumentState =
|
proc getDoc(uri: string): DocumentState =
|
||||||
if not documents.hasKey(uri):
|
if not documents.hasKey(uri):
|
||||||
documents[uri] = DocumentState(uri: uri)
|
documents[uri] = DocumentState(uri: uri)
|
||||||
@@ -630,6 +641,8 @@ proc analyzeFile(path: string, content: string): DocumentState =
|
|||||||
|
|
||||||
# Always refresh workspace import index for this URI (empty clears stale paths)
|
# Always refresh workspace import index for this URI (empty clears stale paths)
|
||||||
registerWorkspaceImports(result.uri, result.importPaths)
|
registerWorkspaceImports(result.uri, result.importPaths)
|
||||||
|
# Type hierarchy / implementation: keep extend-for relations for closed files
|
||||||
|
registerWorkspaceTypeRels(result.uri, result.impls)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Real sema types for hover
|
# Real sema types for hover
|
||||||
@@ -2631,8 +2644,30 @@ proc collectImplementorFuncs(iface, meth: string): seq[FuncSym] =
|
|||||||
|
|
||||||
proc collectTypeImplementorLocs(iface: string): seq[JsonNode] =
|
proc collectTypeImplementorLocs(iface: string): seq[JsonNode] =
|
||||||
## Locations of types that `extend Type for iface`.
|
## Locations of types that `extend Type for iface`.
|
||||||
|
## Uses workspace type-rel index so closed multi-file works without methods.
|
||||||
result = @[]
|
result = @[]
|
||||||
var seen = initHashSet[string]()
|
var seen = initHashSet[string]()
|
||||||
|
# 1) Workspace type relations
|
||||||
|
for uri, rels in workspaceTypeRels.pairs:
|
||||||
|
for impl in rels:
|
||||||
|
if impl.iface != iface: continue
|
||||||
|
let key = uri & "#" & impl.typeName
|
||||||
|
if seen.contains(key): continue
|
||||||
|
seen.incl(key)
|
||||||
|
if workspaceSymbols.hasKey(impl.typeName):
|
||||||
|
let ws = workspaceSymbols[impl.typeName]
|
||||||
|
result.add(locationJson(ws.uri, ws.info.line, ws.info.col, impl.typeName.len))
|
||||||
|
elif documents.hasKey(uri):
|
||||||
|
let doc = documents[uri]
|
||||||
|
ensureAnalyzed(doc)
|
||||||
|
if doc.symbols.hasKey(impl.typeName):
|
||||||
|
let info = doc.symbols[impl.typeName]
|
||||||
|
result.add(locationJson(uri, info.line, info.col, impl.typeName.len))
|
||||||
|
else:
|
||||||
|
result.add(locationJson(uri, impl.line, 0, max(1, impl.typeName.len)))
|
||||||
|
else:
|
||||||
|
result.add(locationJson(uri, impl.line, 0, max(1, impl.typeName.len)))
|
||||||
|
# 2) Open docs
|
||||||
for uri, doc in documents.pairs:
|
for uri, doc in documents.pairs:
|
||||||
ensureAnalyzed(doc)
|
ensureAnalyzed(doc)
|
||||||
for impl in doc.impls:
|
for impl in doc.impls:
|
||||||
@@ -2644,9 +2679,8 @@ proc collectTypeImplementorLocs(iface: string): seq[JsonNode] =
|
|||||||
let info = doc.symbols[impl.typeName]
|
let info = doc.symbols[impl.typeName]
|
||||||
result.add(locationJson(uri, info.line, info.col, impl.typeName.len))
|
result.add(locationJson(uri, info.line, info.col, impl.typeName.len))
|
||||||
else:
|
else:
|
||||||
# Fall back to the `extend` line
|
|
||||||
result.add(locationJson(uri, impl.line, 0, max(1, impl.typeName.len)))
|
result.add(locationJson(uri, impl.line, 0, max(1, impl.typeName.len)))
|
||||||
# Derive types from workspaceImpls (Iface.Method → typeName)
|
# 3) Fallback: workspaceImpls (Iface.Method → typeName)
|
||||||
for wkey, impls in workspaceImpls.pairs:
|
for wkey, impls in workspaceImpls.pairs:
|
||||||
if not wkey.startsWith(iface & "."): continue
|
if not wkey.startsWith(iface & "."): continue
|
||||||
for impl in impls:
|
for impl in impls:
|
||||||
@@ -2990,8 +3024,22 @@ proc typeHierarchyItemSynthetic(uri: string, name: string, kind: string, line: i
|
|||||||
|
|
||||||
proc collectSubtypeItems(iface: string): seq[JsonNode] =
|
proc collectSubtypeItems(iface: string): seq[JsonNode] =
|
||||||
## Types that `extend Type for iface`.
|
## Types that `extend Type for iface`.
|
||||||
|
## Prefer workspace type-rel index (closed multi-file; empty extend bodies OK).
|
||||||
result = @[]
|
result = @[]
|
||||||
var seen = initHashSet[string]()
|
var seen = initHashSet[string]()
|
||||||
|
# 1) Workspace type relations (scan + every analyzeFile) — no open required
|
||||||
|
for uri, rels in workspaceTypeRels.pairs:
|
||||||
|
for impl in rels:
|
||||||
|
if impl.iface != iface: continue
|
||||||
|
let key = uri & "#" & impl.typeName
|
||||||
|
if seen.contains(key): continue
|
||||||
|
seen.incl(key)
|
||||||
|
let (ok, u, info) = resolveTypeSymbol(impl.typeName, uri)
|
||||||
|
if ok:
|
||||||
|
result.add(typeHierarchyItem(u, impl.typeName, info))
|
||||||
|
else:
|
||||||
|
result.add(typeHierarchyItemSynthetic(uri, impl.typeName, "struct", impl.line))
|
||||||
|
# 2) Open docs (live buffer may differ from last register)
|
||||||
for uri, doc in documents.pairs:
|
for uri, doc in documents.pairs:
|
||||||
ensureAnalyzed(doc)
|
ensureAnalyzed(doc)
|
||||||
for impl in doc.impls:
|
for impl in doc.impls:
|
||||||
@@ -3004,7 +3052,7 @@ proc collectSubtypeItems(iface: string): seq[JsonNode] =
|
|||||||
result.add(typeHierarchyItem(u, impl.typeName, info))
|
result.add(typeHierarchyItem(u, impl.typeName, info))
|
||||||
else:
|
else:
|
||||||
result.add(typeHierarchyItemSynthetic(uri, impl.typeName, "struct", impl.line))
|
result.add(typeHierarchyItemSynthetic(uri, impl.typeName, "struct", impl.line))
|
||||||
# workspaceImpls: "Iface.Method" → (uri, typeName, meth)
|
# 3) Fallback: workspaceImpls "Iface.Method" (methods in extend body)
|
||||||
for wkey, impls in workspaceImpls.pairs:
|
for wkey, impls in workspaceImpls.pairs:
|
||||||
if not wkey.startsWith(iface & "."): continue
|
if not wkey.startsWith(iface & "."): continue
|
||||||
for impl in impls:
|
for impl in impls:
|
||||||
@@ -3021,6 +3069,18 @@ proc collectSupertypeItems(typeName: string): seq[JsonNode] =
|
|||||||
## Interfaces that `typeName` implements via `extend typeName for I`.
|
## Interfaces that `typeName` implements via `extend typeName for I`.
|
||||||
result = @[]
|
result = @[]
|
||||||
var seen = initHashSet[string]()
|
var seen = initHashSet[string]()
|
||||||
|
# 1) Workspace type relations (closed multi-file)
|
||||||
|
for uri, rels in workspaceTypeRels.pairs:
|
||||||
|
for impl in rels:
|
||||||
|
if impl.typeName != typeName: continue
|
||||||
|
if seen.contains(impl.iface): continue
|
||||||
|
seen.incl(impl.iface)
|
||||||
|
let (ok, u, info) = resolveTypeSymbol(impl.iface, uri)
|
||||||
|
if ok:
|
||||||
|
result.add(typeHierarchyItem(u, impl.iface, info))
|
||||||
|
else:
|
||||||
|
result.add(typeHierarchyItemSynthetic(uri, impl.iface, "interface", impl.line))
|
||||||
|
# 2) Open docs
|
||||||
for uri, doc in documents.pairs:
|
for uri, doc in documents.pairs:
|
||||||
ensureAnalyzed(doc)
|
ensureAnalyzed(doc)
|
||||||
for impl in doc.impls:
|
for impl in doc.impls:
|
||||||
@@ -3032,6 +3092,7 @@ proc collectSupertypeItems(typeName: string): seq[JsonNode] =
|
|||||||
result.add(typeHierarchyItem(u, impl.iface, info))
|
result.add(typeHierarchyItem(u, impl.iface, info))
|
||||||
else:
|
else:
|
||||||
result.add(typeHierarchyItemSynthetic(uri, impl.iface, "interface", impl.line))
|
result.add(typeHierarchyItemSynthetic(uri, impl.iface, "interface", impl.line))
|
||||||
|
# 3) Fallback: method-based workspaceImpls
|
||||||
for wkey, impls in workspaceImpls.pairs:
|
for wkey, impls in workspaceImpls.pairs:
|
||||||
for impl in impls:
|
for impl in impls:
|
||||||
if impl.typeName != typeName: continue
|
if impl.typeName != typeName: continue
|
||||||
@@ -3137,7 +3198,7 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
|
|||||||
"implementationProvider": true,
|
"implementationProvider": true,
|
||||||
"typeHierarchyProvider": true
|
"typeHierarchyProvider": true
|
||||||
},
|
},
|
||||||
"serverInfo": {"name": "bux-lsp", "version": "0.15.0"}
|
"serverInfo": {"name": "bux-lsp", "version": "0.16.0"}
|
||||||
})
|
})
|
||||||
if paramsNode.hasKey("rootPath") and paramsNode["rootPath"].kind != JNull:
|
if paramsNode.hasKey("rootPath") and paramsNode["rootPath"].kind != JNull:
|
||||||
rootPath = paramsNode["rootPath"].getStr()
|
rootPath = paramsNode["rootPath"].getStr()
|
||||||
|
|||||||
Executable
+81
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Golden-ish smoke: field-move + partial field-move Drop emission.
|
||||||
|
# Ensures C for TakeItems has no Bag_Drop (would double-free returned Array).
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
BUXC="${BUXC:-$ROOT/buxc}"
|
||||||
|
export BUX_STDLIB="${BUX_STDLIB:-$ROOT/lib}"
|
||||||
|
unset BUX_DEBUG_FILE || true
|
||||||
|
|
||||||
|
if [[ ! -x "$BUXC" ]]; then
|
||||||
|
(cd "$ROOT" && make build)
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
# --- move_field (whole local into field) ---
|
||||||
|
echo "=== smoke: move_field ==="
|
||||||
|
mkdir -p "$TMP/mf/src"
|
||||||
|
cp -a "$ROOT/rt" "$TMP/mf/"
|
||||||
|
cat > "$TMP/mf/bux.toml" <<'EOF'
|
||||||
|
[Package]
|
||||||
|
Name = "move_field"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
|
EOF
|
||||||
|
cp "$ROOT/examples/move_field.bux" "$TMP/mf/src/Main.bux"
|
||||||
|
(cd "$TMP/mf" && "$BUXC" run .)
|
||||||
|
# Only the MakeBox *body* (not prototypes / other functions)
|
||||||
|
if sed -n '/^Box MakeBox(void) {/,/^}/p' "$TMP/mf/build/main.c" | grep -q 'Array_Drop\|Bag_Drop'; then
|
||||||
|
echo "error: MakeBox still drops moved Array" >&2
|
||||||
|
sed -n '/^Box MakeBox(void) {/,/^}/p' "$TMP/mf/build/main.c"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " move_field: PASS (run + no Array_Drop of moved local)"
|
||||||
|
|
||||||
|
# --- partial field move ---
|
||||||
|
echo "=== smoke: move_field_partial ==="
|
||||||
|
mkdir -p "$TMP/mp/src"
|
||||||
|
cp -a "$ROOT/rt" "$TMP/mp/"
|
||||||
|
cat > "$TMP/mp/bux.toml" <<'EOF'
|
||||||
|
[Package]
|
||||||
|
Name = "move_field_partial"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
|
EOF
|
||||||
|
cp "$ROOT/examples/move_field_partial.bux" "$TMP/mp/src/Main.bux"
|
||||||
|
(cd "$TMP/mp" && "$BUXC" run .)
|
||||||
|
# TakeItems must not call Bag_Drop after moving bag.items out
|
||||||
|
if sed -n '/^Array_int TakeItems/,/^}/p' "$TMP/mp/build/main.c" | grep -q 'Bag_Drop'; then
|
||||||
|
echo "error: TakeItems still Bag_Drops after partial field move" >&2
|
||||||
|
sed -n '/^Array_int TakeItems/,/^}/p' "$TMP/mp/build/main.c"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " move_field_partial: PASS (run + TakeItems has no Bag_Drop)"
|
||||||
|
|
||||||
|
# --- early return Drop counts ---
|
||||||
|
echo "=== smoke: drop_early_return ==="
|
||||||
|
mkdir -p "$TMP/de/src"
|
||||||
|
cp -a "$ROOT/rt" "$TMP/de/"
|
||||||
|
cat > "$TMP/de/bux.toml" <<'EOF'
|
||||||
|
[Package]
|
||||||
|
Name = "drop_early_return"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
|
EOF
|
||||||
|
cp "$ROOT/examples/drop_early_return.bux" "$TMP/de/src/Main.bux"
|
||||||
|
out=$(cd "$TMP/de" && "$BUXC" run .)
|
||||||
|
echo "$out" | grep -q 'PASS'
|
||||||
|
echo " drop_early_return: PASS"
|
||||||
|
|
||||||
|
echo "PASS: smoke_drop_move (field-move + partial + early-return)"
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Smoke: type hierarchy prepare / subtypes / supertypes (bux-lsp 0.15)
|
# Smoke: type hierarchy prepare / subtypes / supertypes (single-file; bux-lsp 0.15+)
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
LSP="$ROOT/tools/bux-lsp"
|
LSP="$ROOT/tools/bux-lsp"
|
||||||
@@ -62,8 +62,8 @@ URI="file://$TMP/Main.bux"
|
|||||||
rpc '{"jsonrpc":"2.0","method":"exit","params":null}'
|
rpc '{"jsonrpc":"2.0","method":"exit","params":null}'
|
||||||
} | "$LSP" 2>/dev/null | tr '\r' '\n' > "$TMP/out.txt"
|
} | "$LSP" 2>/dev/null | tr '\r' '\n' > "$TMP/out.txt"
|
||||||
|
|
||||||
if ! grep -q '0.15.0' "$TMP/out.txt"; then
|
if ! grep -qE '0\.(15|16)\.0' "$TMP/out.txt"; then
|
||||||
echo "WARN: version not 0.15.0"
|
echo "WARN: unexpected LSP version (expected 0.15+)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! grep -q 'typeHierarchyProvider' "$TMP/out.txt"; then
|
if ! grep -q 'typeHierarchyProvider' "$TMP/out.txt"; then
|
||||||
@@ -120,5 +120,5 @@ if 'Drawable' not in snames:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
print(f' supertypes Circle → {sorted(snames)}')
|
print(f' supertypes Circle → {sorted(snames)}')
|
||||||
|
|
||||||
print('PASS: LSP type hierarchy (0.15)')
|
print('PASS: LSP type hierarchy (single-file)')
|
||||||
PY
|
PY
|
||||||
|
|||||||
Executable
+158
@@ -0,0 +1,158 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Smoke: type hierarchy across closed multi-file workspace (bux-lsp 0.16)
|
||||||
|
# Only Main.bux is opened. Drawable.bux + Shapes.bux stay closed (scanWorkspace).
|
||||||
|
# Empty `extend T for I {}` bodies — no methods — must still populate hierarchy
|
||||||
|
# via workspaceTypeRels (not method-only workspaceImpls).
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
LSP="$ROOT/tools/bux-lsp"
|
||||||
|
TMP=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
if [[ ! -x "$LSP" ]]; then
|
||||||
|
(cd "$ROOT" && make lsp >/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$TMP/Drawable.bux" <<'EOF'
|
||||||
|
interface Drawable {
|
||||||
|
func Draw(self: &Self);
|
||||||
|
}
|
||||||
|
interface Named {
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$TMP/Shapes.bux" <<'EOF'
|
||||||
|
struct Circle {
|
||||||
|
radius: int;
|
||||||
|
}
|
||||||
|
struct Square {
|
||||||
|
side: int;
|
||||||
|
}
|
||||||
|
// Empty extend bodies — no methods; relation must still be indexed
|
||||||
|
extend Circle for Drawable {
|
||||||
|
}
|
||||||
|
extend Square for Drawable {
|
||||||
|
}
|
||||||
|
extend Circle for Named {
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Only this file is opened. Types are closed on disk.
|
||||||
|
cat > "$TMP/Main.bux" <<'EOF'
|
||||||
|
func Use(d: Drawable, c: Circle) -> int {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
func Main() -> int {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
rpc() {
|
||||||
|
local body="$1"
|
||||||
|
local len
|
||||||
|
len=$(printf '%s' "$body" | wc -c)
|
||||||
|
printf 'Content-Length: %s\r\n\r\n%s' "$len" "$body"
|
||||||
|
}
|
||||||
|
|
||||||
|
CONTENT_JSON=$(python3 -c 'import json,sys; print(json.dumps(open(sys.argv[1]).read()))' "$TMP/Main.bux")
|
||||||
|
URI="file://$TMP/Main.bux"
|
||||||
|
# Drawable at col 12, Circle at col 25 in "func Use(d: Drawable, c: Circle)..."
|
||||||
|
DRAW_COL=12
|
||||||
|
CIRC_COL=25
|
||||||
|
|
||||||
|
{
|
||||||
|
rpc '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},"rootUri":"file://'"$TMP"'"}}'
|
||||||
|
rpc '{"jsonrpc":"2.0","method":"initialized","params":{}}'
|
||||||
|
rpc '{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"'"$URI"'","languageId":"bux","version":1,"text":'"$CONTENT_JSON"'}}}'
|
||||||
|
# prepare on Drawable (type annotation; def in closed Drawable.bux)
|
||||||
|
rpc '{"jsonrpc":"2.0","id":2,"method":"textDocument/prepareTypeHierarchy","params":{"textDocument":{"uri":"'"$URI"'"},"position":{"line":0,"character":'"$DRAW_COL"'}}}'
|
||||||
|
# subtypes of Drawable → Circle + Square (closed Shapes.bux, empty extends)
|
||||||
|
rpc '{"jsonrpc":"2.0","id":3,"method":"typeHierarchy/subtypes","params":{"item":{"name":"Drawable","kind":11,"uri":"file://'"$TMP"'/Drawable.bux","data":"Drawable","range":{"start":{"line":0,"character":0},"end":{"line":0,"character":8}},"selectionRange":{"start":{"line":0,"character":0},"end":{"line":0,"character":8}}}}}'
|
||||||
|
# prepare on Circle
|
||||||
|
rpc '{"jsonrpc":"2.0","id":4,"method":"textDocument/prepareTypeHierarchy","params":{"textDocument":{"uri":"'"$URI"'"},"position":{"line":0,"character":'"$CIRC_COL"'}}}'
|
||||||
|
# supertypes of Circle → Drawable + Named
|
||||||
|
rpc '{"jsonrpc":"2.0","id":5,"method":"typeHierarchy/supertypes","params":{"item":{"name":"Circle","kind":23,"uri":"file://'"$TMP"'/Shapes.bux","data":"Circle","range":{"start":{"line":0,"character":0},"end":{"line":0,"character":6}},"selectionRange":{"start":{"line":0,"character":0},"end":{"line":0,"character":6}}}}}'
|
||||||
|
# subtypes of Named → Circle only
|
||||||
|
rpc '{"jsonrpc":"2.0","id":6,"method":"typeHierarchy/subtypes","params":{"item":{"name":"Named","kind":11,"uri":"file://'"$TMP"'/Drawable.bux","data":"Named","range":{"start":{"line":0,"character":0},"end":{"line":0,"character":5}},"selectionRange":{"start":{"line":0,"character":0},"end":{"line":0,"character":5}}}}}'
|
||||||
|
rpc '{"jsonrpc":"2.0","id":7,"method":"shutdown","params":null}'
|
||||||
|
rpc '{"jsonrpc":"2.0","method":"exit","params":null}'
|
||||||
|
} | "$LSP" 2>/dev/null | tr '\r' '\n' > "$TMP/out.txt"
|
||||||
|
|
||||||
|
if ! grep -q '0.16.0' "$TMP/out.txt"; then
|
||||||
|
echo "WARN: version not 0.16.0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 - <<'PY' "$TMP/out.txt" "$TMP"
|
||||||
|
import json, sys, re
|
||||||
|
raw = open(sys.argv[1]).read()
|
||||||
|
tmp = sys.argv[2]
|
||||||
|
got = {}
|
||||||
|
for p in re.split(r'Content-Length:\s*\d+\s*', raw):
|
||||||
|
p = p.strip()
|
||||||
|
if not p.startswith('{'):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
j = json.loads(p)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if 'id' in j and 'result' in j:
|
||||||
|
got[j['id']] = j['result']
|
||||||
|
|
||||||
|
def names(r):
|
||||||
|
if not isinstance(r, list):
|
||||||
|
return set()
|
||||||
|
return {x.get('name') for x in r if isinstance(x, dict)}
|
||||||
|
|
||||||
|
def uris(r):
|
||||||
|
if not isinstance(r, list):
|
||||||
|
return set()
|
||||||
|
return {x.get('uri', '') for x in r if isinstance(x, dict)}
|
||||||
|
|
||||||
|
r2 = got.get(2) or []
|
||||||
|
if 'Drawable' not in names(r2):
|
||||||
|
print('FAIL: prepare Drawable (closed def) missing')
|
||||||
|
print(got.get(2))
|
||||||
|
sys.exit(1)
|
||||||
|
# Prefer closed file URI for the type item
|
||||||
|
u2 = uris(r2)
|
||||||
|
if not any('Drawable.bux' in u for u in u2):
|
||||||
|
print(f'FAIL: prepare Drawable should point at Drawable.bux, got {u2}')
|
||||||
|
sys.exit(1)
|
||||||
|
print(' prepare Drawable → closed Drawable.bux: OK')
|
||||||
|
|
||||||
|
r3 = got.get(3) or []
|
||||||
|
n3 = names(r3)
|
||||||
|
if 'Circle' not in n3 or 'Square' not in n3:
|
||||||
|
print(f'FAIL: subtypes Drawable expected Circle+Square (empty extends), got {n3}')
|
||||||
|
print(json.dumps(r3, indent=2)[:800])
|
||||||
|
sys.exit(1)
|
||||||
|
u3 = uris(r3)
|
||||||
|
if not any('Shapes.bux' in u for u in u3):
|
||||||
|
print(f'FAIL: subtypes should reference closed Shapes.bux, got {u3}')
|
||||||
|
sys.exit(1)
|
||||||
|
print(f' subtypes Drawable → {sorted(n3)} (closed, empty extend): OK')
|
||||||
|
|
||||||
|
r4 = got.get(4) or []
|
||||||
|
if 'Circle' not in names(r4):
|
||||||
|
print('FAIL: prepare Circle missing')
|
||||||
|
print(got.get(4))
|
||||||
|
sys.exit(1)
|
||||||
|
print(' prepare Circle → closed Shapes.bux: OK')
|
||||||
|
|
||||||
|
r5 = got.get(5) or []
|
||||||
|
n5 = names(r5)
|
||||||
|
if 'Drawable' not in n5 or 'Named' not in n5:
|
||||||
|
print(f'FAIL: supertypes Circle expected Drawable+Named, got {n5}')
|
||||||
|
print(json.dumps(r5, indent=2)[:800])
|
||||||
|
sys.exit(1)
|
||||||
|
print(f' supertypes Circle → {sorted(n5)}: OK')
|
||||||
|
|
||||||
|
r6 = got.get(6) or []
|
||||||
|
n6 = names(r6)
|
||||||
|
if n6 != {'Circle'}:
|
||||||
|
print(f'FAIL: subtypes Named expected only Circle, got {n6}')
|
||||||
|
sys.exit(1)
|
||||||
|
print(f' subtypes Named → {sorted(n6)}: OK')
|
||||||
|
|
||||||
|
print('PASS: LSP type hierarchy workspace index (0.16)')
|
||||||
|
PY
|
||||||
+86
-1
@@ -166,4 +166,89 @@ if ! echo "$main_body" | grep -vE '#line 1 "' | grep -qE '#line [0-9]+ ".*Main\.
|
|||||||
fi
|
fi
|
||||||
echo " Expr/Stmt sourceFile: PASS (Main stmts → Main.bux only)"
|
echo " Expr/Stmt sourceFile: PASS (Main stmts → Main.bux only)"
|
||||||
|
|
||||||
echo "PASS: selfhost smoke (move_field + multi-file #line + HirNode/Expr sourceFile)"
|
# ---------------------------------------------------------------------------
|
||||||
|
# 5) Binary op parentheses — C precedence must not rewrite Mul(Add,c)
|
||||||
|
# Without parens selfhost emitted `a + b * c` → 7 instead of (a+b)*c → 9
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "=== selfhost: binary op parentheses (C precedence) ==="
|
||||||
|
PREC="$TMP/c_precedence"
|
||||||
|
mkdir -p "$PREC/src"
|
||||||
|
cp -a "$ROOT/rt" "$PREC/"
|
||||||
|
cat > "$PREC/bux.toml" <<'EOF'
|
||||||
|
[Package]
|
||||||
|
Name = "c_precedence"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
|
EOF
|
||||||
|
cp "$ROOT/examples/c_precedence.bux" "$PREC/src/Main.bux"
|
||||||
|
|
||||||
|
(cd "$PREC" && "$BUXC2" project .)
|
||||||
|
prec_out=$("$PREC/build/c_precedence")
|
||||||
|
echo "$prec_out" | tee "$TMP/prec.out"
|
||||||
|
grep -q 'PASS c_precedence' "$TMP/prec.out"
|
||||||
|
# Generated C must parenthesize the sum before multiply
|
||||||
|
if ! grep -A3 '^int MulSum' "$PREC/build/main.c" | grep -qE '\(a \+ b\) \* c|\(\(a \+ b\) \* c\)'; then
|
||||||
|
echo "error: MulSum C lacks parentheses around a+b before *c" >&2
|
||||||
|
grep -n -A5 '^int MulSum' "$PREC/build/main.c" | head -20
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! grep -A3 '^int SubDiv' "$PREC/build/main.c" | grep -qE '\(a - b\) / c|\(\(a - b\) / c\)'; then
|
||||||
|
echo "error: SubDiv C lacks parentheses around a-b before /c" >&2
|
||||||
|
grep -n -A5 '^int SubDiv' "$PREC/build/main.c" | head -20
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " binary parens: PASS (run 9/3/6/7 + C has (a + b) * c)"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 6) declarative macro! / quote! expand (session 60 selfhost parity)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "=== selfhost: macro! expand ==="
|
||||||
|
MAC="$TMP/macro_twice"
|
||||||
|
mkdir -p "$MAC/src"
|
||||||
|
cp -a "$ROOT/rt" "$MAC/"
|
||||||
|
cat > "$MAC/bux.toml" <<'EOF'
|
||||||
|
[Package]
|
||||||
|
Name = "macro_twice"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
|
EOF
|
||||||
|
cp "$ROOT/examples/macro_twice.bux" "$MAC/src/Main.bux"
|
||||||
|
(cd "$MAC" && "$BUXC2" project .)
|
||||||
|
mac_out=$("$MAC/build/macro_twice")
|
||||||
|
echo "$mac_out" | tee "$TMP/mac.out"
|
||||||
|
grep -q 'PASS macro_twice' "$TMP/mac.out"
|
||||||
|
grep -q '42' "$TMP/mac.out"
|
||||||
|
echo " macro!: PASS (twice/add2/quote → 42/42/43)"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 7) multi-rep / compound zip / nested template $(…)* (session 63)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "=== selfhost: macro_nested multi-rep ==="
|
||||||
|
MACN="$TMP/macro_nested"
|
||||||
|
mkdir -p "$MACN/src"
|
||||||
|
cp -a "$ROOT/rt" "$MACN/"
|
||||||
|
cat > "$MACN/bux.toml" <<'EOF'
|
||||||
|
[Package]
|
||||||
|
Name = "macro_nested"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
|
EOF
|
||||||
|
cp "$ROOT/examples/macro_nested.bux" "$MACN/src/Main.bux"
|
||||||
|
(cd "$MACN" && "$BUXC2" project .)
|
||||||
|
macn_out=$("$MACN/build/macro_nested")
|
||||||
|
echo "$macn_out" | tee "$TMP/macn.out"
|
||||||
|
grep -q 'PASS macro_nested' "$TMP/macn.out"
|
||||||
|
grep -q '33' "$TMP/macn.out"
|
||||||
|
grep -q '63' "$TMP/macn.out"
|
||||||
|
echo " macro_nested: PASS (add_pairs/sum_groups/double_each/named_sum)"
|
||||||
|
|
||||||
|
echo "PASS: selfhost smoke (move_field + multi-file #line + HirNode/Expr sourceFile + binop parens + macro! + multi-rep)"
|
||||||
|
|||||||
Reference in New Issue
Block a user