Compare commits
178 Commits
ac969b37c1
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| a23860be3e | |||
| d60ce2bc3f | |||
| a785747c37 | |||
| a939f74b1b | |||
| fe3b1e8b6a | |||
| 6f2a3b1d88 | |||
| 1f20c683a1 | |||
| 60b7b7522f | |||
| e63dacfdbf | |||
| cb6df5443a | |||
| b332e98ab3 | |||
| a296f1695d | |||
| fe1da8b42e | |||
| c82757d51c | |||
| 200adf4d68 | |||
| 244a3be8f3 | |||
| 00195e2d98 | |||
| 8f645ad68b | |||
| 5de8d8912d | |||
| 70321075a6 | |||
| ed07cf8921 | |||
| 42f9453e18 | |||
| 94cc94f638 | |||
| cfb89dd72f | |||
| adc5c743a6 | |||
| e30d8a5eb9 | |||
| abb2b54dce | |||
| cd7a82822c | |||
| eb81856565 | |||
| 53b43b0f79 | |||
| 3eb1ad3a82 | |||
| 66f11d1869 | |||
| f9185c96b2 | |||
| e3ca724bfa | |||
| eac78f28c1 | |||
| 26631252c0 | |||
| db41ba4d84 | |||
| f619316470 | |||
| 2cbbccc508 | |||
| e805af6ef6 | |||
| f1896183f5 | |||
| 9efba57b4c | |||
| 61ac06ab5f | |||
| 94e6806dda | |||
| 9c632ce389 | |||
| aaeb01e518 | |||
| fc0a560e60 | |||
| e11dce56d2 | |||
| e5e490605a | |||
| 2c8223f94e | |||
| 44bac83471 | |||
| 06db4926a7 | |||
| 84df4bba9d | |||
| a668127721 | |||
| 9bbeb4fd1d | |||
| 0e93ea911c | |||
| 290cbc8f98 | |||
| 224542df7f | |||
| a619247f06 | |||
| a4f25bc737 | |||
| aa2d6f9632 | |||
| b549e09a3b | |||
| 2259567ac2 | |||
| f4f09602c6 | |||
| ef4aa42b0b | |||
| 8f68a9da1a | |||
| e4e4e0d8ef | |||
| 18aa232b1c | |||
| c6bae8b9ee | |||
| aef0cd7936 | |||
| 62d92b0aa6 | |||
| 9a63dcca11 | |||
| 10a2b64ac4 | |||
| 92962fa1b0 | |||
| b971775baf | |||
| 85d15cd16d | |||
| de3ac76581 | |||
| 2824d25369 | |||
| 9cd0580832 | |||
| 9e547f123a | |||
| 211a811f32 | |||
| 4456bb00af | |||
| 6a0a0425dd | |||
| 9e515c157d | |||
| 8f8708c5df | |||
| 7392013301 | |||
| 0b887b92d3 | |||
| 7d6cf7f511 | |||
| 0a41ce85f4 | |||
| d6f0a30948 | |||
| a2236de69a | |||
| a4e410fb30 | |||
| 51bd3c172b | |||
| 3a7b4bd5c1 | |||
| a099999e02 | |||
| 5eec4328f3 | |||
| c7d4c87fcf | |||
| d70ccba5ca | |||
| 01677001c3 | |||
| bb84693acb | |||
| f63cbd1bf0 | |||
| 499b389ba8 | |||
| 632fa1535c | |||
| 40bcbeb710 | |||
| b36df0f5b7 | |||
| b3141dbcd5 | |||
| 74db8a5790 | |||
| 6414ef236b | |||
| 9b473c667c | |||
| ba54aa240e | |||
| c83f6d5994 | |||
| 34504d1647 | |||
| 079b76ae71 | |||
| 3ce7ba51d2 | |||
| 41c0fb1d6f | |||
| f809827bea | |||
| 81281cbb11 | |||
| c14578d9dd | |||
| 0ac254facc | |||
| 392dd64f97 | |||
| 90e67079f4 | |||
| 265bda850d | |||
| 3ed5891de2 | |||
| 64bd41067a | |||
| 4a860095fd | |||
| 0c41c7bb25 | |||
| 2e536488e6 | |||
| 3f0a3901ca | |||
| 17c896c4dc | |||
| cfce2a93e5 | |||
| d9aceeac6e | |||
| a67271b08c | |||
| cf92b63ba6 | |||
| 9c3d6dc723 | |||
| 7d1d722cba | |||
| 3c7938163c | |||
| c4e3f80405 | |||
| d253a01aa9 | |||
| 2cb971a483 | |||
| b1deff7fd5 | |||
| 38bc469be7 | |||
| bebc362ae6 | |||
| a7a1b45588 | |||
| a13cee8a0f | |||
| 3b7fc455b0 | |||
| e7e900973f | |||
| a2617e9954 | |||
| 3ffa9f7d54 | |||
| b3af1208e8 | |||
| 0d7a30835f | |||
| adca52b269 | |||
| 04be9eb400 | |||
| bfe87a8acb | |||
| 46814b2abc | |||
| 9758b17b1a | |||
| 8347599d23 | |||
| 4794335673 | |||
| 134e8e877d | |||
| 88af8b09d4 | |||
| e41a8d31df | |||
| a11071ab95 | |||
| 093e08a2b9 | |||
| 1500575982 | |||
| fe048eb4aa | |||
| 9dd964dc48 | |||
| e1038059c2 | |||
| 1f0baefa5a | |||
| 3b4456f01f | |||
| 0bdef58182 | |||
| 3ae8c60ebb | |||
| 9416c486b2 | |||
| ba0a55f467 | |||
| 797ca7c80c | |||
| 4f7653a410 | |||
| 62a9c8b84a | |||
| aff03ffb5a | |||
| a8b909726b | |||
| 2698ca92b7 |
@@ -0,0 +1,458 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||||
|
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:
|
||||||
|
# ── Shared bootstrap build (Linux) ──────────────────────────────────────
|
||||||
|
build:
|
||||||
|
name: build (ubuntu)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y --no-install-recommends \
|
||||||
|
gcc make binutils libssl-dev python3
|
||||||
|
|
||||||
|
- 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:
|
||||||
|
BUX_SKIP_BUILD: "1"
|
||||||
|
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 \
|
||||||
|
gcc-aarch64-linux-gnu
|
||||||
|
- 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 + linux-targets
|
||||||
|
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
|
||||||
|
make test-linux-targets 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 openssl curl
|
||||||
|
- name: Download buxc
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: buxc-linux
|
||||||
|
- name: Prepare buxc
|
||||||
|
run: chmod +x buxc && ./buxc --version
|
||||||
|
- name: test-apps + nexus TLS/mTLS
|
||||||
|
env:
|
||||||
|
BUX_SKIP_BUILD: "1"
|
||||||
|
run: |
|
||||||
|
unset BUX_DEBUG_FILE || true
|
||||||
|
make test-apps BUX_SKIP_BUILD=1
|
||||||
|
make test-nexus-tls BUX_SKIP_BUILD=1
|
||||||
|
make test-nexus-mtls 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 openssl curl
|
||||||
|
- 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: |
|
||||||
|
unset BUX_DEBUG_FILE || true
|
||||||
|
unset BUX_SELFHOST_FIXED_POINT || true
|
||||||
|
make test-selfhost-smoke BUX_SKIP_BUILD=1
|
||||||
|
- name: selfhost install + registry
|
||||||
|
env:
|
||||||
|
BUX_SKIP_BUILD: "1"
|
||||||
|
run: |
|
||||||
|
unset BUX_DEBUG_FILE || true
|
||||||
|
make test-selfhost-install BUX_SKIP_BUILD=1
|
||||||
|
make test-selfhost-registry BUX_SKIP_BUILD=1
|
||||||
|
- name: Upload selfhost artifacts on failure
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ci-failure-selfhost
|
||||||
|
path: |
|
||||||
|
build/selfhost/build/main.c
|
||||||
|
_test_tmp_pkg/**
|
||||||
|
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 + unit + MinGW hello) ──────────────────────
|
||||||
|
# Uses rt/runtime_win.c (no pthread/OpenSSL). Full POSIX runtime is Unix-only.
|
||||||
|
windows:
|
||||||
|
name: windows smoke
|
||||||
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 35
|
||||||
|
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: Install MinGW (gcc)
|
||||||
|
uses: msys2/setup-msys2@v2
|
||||||
|
with:
|
||||||
|
msystem: MINGW64
|
||||||
|
update: false
|
||||||
|
install: mingw-w64-x86_64-gcc
|
||||||
|
path-type: inherit
|
||||||
|
|
||||||
|
- name: Add MinGW64 to PATH
|
||||||
|
run: |
|
||||||
|
# setup-msys2 installs under C:/msys64 by default
|
||||||
|
for d in /c/msys64/mingw64/bin /mingw64/bin "$HOME/msys64/mingw64/bin"; do
|
||||||
|
if [[ -x "$d/gcc.exe" || -x "$d/gcc" ]]; then
|
||||||
|
echo "$d" >> "$GITHUB_PATH"
|
||||||
|
export PATH="$d:$PATH"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
gcc --version | head -1
|
||||||
|
|
||||||
|
- 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
|
||||||
|
# MinGW gcc on PATH (from setup-msys2)
|
||||||
|
gcc --version | head -1
|
||||||
|
# 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 "unit+CLI: PASS"
|
||||||
|
|
||||||
|
- name: hello smoke (MinGW + runtime_win)
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
unset BUX_DEBUG_FILE || true
|
||||||
|
export BUX_STDLIB="$PWD/lib"
|
||||||
|
# bootstrap on Windows always picks runtime_win.c
|
||||||
|
chmod +x tools/smoke_windows_hello.sh
|
||||||
|
tools/smoke_windows_hello.sh
|
||||||
|
echo "windows smoke: PASS (bootstrap + unit + CLI + hello)"
|
||||||
|
|
||||||
|
# ── 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"
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# Optional / not on every PR: bootstrap selfhost determinism (make selfhost-loop).
|
||||||
|
# Default PR CI is .github/workflows/ci.yml (`make test`).
|
||||||
|
# Fixed-point gen2 vs gen3: BUX_SELFHOST_FIXED_POINT=1 (workflow_dispatch input).
|
||||||
|
name: selfhost-loop
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
fixed_point:
|
||||||
|
description: "Also run fixed-point buxc2→buxc3→buxc4 (gen2 vs gen3)"
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
schedule:
|
||||||
|
# Sundays 06:00 UTC
|
||||||
|
- cron: "0 6 * * 0"
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- "src/**"
|
||||||
|
- "lib/**"
|
||||||
|
- "rt/**"
|
||||||
|
- "bootstrap/**"
|
||||||
|
- "tools/selfhost_loop.sh"
|
||||||
|
- "Makefile"
|
||||||
|
- ".github/workflows/selfhost-loop.yml"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: selfhost-loop-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
NIM_VERSION: "2.0.8"
|
||||||
|
NIM_INSTALL_DIR: ".nim_runtime"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
selfhost-loop:
|
||||||
|
name: bootstrap determinism
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 45
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
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
|
||||||
|
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
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y --no-install-recommends gcc make binutils libssl-dev
|
||||||
|
|
||||||
|
- name: Selfhost loop
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.fixed_point }}" = "true" ]; then
|
||||||
|
export BUX_SELFHOST_FIXED_POINT=1
|
||||||
|
echo "Fixed-point mode enabled (experimental)"
|
||||||
|
fi
|
||||||
|
make selfhost-loop
|
||||||
|
|
||||||
|
- name: Upload C on failure
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: selfhost-loop-c
|
||||||
|
path: |
|
||||||
|
build/selfhost-loop-a/build/main.c
|
||||||
|
build/selfhost-loop-b/build/main.c
|
||||||
|
if-no-files-found: ignore
|
||||||
+12
@@ -1,5 +1,6 @@
|
|||||||
# Compiled binary
|
# Compiled binary
|
||||||
buxc
|
buxc
|
||||||
|
buxc_lir
|
||||||
src/main
|
src/main
|
||||||
|
|
||||||
# Nim cache
|
# Nim cache
|
||||||
@@ -10,6 +11,7 @@ tests/lexer_test
|
|||||||
tests/parser_test
|
tests/parser_test
|
||||||
tests/sema_test
|
tests/sema_test
|
||||||
tests/hir_test
|
tests/hir_test
|
||||||
|
tests/borrow_test
|
||||||
tests/test_generics_parse
|
tests/test_generics_parse
|
||||||
tests/*.exe
|
tests/*.exe
|
||||||
|
|
||||||
@@ -21,3 +23,13 @@ build/
|
|||||||
_test_tmp_pkg/
|
_test_tmp_pkg/
|
||||||
test_pkg/
|
test_pkg/
|
||||||
examples_pkg/
|
examples_pkg/
|
||||||
|
|
||||||
|
# Temporary test directories
|
||||||
|
_test_*/
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
*.bak
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
*.log
|
||||||
|
.nim_runtime/
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Dimitar Gigov
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,42 +1,64 @@
|
|||||||
NIM := nim
|
NIM := nim
|
||||||
SRC := compiler/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 ctfe async concurrency os_time process json iter
|
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 ctfe_crc 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 move_field_remaining move_field_nested move_field_ptr move_cross_fn c_precedence macro_twice macro_repeat macro_nested macro_hygiene macro_unhygienic macro_stmt_pat macro_tt macro_tt_raw macro_type collections_extra
|
||||||
|
|
||||||
.PHONY: all build dev debug test clean clean-all test-examples selfhost
|
# Platform smoke (macOS CI): full EXAMPLES still runs on Linux.
|
||||||
|
EXAMPLES_SMOKE := hello ownership ownership_release strings map move_field move_field_partial move_field_remaining move_field_nested move_field_ptr move_cross_fn c_precedence macro_twice macro_repeat macro_nested macro_hygiene macro_unhygienic macro_stmt_pat macro_tt macro_tt_raw ctfe_crc
|
||||||
|
|
||||||
|
.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 test-linux-targets 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).
|
||||||
strip $(OUT)
|
$(OUT): $(wildcard bootstrap/*.nim)
|
||||||
|
$(NIM) c $(NIMFLAGS) -o:$(OUT) -d:release --opt:size $(SRC)
|
||||||
|
# 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 test-examples
|
# 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-linux-targets 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 compiler/tests/lexer_test.nim
|
$(NIM) c $(NIMFLAGS) -r tests/lexer_test.nim
|
||||||
@echo "Running parser tests..."
|
@echo "Running parser tests..."
|
||||||
$(NIM) c -r compiler/tests/parser_test.nim
|
$(NIM) c $(NIMFLAGS) -r tests/parser_test.nim
|
||||||
@echo "Running sema tests..."
|
@echo "Running sema tests..."
|
||||||
$(NIM) c -r compiler/tests/sema_test.nim
|
$(NIM) c $(NIMFLAGS) -r tests/sema_test.nim
|
||||||
@echo "Running HIR tests..."
|
@echo "Running HIR tests..."
|
||||||
$(NIM) c -r compiler/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 compiler/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)
|
||||||
@@ -62,14 +97,227 @@ clean:
|
|||||||
rm -rf _test_cast _test_cast2 _test_cast3 _test_channel
|
rm -rf _test_cast _test_cast2 _test_cast3 _test_channel
|
||||||
|
|
||||||
clean-all: clean
|
clean-all: clean
|
||||||
rm -rf _selfhost/src _selfhost/build
|
rm -rf build/selfhost build/selfhost-loop-a build/selfhost-loop-b build/selfhost-loop-c
|
||||||
|
rm -rf tests/golden/*/build
|
||||||
|
|
||||||
selfhost: build
|
selfhost: ensure-buxc
|
||||||
@echo "=== Building self-hosted compiler ==="
|
@echo "=== Building self-hosted compiler ==="
|
||||||
@rm -rf _selfhost/src _selfhost/build
|
@rm -rf build/selfhost
|
||||||
@mkdir -p _selfhost/src
|
@mkdir -p build/selfhost/src
|
||||||
@cp compiler/selfhost/*.bux _selfhost/src/
|
@cp src/*.bux build/selfhost/src/
|
||||||
@mv _selfhost/src/main.bux _selfhost/src/Main.bux 2>/dev/null || true
|
@cp src/bux.toml build/selfhost/
|
||||||
@cd _selfhost && ../$(OUT) build
|
@mv build/selfhost/src/main.bux build/selfhost/src/Main.bux 2>/dev/null || true
|
||||||
@strip _selfhost/build/buxc2
|
@cd build/selfhost && ../../$(OUT) build
|
||||||
|
# strip removed for debug
|
||||||
@echo "=== Self-hosted compiler built successfully ==="
|
@echo "=== Self-hosted compiler built successfully ==="
|
||||||
|
|
||||||
|
.PHONY: test-golden
|
||||||
|
|
||||||
|
GOLDEN_TESTS := hello fibonacci structs generics algebraic_enums enums methods strings modern_features
|
||||||
|
|
||||||
|
test-golden: ensure-buxc
|
||||||
|
@echo "=== Golden tests ==="
|
||||||
|
@passed=0; failed=0; \
|
||||||
|
for test in $(GOLDEN_TESTS); do \
|
||||||
|
gd="tests/golden/$$test"; \
|
||||||
|
if [ ! -d "$$gd" ]; then echo " SKIP $$test (no dir)"; continue; fi; \
|
||||||
|
if [ ! -f "$$gd/expected.c" ]; then echo " SKIP $$test (no expected.c)"; continue; fi; \
|
||||||
|
rm -rf "$$gd/build"; \
|
||||||
|
(cd "$$gd" && ../../../$(OUT) build > /dev/null 2>&1); \
|
||||||
|
if diff "$$gd/build/main.c" "$$gd/expected.c" > /dev/null 2>&1; then \
|
||||||
|
echo " PASS $$test"; \
|
||||||
|
passed=$$((passed + 1)); \
|
||||||
|
else \
|
||||||
|
echo " FAIL $$test — C output differs from expected"; \
|
||||||
|
failed=$$((failed + 1)); \
|
||||||
|
fi; \
|
||||||
|
done; \
|
||||||
|
echo "Golden tests: $$passed passed, $$failed failed"; \
|
||||||
|
if [ $$failed -gt 0 ]; then exit 1; fi
|
||||||
|
|
||||||
|
test-errors: ensure-buxc
|
||||||
|
@echo "=== Error diagnostic golden tests ==="
|
||||||
|
@chmod +x tests/error_golden/run.sh
|
||||||
|
@tests/error_golden/run.sh ./$(OUT)
|
||||||
|
|
||||||
|
test-stdlib: ensure-buxc
|
||||||
|
@echo "=== Stdlib golden tests ==="
|
||||||
|
@chmod +x tests/stdlib_golden/run.sh
|
||||||
|
@tests/stdlib_golden/run.sh ./$(OUT)
|
||||||
|
|
||||||
|
# Generate stdlib API docs from /// comments → docs/api/stdlib.md
|
||||||
|
docs: ensure-buxc
|
||||||
|
@mkdir -p docs/api
|
||||||
|
@./$(OUT) doc --out docs/api/stdlib.md lib/
|
||||||
|
@echo "docs/api/stdlib.md updated"
|
||||||
|
|
||||||
|
# CI: full-tree format check (lib / examples / src / tests / apps) + dirty-path smoke.
|
||||||
|
fmt-check: ensure-buxc
|
||||||
|
@echo "=== fmt --check (full tree) ==="
|
||||||
|
@./$(OUT) fmt --check lib/
|
||||||
|
@./$(OUT) fmt --check examples/
|
||||||
|
@./$(OUT) fmt --check src/
|
||||||
|
@./$(OUT) fmt --check tests/
|
||||||
|
@./$(OUT) fmt --check apps/
|
||||||
|
@echo "=== fmt --check dirty-path smoke ==="
|
||||||
|
@mkdir -p /tmp/bux_fmt_smoke
|
||||||
|
@printf 'func Main() -> int {\nreturn 0;\n}\n' > /tmp/bux_fmt_smoke/bad.bux
|
||||||
|
@if ./$(OUT) fmt --check /tmp/bux_fmt_smoke/bad.bux >/dev/null 2>&1; then \
|
||||||
|
echo "error: expected --check to fail on dirty file"; exit 1; \
|
||||||
|
fi
|
||||||
|
@echo "fmt --check passed (tree clean + dirty exits 1)"
|
||||||
|
|
||||||
|
# One-shot reformat of the same trees (run before committing style-only fixes)
|
||||||
|
.PHONY: fmt
|
||||||
|
fmt: ensure-buxc
|
||||||
|
@./$(OUT) fmt lib/
|
||||||
|
@./$(OUT) fmt examples/
|
||||||
|
@./$(OUT) fmt src/
|
||||||
|
@./$(OUT) fmt tests/
|
||||||
|
@./$(OUT) fmt apps/
|
||||||
|
@echo "Formatted lib/ examples/ src/ tests/ apps/"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
selfhost-loop: ensure-buxc
|
||||||
|
@chmod +x tools/selfhost_loop.sh
|
||||||
|
@tools/selfhost_loop.sh
|
||||||
|
|
||||||
|
lsp: tools/bux-lsp
|
||||||
|
@echo "LSP server ready at tools/bux-lsp"
|
||||||
|
|
||||||
|
tools/bux-lsp: tools/lsp_server.nim bootstrap/*.nim
|
||||||
|
cd tools && $(NIM) c -d:release --opt:size --path:../bootstrap -o:bux-lsp lsp_server.nim
|
||||||
|
|
||||||
|
.PHONY: test-lsp
|
||||||
|
test-lsp: lsp
|
||||||
|
@echo "=== LSP unit (locals / inference) ==="
|
||||||
|
$(NIM) r --path:bootstrap tools/test_lsp_locals.nim
|
||||||
|
@echo "=== LSP hover smoke ==="
|
||||||
|
@chmod +x tools/smoke_lsp_hover.sh
|
||||||
|
@tools/smoke_lsp_hover.sh
|
||||||
|
@echo "=== LSP references / rename smoke ==="
|
||||||
|
@chmod +x tools/smoke_lsp_rename.sh
|
||||||
|
@tools/smoke_lsp_rename.sh
|
||||||
|
@echo "=== LSP workspace/symbol smoke ==="
|
||||||
|
@chmod +x tools/smoke_lsp_workspace.sh
|
||||||
|
@tools/smoke_lsp_workspace.sh
|
||||||
|
@echo "=== LSP deeper rename smoke ==="
|
||||||
|
@chmod +x tools/smoke_lsp_rename_deep.sh
|
||||||
|
@tools/smoke_lsp_rename_deep.sh
|
||||||
|
@echo "=== LSP call hierarchy smoke ==="
|
||||||
|
@chmod +x tools/smoke_lsp_call_hierarchy.sh
|
||||||
|
@tools/smoke_lsp_call_hierarchy.sh
|
||||||
|
@echo "=== LSP method call hierarchy smoke ==="
|
||||||
|
@chmod +x tools/smoke_lsp_method_hierarchy.sh
|
||||||
|
@tools/smoke_lsp_method_hierarchy.sh
|
||||||
|
@echo "=== LSP method/type/receiver rename smoke ==="
|
||||||
|
@chmod +x tools/smoke_lsp_rename_method.sh
|
||||||
|
@tools/smoke_lsp_rename_method.sh
|
||||||
|
@echo "=== LSP interface dispatch hierarchy smoke ==="
|
||||||
|
@chmod +x tools/smoke_lsp_iface_hierarchy.sh
|
||||||
|
@tools/smoke_lsp_iface_hierarchy.sh
|
||||||
|
@echo "==> LSP path rename smoke"
|
||||||
|
@chmod +x tools/smoke_lsp_rename_path.sh
|
||||||
|
@tools/smoke_lsp_rename_path.sh
|
||||||
|
@echo "==> LSP implementation smoke"
|
||||||
|
@chmod +x tools/smoke_lsp_implementation.sh
|
||||||
|
@tools/smoke_lsp_implementation.sh
|
||||||
|
@echo "==> LSP workspace import index smoke"
|
||||||
|
@chmod +x tools/smoke_lsp_workspace_imports.sh
|
||||||
|
@tools/smoke_lsp_workspace_imports.sh
|
||||||
|
@echo "==> LSP type hierarchy smoke"
|
||||||
|
@chmod +x 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
|
||||||
|
test-registry: ensure-buxc
|
||||||
|
@echo "=== Registry smoke (E.1 + HTTP) ==="
|
||||||
|
@chmod +x tools/smoke_registry.sh
|
||||||
|
@tools/smoke_registry.sh
|
||||||
|
|
||||||
|
# E.2 — build showcase apps + simpledb/jwt CLI smoke
|
||||||
|
.PHONY: test-apps
|
||||||
|
test-apps: ensure-buxc
|
||||||
|
@echo "=== Apps smoke (E.2) ==="
|
||||||
|
@chmod +x tools/smoke_apps.sh
|
||||||
|
@tools/smoke_apps.sh
|
||||||
|
|
||||||
|
# E.5 — micro-benchmarks (Bux + C/Nim/Zig twins)
|
||||||
|
.PHONY: bench
|
||||||
|
bench: ensure-buxc
|
||||||
|
@chmod +x tools/bench.sh
|
||||||
|
@tools/bench.sh
|
||||||
|
|
||||||
|
# E.5 — Nexus HTTP throughput (wrk); optional via BENCH_NEXUS=1 make bench
|
||||||
|
.PHONY: bench-nexus
|
||||||
|
bench-nexus: ensure-buxc
|
||||||
|
@chmod +x tools/bench_nexus.sh
|
||||||
|
@tools/bench_nexus.sh
|
||||||
|
|
||||||
|
# E.4 — DWARF / #line debugger smoke
|
||||||
|
.PHONY: test-dwarf
|
||||||
|
test-dwarf: ensure-buxc
|
||||||
|
@echo "=== DWARF / #line smoke (E.4) ==="
|
||||||
|
@chmod +x tools/smoke_dwarf.sh
|
||||||
|
@tools/smoke_dwarf.sh
|
||||||
|
|
||||||
|
# Session 75/85 — Linux / cloud / embedded: minimal, static, aarch64+riscv64 cross, CTFE CRC
|
||||||
|
.PHONY: test-linux-targets
|
||||||
|
test-linux-targets: ensure-buxc
|
||||||
|
@echo "=== Linux targets smoke (minimal / static / aarch64+riscv64 cross) ==="
|
||||||
|
@chmod +x tools/smoke_linux_targets.sh
|
||||||
|
@tools/smoke_linux_targets.sh
|
||||||
|
|
||||||
|
# Session 78 — Nexus HTTPS (self-signed) smoke
|
||||||
|
.PHONY: test-nexus-tls
|
||||||
|
test-nexus-tls: ensure-buxc
|
||||||
|
@echo "=== Nexus TLS smoke ==="
|
||||||
|
@chmod +x tools/smoke_nexus_tls.sh
|
||||||
|
@tools/smoke_nexus_tls.sh
|
||||||
|
|
||||||
|
# Session 79 — musl static (SKIP if no musl-gcc/zig)
|
||||||
|
.PHONY: test-musl-static
|
||||||
|
test-musl-static: ensure-buxc
|
||||||
|
@echo "=== musl static smoke ==="
|
||||||
|
@chmod +x tools/smoke_musl_static.sh
|
||||||
|
@tools/smoke_musl_static.sh
|
||||||
|
|
||||||
|
# Session 80 — selfhost install --locked + Nexus mTLS
|
||||||
|
.PHONY: test-selfhost-install test-nexus-mtls test-selfhost-registry
|
||||||
|
test-selfhost-install: ensure-buxc selfhost
|
||||||
|
@echo "=== Selfhost install --locked ==="
|
||||||
|
@chmod +x tools/smoke_selfhost_install.sh
|
||||||
|
@tools/smoke_selfhost_install.sh
|
||||||
|
|
||||||
|
test-nexus-mtls: ensure-buxc
|
||||||
|
@echo "=== Nexus mTLS smoke ==="
|
||||||
|
@chmod +x tools/smoke_nexus_mtls.sh
|
||||||
|
@tools/smoke_nexus_mtls.sh
|
||||||
|
|
||||||
|
# Session 81 — selfhost full registry (search / add / HTTP)
|
||||||
|
test-selfhost-registry: ensure-buxc selfhost
|
||||||
|
@echo "=== Selfhost registry ==="
|
||||||
|
@chmod +x tools/smoke_selfhost_registry.sh
|
||||||
|
@tools/smoke_selfhost_registry.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)
|
||||||
|
# When BUX_SKIP_BUILD=1, reuse prebuilt buxc; still builds buxc2 via selfhost.
|
||||||
|
.PHONY: test-selfhost-smoke
|
||||||
|
test-selfhost-smoke: ensure-buxc selfhost
|
||||||
|
@echo "=== Selfhost smoke (move_field + multi-file #line) ==="
|
||||||
|
@chmod +x tools/smoke_selfhost.sh
|
||||||
|
@tools/smoke_selfhost.sh
|
||||||
|
@echo "=== Graft / quote hygiene smoke ==="
|
||||||
|
@chmod +x tools/smoke_graft_hygiene.sh
|
||||||
|
@tools/smoke_graft_hygiene.sh
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
# Bux Programming Language — Roadmap to Self-Hosting
|
# Bux Programming Language — Roadmap to v1.0.0
|
||||||
|
|
||||||
> **Bootstrap Implementation:** Nim
|
> **Version:** 0.3.1 (2026-06-06)
|
||||||
> **Target:** Bux compiler written in Bux (self-hosting)
|
> **Bootstrap:** Nim (`bootstrap/`) — compiles `src/` → `buxc`
|
||||||
|
> **Self-host:** Bux (`src/`) — compiles via `buxc` → `buxc2`
|
||||||
|
> **Target:** Bux v1.0.0 — fully self-hosting, gradual ownership, tooling
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Bux is a fast, compiled, strongly-typed, multi-paradigm systems programming language. The strategy is **bootstrap via Nim** — we build the first Bux compiler in Nim, then progressively rewrite it in Bux until it compiles itself.
|
Bux is a fast, compiled, strongly-typed, multi-paradigm systems programming language. The strategy is **bootstrap via Nim** — we built the first Bux compiler in Nim, then rewrote it in Bux. The Nim bootstrap now exists only as a build scaffold.
|
||||||
|
|
||||||
**Core philosophy:** Systems-level control with modern ergonomics. No hidden costs, no hidden allocations, no hidden control flow.
|
**Core philosophy:** Systems-level control with modern ergonomics. No hidden costs, no hidden allocations, no hidden control flow.
|
||||||
|
|
||||||
|
**Killer feature:** Gradual ownership — write fast like C, add safety like Rust, but only where you choose (`@[Checked]`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Language Design Goals (Bux vs Rust vs Nim vs Zig)
|
## Language Design Goals (Bux vs Rust vs Nim vs Zig)
|
||||||
@@ -286,11 +290,11 @@ Phase 7.5 — Driver (depends on all):
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 7 — Self-Hosting: The Great Rewrite 🔄 (In Progress)
|
## Phase 7 — Self-Hosting: The Great Rewrite ✅ (Complete)
|
||||||
|
|
||||||
**Goal:** Bux compiler compiles itself. This is the **main milestone**.
|
**Goal:** Bux compiler compiles itself. This is the **main milestone**.
|
||||||
|
|
||||||
**All 14 modules ported** in `compiler/selfhost/` (4094 LOC total). Self-hosted project structure in `_selfhost/`.
|
**All 14 modules ported** in `src/` (4094 LOC total). Built via `make selfhost`.
|
||||||
|
|
||||||
| Task | Status | Details | LOC |
|
| Task | Status | Details | LOC |
|
||||||
|------|--------|---------|-----|
|
|------|--------|---------|-----|
|
||||||
@@ -342,7 +346,7 @@ Phase 7.5 — Driver (depends on all):
|
|||||||
|
|
||||||
**Self-hosted compiler stats:**
|
**Self-hosted compiler stats:**
|
||||||
```
|
```
|
||||||
$ _selfhost/build/buxc2 version
|
$ src/build/buxc2 version
|
||||||
Bux 0.2.0 (self-hosting bootstrap)
|
Bux 0.2.0 (self-hosting bootstrap)
|
||||||
Pipeline modules:
|
Pipeline modules:
|
||||||
Lexer ✅ 695 lines
|
Lexer ✅ 695 lines
|
||||||
@@ -355,8 +359,8 @@ Pipeline modules:
|
|||||||
|
|
||||||
**Bootstrap loop goal:**
|
**Bootstrap loop goal:**
|
||||||
```
|
```
|
||||||
buxc (Nim) → compile compiler/selfhost/*.bux → buxc2 (Bux binary)
|
buxc (Nim) → compile src/*.bux → buxc2 (Bux binary)
|
||||||
buxc2 (Bux) → compile compiler/selfhost/*.bux → buxc3 (Bux binary)
|
buxc2 (Bux) → compile src/*.bux → buxc3 (Bux binary)
|
||||||
compare buxc2 == buxc3 → SELF-HOSTED ✅
|
compare buxc2 == buxc3 → SELF-HOSTED ✅
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -428,7 +432,7 @@ func ReadFile(path: String) -> Result<String, IoError> {
|
|||||||
| `8.2.1` `own` keyword | ✅ | `own T` parsed and resolves to `T`; ready for borrow checker integration |
|
| `8.2.1` `own` keyword | ✅ | `own T` parsed and resolves to `T`; ready for borrow checker integration |
|
||||||
| `8.2.2` `borrow` / `&` | ✅ | `&T` shared reference type checked and enforced |
|
| `8.2.2` `borrow` / `&` | ✅ | `&T` shared reference type checked and enforced |
|
||||||
| `8.2.3` `mut` references | ✅ | `&mut T` mutable reference type checked and enforced |
|
| `8.2.3` `mut` references | ✅ | `&mut T` mutable reference type checked and enforced |
|
||||||
| `8.2.4` Lifetime elision | ⏳ | Simple rules for common cases; explicit `'a` for complex |
|
| `8.2.4` Lifetime elision | ✅ | Single-input elision + dangling return; explicit `'a` for multi-input |
|
||||||
| `8.2.5` Opt-in checker | ✅ | `@[Checked]` attribute enables borrow checking: writes through `&T` are rejected |
|
| `8.2.5` Opt-in checker | ✅ | `@[Checked]` attribute enables borrow checking: writes through `&T` are rejected |
|
||||||
|
|
||||||
```bux
|
```bux
|
||||||
@@ -516,6 +520,34 @@ const TABLE_SIZE = Factorial(10); // Computed at compile time
|
|||||||
| `8.6.2` Procedural macros | `#[derive(Clone)]`, `#[derive(Debug)]` |
|
| `8.6.2` Procedural macros | `#[derive(Clone)]`, `#[derive(Debug)]` |
|
||||||
| `8.6.3` Reflection | Compile-time type introspection for serialization |
|
| `8.6.3` Reflection | Compile-time type introspection for serialization |
|
||||||
|
|
||||||
|
### 8.7 — Optimization & Release Mode
|
||||||
|
|
||||||
|
**Goal:** C-speed for hot paths. `@[Release]` disables all runtime checks and inlines auto-drop.
|
||||||
|
|
||||||
|
| Task | Status | Details |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `8.7.1` `@[Release]` attribute | ⏳ | Disables bounds checking, borrow checking, null checks inside the function |
|
||||||
|
| `8.7.2` Auto-drop inline | ⏳ | Emit `free`/`close` directly instead of `Type_Drop(&var)` call |
|
||||||
|
| `8.7.3` Dead-store elimination | ⏳ | C backend removes redundant temp variables (`_t1`, `_t2`) |
|
||||||
|
| `8.7.4` `-O3` by default | ⏳ | `bux build --release` passes `-O3 -flto` to C compiler |
|
||||||
|
| `8.7.5` Profile-guided optimization | ⏳ | `bux profile` + `bux build --pgo` for guided inlining |
|
||||||
|
|
||||||
|
```bux
|
||||||
|
// Default: safe but slower — bounds checks + borrow checks
|
||||||
|
@[Checked]
|
||||||
|
func SafeSum(arr: Array<int>) -> int { ... }
|
||||||
|
|
||||||
|
// Hot path: zero-cost — straight C
|
||||||
|
@[Release]
|
||||||
|
func HotLoop(data: *float64, n: int) {
|
||||||
|
for i in 0..n {
|
||||||
|
data[i] = data[i] * 2.0; // no bounds check, inlined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this matters:** Nim compiles to C but has ARC/ORC overhead. Crystal has Boehm GC. Bux with `@[Release]` has **literally zero overhead** — the generated C is hand-written quality.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 9 — Ecosystem & Tooling (Week 35+)
|
## Phase 9 — Ecosystem & Tooling (Week 35+)
|
||||||
@@ -534,60 +566,188 @@ const TABLE_SIZE = Factorial(10); // Computed at compile time
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## File Structure (Target)
|
## File Structure (v0.3.0 — Current)
|
||||||
|
|
||||||
```
|
```
|
||||||
bux/
|
bux/
|
||||||
├── bux.toml # Compiler package manifest
|
├── bux.toml # Bootstrap compiler manifest (v0.3.0)
|
||||||
├── README.md
|
├── README.md
|
||||||
├── PLAN.md
|
├── PLAN.md # This file
|
||||||
├── Makefile # build, test, selfhost
|
├── Makefile # build, test, selfhost
|
||||||
├── src/
|
├── src/ # 🎯 CANONICAL: Bux compiler source
|
||||||
│ ├── Main.bux # CLI entry point
|
│ ├── bux.toml # Self-host compiler manifest
|
||||||
│ ├── Lexer.bux
|
│ ├── main.bux # Entry point (renamed → Main.bux at build)
|
||||||
│ ├── Parser.bux
|
│ ├── lexer.bux # Tokenizer (UTF-8 state machine, 697 LOC)
|
||||||
│ ├── Ast.bux
|
│ ├── parser.bux # Pratt parser (1004 LOC)
|
||||||
│ ├── Sema.bux
|
│ ├── ast.bux # AST node types (algebraic enums)
|
||||||
│ ├── Type.bux
|
│ ├── sema.bux # Type checker / semantic analysis (393 LOC)
|
||||||
│ ├── Hir.bux
|
│ ├── types.bux # Type factories
|
||||||
│ ├── Lir.bux
|
│ ├── scope.bux # Symbol table
|
||||||
│ ├── CBackend.bux # C transpiler (primary backend)
|
│ ├── hir.bux # High-level IR definitions
|
||||||
│ ├── X64Backend.bux # Native x86-64 backend (optional)
|
│ ├── hir_lower.bux # AST → HIR lowering (307 LOC)
|
||||||
│ ├── Linker.bux # Custom linker / build driver
|
│ ├── c_backend.bux # HIR → C code generator (264 LOC)
|
||||||
│ ├── Manifest.bux # bux.toml parser
|
│ ├── cli.bux # CLI command dispatch
|
||||||
│ └── Package.bux # Package resolution
|
│ ├── manifest.bux # bux.toml / TOML parser
|
||||||
├── library/
|
│ ├── token.bux # Token kind definitions
|
||||||
│ ├── Std/
|
│ └── source_location.bux # Source location tracking
|
||||||
│ │ ├── Io.bux
|
├── bootstrap/ # 🔧 Nim bootstrap (build scaffold only)
|
||||||
│ │ ├── Memory.bux
|
│ ├── main.nim # Entry point
|
||||||
│ │ ├── String.bux
|
│ ├── cli.nim # CLI commands + build driver
|
||||||
│ │ ├── Array.bux
|
│ └── ... # (mirrors src/ structure)
|
||||||
│ │ ├── Map.bux
|
├── lib/ # 📦 Standard library (23 modules)
|
||||||
│ │ ├── Math.bux
|
│ ├── Io.bux # Print, ReadFile, WriteFile
|
||||||
│ │ ├── Os.bux
|
│ ├── String.bux # Full string API (len, concat, split, format...)
|
||||||
│ │ ├── Path.bux
|
│ ├── Array.bux # Generic Array<T>
|
||||||
│ │ ├── Process.bux
|
│ ├── Map.bux # Generic Map<K,V> + StringMap
|
||||||
│ │ ├── Result.bux # Result<T,E> and Option<T>
|
│ ├── Set.bux # Generic Set<T>
|
||||||
│ │ ├── Iter.bux # Iterator trait and combinators
|
│ ├── Math.bux # Sqrt, Pow, Min, Max, Abs
|
||||||
│ │ ├── Fmt.bux # String formatting
|
│ ├── Mem.bux # Alloc, Realloc, Free
|
||||||
│ │ ├── Task.bux # Lightweight concurrency
|
│ ├── Path.bux # Path_Join, Path_Parent, Path_Ext
|
||||||
│ │ ├── Channel.bux # Message passing
|
│ ├── Fs.bux # DirExists, Mkdir, ListDir
|
||||||
│ │ └── Sync.bux # Mutex, RwLock, atomic
|
│ ├── Task.bux # Lightweight tasks (spawn/await)
|
||||||
│ └── Runtime.c # C runtime shim
|
│ ├── Channel.bux # Producer/consumer channels
|
||||||
├── tests/
|
│ ├── Sync.bux # Mutex, RwLock
|
||||||
│ ├── Lexer/
|
│ ├── Result.bux # Result<T,E> + Option<T> + ? operator
|
||||||
│ ├── Parser/
|
│ ├── Iter.bux # Iterator trait
|
||||||
│ ├── Sema/
|
│ ├── Fmt.bux # String formatting
|
||||||
│ ├── Codegen/
|
│ ├── Os.bux # Args, Env, Exit, Cwd
|
||||||
│ └── Integration/
|
│ ├── Time.bux # Time measurement
|
||||||
└── docs/
|
│ ├── Process.bux # Subprocess spawning
|
||||||
├── LanguageRef.md
|
│ ├── Net.bux # TCP client/server
|
||||||
├── Ownership.md
|
│ ├── Crypto.bux # SHA256, HMAC, Base64
|
||||||
└── Concurrency.md
|
│ ├── Json.bux # JSON parse/stringify
|
||||||
|
│ └── Test.bux # Test framework
|
||||||
|
├── rt/ # ⚙️ C runtime
|
||||||
|
│ ├── runtime.c # Memory, string, path helpers
|
||||||
|
│ └── io.c # File I/O wrappers
|
||||||
|
├── tests/ # 🧪 Unit tests (Nim)
|
||||||
|
│ ├── lexer_test.nim
|
||||||
|
│ ├── parser_test.nim
|
||||||
|
│ ├── sema_test.nim
|
||||||
|
│ ├── hir_test.nim
|
||||||
|
│ ├── borrow_test.nim
|
||||||
|
│ └── testdata/
|
||||||
|
├── examples/ # Example programs
|
||||||
|
├── docs/ # Documentation
|
||||||
|
│ ├── LanguageRef.md
|
||||||
|
│ ├── BuildAndTest.md
|
||||||
|
│ ├── STRATEGY.md
|
||||||
|
│ ├── PHASE8_STRATEGY.md
|
||||||
|
│ └── Packages.md
|
||||||
|
├── build/ # Build artifacts (gitignored)
|
||||||
|
│ └── selfhost/ # Self-host compiler build dir
|
||||||
|
└── vendor/ # Vendored dependencies
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Phase 10 🔄 — Path to v1.0.0 (In Progress)
|
||||||
|
|
||||||
|
### 10.0 — v0.3.0 Restructuring ✅ (Completed 2026-06-06)
|
||||||
|
|
||||||
|
| Task | Status | Details |
|
||||||
|
|------|--------|---------|
|
||||||
|
| Directory restructure | ✅ | `compiler/selfhost/` → `src/`, `compiler/bootstrap/` → `bootstrap/`, `library/std/` → `lib/`, `library/runtime/` → `rt/`, `compiler/tests/` → `tests/` |
|
||||||
|
| Path updates | ✅ | Updated Makefile, cli.nim, cli.bux, test files, docs |
|
||||||
|
| Selfhost fix | ✅ | Build via `build/selfhost/` (project wrapper) |
|
||||||
|
| Push to GitHub | ✅ | `ac969b3` — v0.3.0 restructure |
|
||||||
|
|
||||||
|
### 10.1 — Selfhost Loop ✅ (v0.5.0 target)
|
||||||
|
|
||||||
|
**Goal:** `buxc2` can compile itself producing a binary-identical `buxc3`.
|
||||||
|
|
||||||
|
```
|
||||||
|
buxc (Nim) → src/*.bux → buxc2 ✅
|
||||||
|
buxc2 → src/*.bux → buxc3 ✅
|
||||||
|
buxc2 == buxc3 ✅ (binary-identical)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Task | Status | Details |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `10.1.1` Verify buxc2 can build src/ | ✅ | `buxc2 build` works on selfhost project |
|
||||||
|
| `10.1.2` Deterministic C codegen | ✅ | C output identical on every iteration |
|
||||||
|
| `10.1.3` Binary-identical loop | ✅ | `make selfhost-loop` passes (C + ELF parity) |
|
||||||
|
| `10.1.4` Remove hardcoded paths | ✅ | Paths resolved via `bux_getcwd()` / `bux_path_join()` |
|
||||||
|
| `10.1.5` Selfhost test in CI | ⏳ | Add to `make test` |
|
||||||
|
|
||||||
|
### 10.2 — Gradual Ownership ✅ (v0.5.0 target) ⭐ Killer Feature
|
||||||
|
|
||||||
|
**Goal:** `@[Checked]` functions get full borrow checking. `@[Release]` disables checks for zero-cost hot paths.
|
||||||
|
|
||||||
|
| Task | Status | Details |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `10.2.1` `@[Checked]` attribute gate | ✅ | Enable/disable borrow checker per function |
|
||||||
|
| `10.2.2` `&T` shared reference check | ✅ | No mutation through shared refs |
|
||||||
|
| `10.2.3` `&mut T` exclusive mutable check | ✅ | No aliasing of mutable refs |
|
||||||
|
| `10.2.4` Bounds checking on slices | ✅ | `Slice_Get` / `Array_Get` with `bux_bounds_check` |
|
||||||
|
| `10.2.5` `@[Release]` zero-cost mode | ✅ | Disables borrow + bounds checks, passes `-O3 -flto` |
|
||||||
|
| `10.2.6` Lifetime elision (simple rules) | ✅ | Single-input elision; multi-input requires `'a` |
|
||||||
|
| `10.2.7` Explicit lifetimes `'a` | ✅ | Parsed + checked; multi-input + mismatch |
|
||||||
|
|
||||||
|
### 10.3 — Compiler Architecture Upgrade (v0.6.0 target)
|
||||||
|
|
||||||
|
**Goal:** Proper module structure instead of flat file mirror of Nim bootstrap.
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main.bux
|
||||||
|
├── frontend/
|
||||||
|
│ ├── lexer.bux
|
||||||
|
│ ├── parser.bux
|
||||||
|
│ └── ast.bux
|
||||||
|
├── analysis/
|
||||||
|
│ ├── sema.bux
|
||||||
|
│ ├── types.bux
|
||||||
|
│ ├── scope.bux
|
||||||
|
│ └── borrow.bux # NEW — borrow checker
|
||||||
|
├── lowering/
|
||||||
|
│ ├── hir.bux
|
||||||
|
│ └── hir_lower.bux
|
||||||
|
├── backend/
|
||||||
|
│ └── c_backend.bux
|
||||||
|
└── driver/
|
||||||
|
├── cli.bux
|
||||||
|
├── manifest.bux
|
||||||
|
├── token.bux
|
||||||
|
└── source_location.bux
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.4 — Stdlib Completion (v0.7.0 target)
|
||||||
|
|
||||||
|
| Module | Status | Priority |
|
||||||
|
|--------|--------|----------|
|
||||||
|
| `Std::Os` — Args, Env, Exit, Cwd | ⏳ | P0 |
|
||||||
|
| `Std::Process` — spawn subprocess | ⏳ | P0 |
|
||||||
|
| `Std::Iter` — map, filter, fold | ⏳ | P1 |
|
||||||
|
| `Std::Fmt` — string interpolation | ⏳ | P1 |
|
||||||
|
|
||||||
|
### 10.5 — Tooling (v0.8.0 target)
|
||||||
|
|
||||||
|
| Tool | Status | Priority |
|
||||||
|
|------|--------|----------|
|
||||||
|
| `bux test` — test runner | ⏳ | P0 |
|
||||||
|
| `bux fmt` — code formatter | ⏳ | P1 |
|
||||||
|
| `bux doc` — doc generator | ⏳ | P2 |
|
||||||
|
|
||||||
|
### 10.6 — Native Backend (v0.9.0 target)
|
||||||
|
|
||||||
|
| Task | Status |
|
||||||
|
|------|--------|
|
||||||
|
| Direct x86-64 codegen (no C) | ⏳ |
|
||||||
|
| ELF64 output | ⏳ |
|
||||||
|
| System V AMD64 ABI | ⏳ |
|
||||||
|
|
||||||
|
### 10.7 — v1.0.0 Release Criteria
|
||||||
|
|
||||||
|
- [ ] Compiler self-hosts binary-identically
|
||||||
|
- [ ] `@[Checked]` borrow checker works on real code
|
||||||
|
- [ ] Stdlib complete enough for CLI tools
|
||||||
|
- [ ] `bux test`, `bux fmt`, `bux doc` exist
|
||||||
|
- [ ] Documentation + 30+ examples
|
||||||
|
- [ ] CI/CD pipeline (build + test on push)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Language Design Decisions (Bux Improvements)
|
## Language Design Decisions (Bux Improvements)
|
||||||
|
|
||||||
### What Bux learns from Rust
|
### What Bux learns from Rust
|
||||||
@@ -689,12 +849,18 @@ func Main() -> int {
|
|||||||
| **M2** | 2 | ✅ | Type-checker rejects invalid programs |
|
| **M2** | 2 | ✅ | Type-checker rejects invalid programs |
|
||||||
| **M3** | 3 | ✅ | HIR lowering works for all constructs |
|
| **M3** | 3 | ✅ | HIR lowering works for all constructs |
|
||||||
| **M4** | 5A | ✅ | `bux run` produces working binary via C transpiler |
|
| **M4** | 5A | ✅ | `bux run` produces working binary via C transpiler |
|
||||||
| **M5** | 6 | ✅ | Can write compiler-adjacent tools in Bux (18 examples) |
|
| **M5** | 6 | ✅ | Can write compiler-adjacent tools in Bux (26 examples) |
|
||||||
| **M6** | 7 | ✅ | **Self-hosted**: `buxc2` (Bux) compiles via `buxc` (Nim) — 88KB working binary |
|
| **M6** | 7 | ✅ | **Self-hosted**: `buxc2` (Bux) compiles via `buxc` (Nim) — working binary |
|
||||||
| **M7** | 8 | ✅ | Result/Option/`?`/`!` done; **borrow checker working**; **CTFE working** |
|
| **M7** | 8 | ✅ | Result/Option/`?`/`!` done; **borrow checker working**; **CTFE working** |
|
||||||
| **M8** | 8-9 | ✅ | **Borrow checker**, **CTFE**, **Package manager** working |
|
| **M8** | 8-9 | ✅ | **Borrow checker**, **CTFE**, **Package manager** working |
|
||||||
| **M9** | 8.5 | ✅ | **Trait bounds** (`<T: Comparable>`) — semantic checking implemented |
|
| **M9** | 8.5 | ✅ | **Trait bounds** (`<T: Comparable>`) — semantic checking implemented |
|
||||||
| **M8** | 9 | ⏳ | Package manager + LSP + formatter shipped |
|
| **M10** | 10 | ✅ | **LIR backend** replaces HIR→C; 26/26 examples pass; selfhost builds |
|
||||||
|
| **M11** | 11 | ✅ | **Selfhost loop**: `buxc2` compiles itself → binary-identical `buxc3` |
|
||||||
|
| **M12** | 11.3 | ✅ | `@[Checked]` borrow checker works on real code |
|
||||||
|
| **M13** | 11.4 | ✅ | `@[Release]` zero-cost mode; Rust-style error snippets |
|
||||||
|
| **M14** | 8.3 | ✅ | **Green threads** — M:N scheduler with channels |
|
||||||
|
| **M15** | 11.5 | ⏳ | Native x86-64 backend (no C transpiler) |
|
||||||
|
| **M16** | 11.4 | ⏳ | `bux test`, `bux fmt`, `bux doc` shipped |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -752,11 +918,58 @@ func Main() -> int {
|
|||||||
|
|
||||||
### Next Actions (Priority Order)
|
### Next Actions (Priority Order)
|
||||||
|
|
||||||
1. **Fix parameter/return types** — `String*` instead of `int` in function signatures
|
1. ✅ **Fix LIR backend type inference** — struct temps, undeclared vars, break/continue, duplicate declarations
|
||||||
2. **Debug ast/lexer/parser** — Get all 14 modules passing check
|
2. ✅ **All 30 examples passing** — bootstrap and self-host compilers build and run every example
|
||||||
3. **Full 14-module project build** — Complete bootstrap loop: buxc3 produced by buxc2
|
3. ✅ **Selfhost build working** — `make selfhost` produces working `buxc2`
|
||||||
4. **Compare buxc2 vs buxc3 output** — True self-hosting verification
|
4. ✅ **Selfhost loop verification** — `buxc2` compiles `src/` → `buxc3`; C output and stripped binary identical
|
||||||
5. **Phase 8** — Advanced features: ownership checker, CTFE evaluation, string interpolation
|
5. ✅ **Golden tests for C codegen** — `make test-golden` passes for 8 critical examples
|
||||||
|
6. ✅ **`bux test` runner** — discovers `tests/*.bux`, builds each as a temp package, and reports PASS/FAIL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 11 — Post-LIR Stabilization & Path to v0.4.0 🔄
|
||||||
|
|
||||||
|
### 11.1 — LIR Backend Hardening (v0.3.1)
|
||||||
|
|
||||||
|
| Task | Status | Priority | Details |
|
||||||
|
|------|--------|----------|---------|
|
||||||
|
| `11.1.1` Golden tests for C codegen | ⏳ | P0 | Expected `.c` output for 5–6 critical examples; diff on regression |
|
||||||
|
| `11.1.2` LIR debug dump | ⏳ | P1 | `bux build --emit-lir` produces readable LIR |
|
||||||
|
| `11.1.3` Type info in `LirInstr` | ⏳ | P1 | Add `cType` field to instructions; eliminate inference hacks |
|
||||||
|
| `11.1.4` Dead code cleanup | ⏳ | P2 | Remove unused `typeFromValue`, `setTempType`, `emit` |
|
||||||
|
|
||||||
|
### 11.2 — Selfhost Loop (v0.4.0) ⭐
|
||||||
|
|
||||||
|
| Task | Status | Priority | Details |
|
||||||
|
|------|--------|----------|---------|
|
||||||
|
| `11.2.1` `buxc2 build` on `src/` | ✅ | P0 | Verify selfhost compiler can build itself |
|
||||||
|
| `11.2.2` Deterministic C codegen | ✅ | P0 | Remove non-deterministic ordering in `emitModule` |
|
||||||
|
| `11.2.3` `make selfhost-loop` | ✅ | P0 | Makefile target: buxc2 → buxc3 → binary compare |
|
||||||
|
| `11.2.4` Cross-module generics in selfhost | ⏳ | P1 | `Map<K,V>` and `Array<T>` from stdlib in selfhost context |
|
||||||
|
|
||||||
|
### 11.3 — Gradual Ownership (v0.5.0) ⭐⭐
|
||||||
|
|
||||||
|
| Task | Status | Priority | Details |
|
||||||
|
|------|--------|----------|---------|
|
||||||
|
| `11.3.1` `&T` / `&mut T` lifetime analysis | ⏳ | P0 | Basic borrow checker integrated in LIR lowering |
|
||||||
|
| `11.3.2` Bounds checking on slices | ⏳ | P0 | `Slice<T>` index checks `.len` at runtime |
|
||||||
|
| `11.3.3` Lifetime elision (simple rules) | ⏳ | P1 | 80% of cases without explicit annotations |
|
||||||
|
|
||||||
|
### 11.4 — Tooling & Ecosystem (v0.6.0–v0.8.0)
|
||||||
|
|
||||||
|
| Task | Status | Priority | Details |
|
||||||
|
|------|--------|----------|---------|
|
||||||
|
| `11.4.1` `bux test` runner | ✅ | P0 | Built-in test framework with assertions |
|
||||||
|
| `11.4.2` `bux fmt` formatter | ⏳ | P1 | Auto-format Bux source |
|
||||||
|
| `11.4.3` LSP prototype | ⏳ | P2 | Autocomplete, hover types, go-to-definition |
|
||||||
|
| `11.4.4` `bux doc` generator | ⏳ | P2 | HTML from `///` doc comments |
|
||||||
|
|
||||||
|
### 11.5 — Native Backend (v0.9.0)
|
||||||
|
|
||||||
|
| Task | Status | Priority | Details |
|
||||||
|
|------|--------|----------|---------|
|
||||||
|
| `11.5.1` x86-64 codegen (no C) | ⏳ | P1 | ELF64 output, System V AMD64 ABI |
|
||||||
|
| `11.5.2` Custom linker / `.bcu` format | ⏳ | P2 | Bespoke object format for faster builds |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,16 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
> **Status:** Bootstrap compiler (`buxc`, Nim) compiles `.bux` → C → native binary.
|
> **Status:** **v1.0.0** — language freeze. Bootstrap (`buxc`, Nim) and self-hosted (`buxc2`, Bux) both compile `.bux` → C → native binary.
|
||||||
> Self-hosted compiler (`buxc2`) exists as a proof-of-concept but is **deprioritized** in favor of language features and stdlib maturity.
|
> **Selfhost loop:** deterministic C codegen + ELF verified.
|
||||||
|
> **Gradual Ownership:** `@[Checked]` borrow checker, `@[Release]` zero-cost mode, `borrow &mut` expressions.
|
||||||
|
> **Closures:** multi-instance capturing closures via fat function pointers (`BuxFn { code, env }`) in both compilers.
|
||||||
|
> **Tuples:** `(T, U)` types and `.0`/`.1` field access (bootstrap + selfhost).
|
||||||
|
> **Green Threads:** M:N scheduler with channels (Go-style goroutines without GC).
|
||||||
|
> **Examples:** 40+ programs pass (`make test-examples`). Apps: `boko-framework`, `jwt-pitbul`, `nexus`, `simpledb`.
|
||||||
|
> **Semver:** after 1.0, breaking changes require MAJOR (`docs/SEMVER.md`).
|
||||||
|
|
||||||
Bux is a fast, compiled, strongly-typed systems programming language. Features a C backend for native code generation, raw multi-line strings, gradual ownership, async/await, generics, algebraic enums, and a package manager.
|
Bux is a fast, compiled, strongly-typed systems programming language. Features a C backend for native code generation, raw multi-line strings, gradual ownership (opt-in borrow checking), multi-instance closures, async/await, generics, algebraic enums, and a package manager.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -21,6 +27,12 @@ cd hello
|
|||||||
|
|
||||||
# Build and run
|
# Build and run
|
||||||
bux run
|
bux run
|
||||||
|
|
||||||
|
# Build optimized release binary
|
||||||
|
bux build --release
|
||||||
|
|
||||||
|
# Cross-compile for ARM Linux (requires clang)
|
||||||
|
bux build --target aarch64-linux-gnu
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -128,7 +140,7 @@ func Main() -> int {
|
|||||||
### Channels (Producer/Consumer)
|
### Channels (Producer/Consumer)
|
||||||
```bux
|
```bux
|
||||||
import Std::Io::{PrintLine, PrintInt};
|
import Std::Io::{PrintLine, PrintInt};
|
||||||
import Std::Task::{Task_Join, TaskHandle};
|
import Std::Task::{Task_Wait, TaskHandle};
|
||||||
import Std::Channel::{Channel, Channel_New, Channel_SendInt, Channel_RecvInt, Channel_Close};
|
import Std::Channel::{Channel, Channel_New, Channel_SendInt, Channel_RecvInt, Channel_Close};
|
||||||
|
|
||||||
func Producer(chPtr: *Channel<int>) {
|
func Producer(chPtr: *Channel<int>) {
|
||||||
@@ -158,8 +170,8 @@ func Main() -> int {
|
|||||||
let ch: Channel<int> = Channel_New<int>(3);
|
let ch: Channel<int> = Channel_New<int>(3);
|
||||||
let p: *void = spawn Producer(&ch);
|
let p: *void = spawn Producer(&ch);
|
||||||
let c: *void = spawn Consumer(&ch);
|
let c: *void = spawn Consumer(&ch);
|
||||||
Task_Join(TaskHandle { handle: p });
|
Task_Wait(TaskHandle { handle: p });
|
||||||
Task_Join(TaskHandle { handle: c });
|
Task_Wait(TaskHandle { handle: c });
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -176,29 +188,66 @@ func Main() -> int {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Multi-instance closures
|
||||||
|
```bux
|
||||||
|
func MakeAdder(base: int) -> func(int) -> int {
|
||||||
|
return |a: int| -> int { return a + base; };
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
let a10 = MakeAdder(10);
|
||||||
|
let a20 = MakeAdder(20);
|
||||||
|
// Independent capture environments
|
||||||
|
PrintInt(a10(1)); // 11
|
||||||
|
PrintInt(a20(1)); // 21
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Iter map / filter / fold
|
||||||
|
```bux
|
||||||
|
import Std::Iter::{Array_Iter, Iter_MapInt, Iter_FilterInt, Iter_FoldInt};
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
var nums: Array<int> = Array_New<int>(4);
|
||||||
|
Array_Push<int>(&nums, 1);
|
||||||
|
Array_Push<int>(&nums, 2);
|
||||||
|
Array_Push<int>(&nums, 3);
|
||||||
|
let it = Array_Iter<int>(&nums);
|
||||||
|
let doubled = Iter_MapInt(&it, |x: int| -> int { return x * 2; });
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
| Feature | Status |
|
| Feature | Status |
|
||||||
|---------|--------|
|
|---------|--------|
|
||||||
| **Types** | Primitives, pointers, slices, tuples, structs, enums, unions |
|
| **Types** | Primitives, pointers, slices, tuples `(T,U)` + `.0`/`.1`, structs, enums, unions |
|
||||||
| **Generics** | Generic functions (monomorphization) |
|
| **Generics** | Generic functions (monomorphization) |
|
||||||
| **Algebraic Enums** | Enums with data (`Result`, `Option`) |
|
| **Algebraic Enums** | Enums with data (`Result`, `Option`) |
|
||||||
| **Pattern Matching** | `match` with guards |
|
| **Pattern Matching** | `match` with guards |
|
||||||
| **Methods** | `extend` blocks for struct methods |
|
| **Methods** | `extend` blocks for struct methods |
|
||||||
| **Interfaces** | `interface` + `extend` for trait-like behavior |
|
| **Interfaces** | `interface` + `extend` for trait-like behavior |
|
||||||
| **Error Handling** | `Result<T,E>`, `Option<T>`, and the `?` operator |
|
| **Error Handling** | `Result`/`Option`, `?`, `Expect`/`UnwrapOr`/`Or` helpers |
|
||||||
| **Standard Library** | `Io`, `Array`, `String`, `Map`, `Fs`, `Mem`, `Set`, `Path`, `Math`, `Task`, `Channel`, `Sync`, `Os`, `Time`, `Process` |
|
| **Closures** | Capture-less + capturing; **multi-instance** fat pointers (`BuxFn`) |
|
||||||
| **Backend** | C transpiler (self-hosting + bootstrap) |
|
| **Function pointers** | `func(T) -> R` as fat values; named funcs via adapters |
|
||||||
| **Strings** | Raw multi-line backtick strings (`...`), C-string interop |
|
| **Standard Library** | `Io`, `Array`, `String`, `Map`, `Set`, `Iter` (map/filter/fold), `Fs`, `Mem`, `Path`, `Math`, `Task`, `Channel`, `Sync`, `Os`, `Time`, `Process`, `Test`, … |
|
||||||
| **Gradual Ownership** | `@[Checked]` + `&T`/`&mut T` borrow checking |
|
| **Backend** | LIR → C transpiler (clean 3-address code, then gcc/clang) |
|
||||||
|
| **Strings** | Raw multi-line backticks, `f"..."` interp (bootstrap), `ReplaceAll` / `IsBlank` / `Repeat` |
|
||||||
|
| **Gradual Ownership** | `@[Checked]` + `@[Release]` + `@[Shared]` + `borrow &mut` / `borrow &` |
|
||||||
|
| **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 |
|
||||||
| **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` |
|
||||||
| **CTFE** | `const func` — compile-time function execution |
|
| **CTFE** | `const func` — compile-time function execution |
|
||||||
| **Trait Bounds** | `func Max<T: Comparable>(a: T, b: T) -> T` |
|
| **Trait Bounds** | `func Max<T: Comparable>(a: T, b: T) -> T` |
|
||||||
| **Package Manager** | `bux add`, `bux install`, `bux.lock`, path + git deps |
|
| **Package Manager** | `bux add`, `bux install`, `bux.lock`, path + git deps |
|
||||||
| **Tooling** | `bux new`, `bux build`, `bux run`, `bux test`, `bux check` |
|
| **Cross-Compilation** | `--target <triple>` via clang (e.g. `aarch64-linux-gnu`) |
|
||||||
|
| **Diagnostics** | Rust-style snippets, multi-char underlines, `= help:` hints |
|
||||||
|
| **Tooling** | `bux new/build/run/test/check/fmt/doc`, LSP 0.4.0 (locals + inferred lets) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -206,43 +255,108 @@ func Main() -> int {
|
|||||||
|
|
||||||
```
|
```
|
||||||
bux/
|
bux/
|
||||||
├── compiler/ # Compiler source code
|
├── src/ # 🎯 Self-hosting compiler source (Bux)
|
||||||
│ ├── bootstrap/ # Bootstrap compiler (Nim)
|
│ ├── main.bux # Entry point
|
||||||
│ ├── selfhost/ # Self-hosting compiler source (Bux)
|
│ ├── lexer.bux # Tokenizer
|
||||||
│ └── tests/ # Compiler unit tests (Nim)
|
│ ├── parser.bux # Pratt parser
|
||||||
├── library/ # Standard library
|
│ ├── ast.bux # AST node types
|
||||||
│ ├── std/ # Standard library modules (.bux)
|
│ ├── sema.bux # Type checker
|
||||||
│ │ ├── Io.bux, String.bux, Array.bux, Map.bux
|
│ ├── hir_lower.bux # AST → HIR lowering
|
||||||
│ │ ├── Fs.bux, Mem.bux, Set.bux
|
│ ├── c_backend.bux # HIR → C code generator
|
||||||
│ │ ├── Path.bux, Math.bux
|
│ └── cli.bux # CLI driver
|
||||||
│ │ └── Task.bux, Channel.bux
|
├── bootstrap/ # 🔧 Bootstrap compiler (Nim)
|
||||||
│ └── runtime/ # C runtime files (runtime.c, io.c)
|
│ ├── main.nim # Entry point
|
||||||
├── tests/ # Language integration tests
|
│ ├── cli.nim # CLI commands + build driver
|
||||||
|
│ └── ... # (mirrors src/ structure)
|
||||||
|
├── lib/ # 📦 Standard library (23 modules)
|
||||||
|
│ ├── Io.bux # Print, ReadFile, WriteFile
|
||||||
|
│ ├── String.bux # Full string API
|
||||||
|
│ ├── Array.bux # Generic Array<T>
|
||||||
|
│ ├── Map.bux # Generic Map<K,V> + StringMap
|
||||||
|
│ ├── Set.bux # Generic Set<T>
|
||||||
|
│ ├── Task.bux # Green threads (spawn/await)
|
||||||
|
│ ├── Channel.bux # Producer/consumer channels
|
||||||
|
│ ├── Drop.bux # Drop trait interface
|
||||||
|
│ └── ... # Math, Fs, Path, Sync, Result, ...
|
||||||
|
├── rt/ # ⚙️ C runtime
|
||||||
|
│ ├── runtime.c # Memory, scheduler, channels
|
||||||
|
│ └── io.c # File I/O wrappers
|
||||||
|
├── tests/ # 🧪 Unit tests (Nim)
|
||||||
├── examples/ # Example programs
|
├── examples/ # Example programs
|
||||||
├── tools/ # Additional tooling
|
├── apps/ # Real-world applications
|
||||||
├── docs/ # Documentation
|
├── docs/ # Documentation
|
||||||
├── buxs/ # Windows-compatible project root
|
|
||||||
├── README.md
|
├── README.md
|
||||||
├── PLAN.md # Roadmap to self-hosting
|
├── PLAN.md # Roadmap to v1.0.0
|
||||||
└── Makefile
|
└── Makefile
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
| Doc | Description |
|
||||||
|
|-----|-------------|
|
||||||
|
| [`docs/LanguageRef.md`](docs/LanguageRef.md) | Language reference |
|
||||||
|
| [`docs/Stdlib.md`](docs/Stdlib.md) | Standard library API |
|
||||||
|
| [`docs/BuildAndTest.md`](docs/BuildAndTest.md) | Build, test, and tooling |
|
||||||
|
| [`docs/QUALITY_PLAN.md`](docs/QUALITY_PLAN.md) | Roadmap toward a “good” v1.0 |
|
||||||
|
| [`docs/Packages.md`](docs/Packages.md) | Package manager + registry |
|
||||||
|
| [`docs/SEMVER.md`](docs/SEMVER.md) | Versioning policy |
|
||||||
|
| [`docs/ROADMAP.md`](docs/ROADMAP.md) | Feature status (constructs) |
|
||||||
|
| [`PLAN.md`](PLAN.md) | Long-form phase plan |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Build & Test
|
## Build & Test
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build bootstrap compiler (Nim → C)
|
# Build bootstrap compiler (Nim → C)
|
||||||
make build
|
make build
|
||||||
|
|
||||||
|
# Run all example programs
|
||||||
|
make test-examples
|
||||||
|
|
||||||
|
# Golden diagnostic tests (Rust-style error format)
|
||||||
|
make test-errors
|
||||||
|
|
||||||
|
# Full unit + example suite (local; CI splits the same coverage across jobs)
|
||||||
|
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/)
|
||||||
|
make fmt-check
|
||||||
|
# Reformat those trees
|
||||||
|
make fmt
|
||||||
|
|
||||||
|
# Stdlib behavioral goldens (Array / String / collections)
|
||||||
|
make test-stdlib
|
||||||
|
|
||||||
|
# Generate stdlib API docs from /// comments
|
||||||
|
make docs
|
||||||
|
|
||||||
# Build self-hosted compiler (Bux → C → native)
|
# Build self-hosted compiler (Bux → C → native)
|
||||||
make selfhost
|
make selfhost
|
||||||
|
|
||||||
# Run all tests
|
# Package tests (filter + summary table)
|
||||||
make test
|
./buxc test --filter first _test_runner
|
||||||
|
|
||||||
# Run example programs
|
# Format / CI format check
|
||||||
make test-examples
|
./buxc fmt path/to/file.bux
|
||||||
|
./buxc fmt --check path/
|
||||||
|
|
||||||
|
# API docs
|
||||||
|
./buxc doc --out docs/api/stdlib.md lib/
|
||||||
|
|
||||||
|
# Package registry
|
||||||
|
./buxc search greet
|
||||||
|
make test-registry # add greet → install → build temp app
|
||||||
|
|
||||||
|
# Selfhost determinism (optional CI job; not in default `make test`)
|
||||||
|
make selfhost-loop
|
||||||
|
# Full fixed-point: buxc2 → buxc3 → buxc4 (gen2 vs gen3 C+ELF identical)
|
||||||
|
# BUX_SELFHOST_FIXED_POINT=1 make selfhost-loop
|
||||||
|
|
||||||
# Clean build artifacts
|
# Clean build artifacts
|
||||||
make clean
|
make clean
|
||||||
@@ -252,6 +366,20 @@ make clean
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Applications
|
||||||
|
|
||||||
|
The `apps/` directory contains real-world Bux applications that serve as integration tests for the compiler:
|
||||||
|
|
||||||
|
| App | Description | Lines |
|
||||||
|
|-----|-------------|-------|
|
||||||
|
| **boko-framework** | Async web framework (like FastAPI), multi-threaded HTTP server | ~660 |
|
||||||
|
| **jwt-pitbul** | JWT CLI tool — sign, verify, decode (HS256/384/512, RS256/384/512, ES256/384, EdDSA) | ~326 |
|
||||||
|
| **nexus** | High-performance HTTP/1.1, HTTP/2 & WebSocket server | ~550 |
|
||||||
|
|
||||||
|
The bootstrap compiler successfully parses and type-checks all three applications without hanging or crashing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [`docs/LanguageRef.md`](docs/LanguageRef.md) — Language reference
|
- [`docs/LanguageRef.md`](docs/LanguageRef.md) — Language reference
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
[Package]
|
|
||||||
Name = "buxc2"
|
|
||||||
Version = "0.2.0"
|
|
||||||
Type = "bin"
|
|
||||||
|
|
||||||
[Build]
|
|
||||||
Output = "Bin"
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
// main.bux — Entry point for the Bux self-hosting compiler
|
|
||||||
module Main {
|
|
||||||
|
|
||||||
// C runtime for command-line args
|
|
||||||
extern func bux_argc() -> int;
|
|
||||||
extern func bux_argv(index: int) -> String;
|
|
||||||
extern func bux_alloc(size: uint) -> *void;
|
|
||||||
|
|
||||||
// Forward declaration from Cli module
|
|
||||||
func Cli_Run(args: *String, argCount: int) -> int;
|
|
||||||
|
|
||||||
func Main() -> int {
|
|
||||||
let count: int = bux_argc();
|
|
||||||
// Allocate array of String pointers
|
|
||||||
let args: *String = bux_alloc(count as uint * 8) as *String;
|
|
||||||
var i: int = 0;
|
|
||||||
while i < count {
|
|
||||||
args[i] = bux_argv(i);
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
return Cli_Run(args, count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,431 +0,0 @@
|
|||||||
// ast.bux — AST node types (Expr, Stmt, Decl, Pattern, TypeExpr)
|
|
||||||
module Ast {
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// SourceLocation (inline for convenience)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
struct SourceLoc {
|
|
||||||
line: uint32,
|
|
||||||
column: uint32,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Token (lightweight inline)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
struct AstToken {
|
|
||||||
kind: int,
|
|
||||||
text: String,
|
|
||||||
line: uint32,
|
|
||||||
column: uint32,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// TypeExpr — type expressions
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
const tekNamed: int = 0;
|
|
||||||
const tekPath: int = 1;
|
|
||||||
const tekSlice: int = 2;
|
|
||||||
const tekPointer: int = 3;
|
|
||||||
const tekTuple: int = 4;
|
|
||||||
const tekSelf: int = 5;
|
|
||||||
|
|
||||||
struct TypeExpr {
|
|
||||||
kind: int,
|
|
||||||
line: uint32,
|
|
||||||
column: uint32,
|
|
||||||
typeName: String, // for tekNamed
|
|
||||||
pathStr: String, // for tekPath (segments joined with ::)
|
|
||||||
pathCount: int, // number of path segments
|
|
||||||
typeArgName0: String, // up to 2 type args
|
|
||||||
typeArgName1: String,
|
|
||||||
typeArgCount: int,
|
|
||||||
sliceElement: *TypeExpr, // for tekSlice
|
|
||||||
pointerPointee: *TypeExpr, // for tekPointer
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Pattern — match patterns
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
const pkWildcard: int = 0;
|
|
||||||
const pkLiteral: int = 1;
|
|
||||||
const pkIdent: int = 2;
|
|
||||||
const pkRange: int = 3;
|
|
||||||
const pkEnum: int = 4;
|
|
||||||
const pkStruct: int = 5;
|
|
||||||
const pkTuple: int = 6;
|
|
||||||
|
|
||||||
struct Pattern {
|
|
||||||
kind: int,
|
|
||||||
line: uint32,
|
|
||||||
column: uint32,
|
|
||||||
patIdent: String, // for pkIdent
|
|
||||||
patLitKind: int, // for pkLiteral (token kind)
|
|
||||||
patLitText: String, // for pkLiteral (token text)
|
|
||||||
patRangeInclusive: bool, // for pkRange
|
|
||||||
patEnumPath: String, // for pkEnum (path joined)
|
|
||||||
patStructName: String, // for pkStruct
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Expr — expressions (tagged union)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
const ekLiteral: int = 0;
|
|
||||||
const ekIdent: int = 1;
|
|
||||||
const ekSelf: int = 2;
|
|
||||||
const ekPath: int = 3;
|
|
||||||
const ekSizeOf: int = 4;
|
|
||||||
const ekUnary: int = 5;
|
|
||||||
const ekPostfix: int = 6;
|
|
||||||
const ekBinary: int = 7;
|
|
||||||
const ekAssign: int = 8;
|
|
||||||
const ekTernary: int = 9;
|
|
||||||
const ekRange: int = 10;
|
|
||||||
const ekCall: int = 11;
|
|
||||||
const ekGenericCall: int = 12;
|
|
||||||
const ekIndex: int = 13;
|
|
||||||
const ekField: int = 14;
|
|
||||||
const ekStructInit: int = 15;
|
|
||||||
const ekSlice: int = 16;
|
|
||||||
const ekTuple: int = 17;
|
|
||||||
const ekCast: int = 18;
|
|
||||||
const ekIs: int = 19;
|
|
||||||
const ekTry: int = 20;
|
|
||||||
const ekUnwrap: int = 23;
|
|
||||||
const ekBlock: int = 21;
|
|
||||||
const ekMatch: int = 22;
|
|
||||||
const ekSpawn: int = 24;
|
|
||||||
const ekAwait: int = 25;
|
|
||||||
|
|
||||||
struct ExprList {
|
|
||||||
expr: *Expr,
|
|
||||||
next: *ExprList,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Expr {
|
|
||||||
kind: int,
|
|
||||||
line: uint32,
|
|
||||||
column: uint32,
|
|
||||||
// Common fields
|
|
||||||
strValue: String, // ident name, path segments, field name, callee
|
|
||||||
intValue: int, // operator kind, intrinsic kind
|
|
||||||
boolValue: bool, // range inclusive
|
|
||||||
tokKind: int, // literal token kind
|
|
||||||
tokText: String, // literal token text
|
|
||||||
// Children (up to 3 sub-expressions)
|
|
||||||
child1: *Expr, // left, operand, callee, cond, subject
|
|
||||||
child2: *Expr, // right, index, then, value
|
|
||||||
child3: *Expr, // else, third
|
|
||||||
// Extra references
|
|
||||||
refType: *TypeExpr, // cast type, is type, sizeof type
|
|
||||||
refBlock: *Block, // for ekBlock
|
|
||||||
// Generic call
|
|
||||||
genericCallee: String,
|
|
||||||
genericTypeArg0: String,
|
|
||||||
genericTypeArg1: String,
|
|
||||||
genericTypeArgCount: int,
|
|
||||||
// Struct init fields
|
|
||||||
structName: String,
|
|
||||||
structFieldCount: int,
|
|
||||||
// Call arguments (linked list for multi-arg support)
|
|
||||||
callArgs: *ExprList,
|
|
||||||
callArgCount: int,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Block — sequence of statements
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
struct Block {
|
|
||||||
line: uint32,
|
|
||||||
column: uint32,
|
|
||||||
stmtCount: int,
|
|
||||||
firstStmt: *Stmt,
|
|
||||||
lastStmt: *Stmt,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Stmt — statements
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
const skExpr: int = 0;
|
|
||||||
const skLet: int = 1;
|
|
||||||
const skIf: int = 2;
|
|
||||||
const skWhile: int = 3;
|
|
||||||
const skDoWhile: int = 4;
|
|
||||||
const skLoop: int = 5;
|
|
||||||
const skFor: int = 6;
|
|
||||||
const skMatch: int = 7;
|
|
||||||
const skReturn: int = 8;
|
|
||||||
const skBreak: int = 9;
|
|
||||||
const skContinue: int = 10;
|
|
||||||
const skDecl: int = 11;
|
|
||||||
|
|
||||||
struct ElseIf {
|
|
||||||
line: uint32;
|
|
||||||
column: uint32;
|
|
||||||
cond: *Expr;
|
|
||||||
block: *Block;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Stmt {
|
|
||||||
kind: int,
|
|
||||||
line: uint32,
|
|
||||||
column: uint32,
|
|
||||||
// Common fields
|
|
||||||
strValue: String, // let name, pattern ident, label, for var
|
|
||||||
boolValue: bool, // let mutable
|
|
||||||
// Children
|
|
||||||
child1: *Expr, // init expr, condition, iter expr, return value
|
|
||||||
child2: *Expr, // match subject
|
|
||||||
child3: *Expr, // extra
|
|
||||||
refStmtType: *TypeExpr, // let type annotation
|
|
||||||
refStmtPattern: *Pattern,// let pattern
|
|
||||||
refStmtDecl: *Decl, // for skDecl
|
|
||||||
refStmtBlock: *Block, // then/body block
|
|
||||||
refStmtElse: *Block, // else block
|
|
||||||
// Else-if chain
|
|
||||||
elseIfCount: int,
|
|
||||||
// Linked list
|
|
||||||
nextStmt: *Stmt,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Decl — declarations
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
const dkFunc: int = 0;
|
|
||||||
const dkStruct: int = 1;
|
|
||||||
const dkEnum: int = 2;
|
|
||||||
const dkUnion: int = 3;
|
|
||||||
const dkInterface: int = 4;
|
|
||||||
const dkImpl: int = 5;
|
|
||||||
const dkModule: int = 6;
|
|
||||||
const dkUse: int = 7;
|
|
||||||
const dkConst: int = 8;
|
|
||||||
const dkTypeAlias: int = 9;
|
|
||||||
const dkExternFunc: int = 10;
|
|
||||||
const dkExternVar: int = 11;
|
|
||||||
|
|
||||||
struct Param {
|
|
||||||
line: uint32;
|
|
||||||
column: uint32;
|
|
||||||
name: String;
|
|
||||||
refParamType: *TypeExpr;
|
|
||||||
isVariadic: bool;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct StructField {
|
|
||||||
line: uint32;
|
|
||||||
column: uint32;
|
|
||||||
isPublic: bool;
|
|
||||||
name: String;
|
|
||||||
refFieldType: *TypeExpr;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct EnumVariant {
|
|
||||||
line: uint32;
|
|
||||||
column: uint32;
|
|
||||||
name: String;
|
|
||||||
fieldCount: int;
|
|
||||||
fieldTypeName0: String;
|
|
||||||
fieldTypeName1: String;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Decl {
|
|
||||||
kind: int,
|
|
||||||
line: uint32,
|
|
||||||
column: uint32,
|
|
||||||
isPublic: bool,
|
|
||||||
isAsync: bool,
|
|
||||||
// Names
|
|
||||||
strValue: String, // decl name
|
|
||||||
strValue2: String, // interface name, dll name, module path
|
|
||||||
// Type params (up to 2)
|
|
||||||
typeParam0: String,
|
|
||||||
typeParam1: String,
|
|
||||||
typeParamCount: int,
|
|
||||||
// Params (for functions)
|
|
||||||
paramCount: int,
|
|
||||||
param0: Param,
|
|
||||||
param1: Param,
|
|
||||||
param2: Param,
|
|
||||||
param3: Param,
|
|
||||||
param4: Param,
|
|
||||||
param5: Param,
|
|
||||||
param6: Param,
|
|
||||||
param7: Param,
|
|
||||||
param8: Param,
|
|
||||||
retType: *TypeExpr,
|
|
||||||
// Body
|
|
||||||
refBody: *Block,
|
|
||||||
// Struct fields (up to 64)
|
|
||||||
fieldCount: int,
|
|
||||||
field0: StructField,
|
|
||||||
field1: StructField,
|
|
||||||
field2: StructField,
|
|
||||||
field3: StructField,
|
|
||||||
field4: StructField,
|
|
||||||
field5: StructField,
|
|
||||||
field6: StructField,
|
|
||||||
field7: StructField,
|
|
||||||
field8: StructField,
|
|
||||||
field9: StructField,
|
|
||||||
field10: StructField,
|
|
||||||
field11: StructField,
|
|
||||||
field12: StructField,
|
|
||||||
field13: StructField,
|
|
||||||
field14: StructField,
|
|
||||||
field15: StructField,
|
|
||||||
field16: StructField,
|
|
||||||
field17: StructField,
|
|
||||||
field18: StructField,
|
|
||||||
field19: StructField,
|
|
||||||
field20: StructField,
|
|
||||||
field21: StructField,
|
|
||||||
field22: StructField,
|
|
||||||
field23: StructField,
|
|
||||||
field24: StructField,
|
|
||||||
field25: StructField,
|
|
||||||
field26: StructField,
|
|
||||||
field27: StructField,
|
|
||||||
field28: StructField,
|
|
||||||
field29: StructField,
|
|
||||||
field30: StructField,
|
|
||||||
field31: StructField,
|
|
||||||
field32: StructField,
|
|
||||||
field33: StructField,
|
|
||||||
field34: StructField,
|
|
||||||
field35: StructField,
|
|
||||||
field36: StructField,
|
|
||||||
field37: StructField,
|
|
||||||
field38: StructField,
|
|
||||||
field39: StructField,
|
|
||||||
field40: StructField,
|
|
||||||
field41: StructField,
|
|
||||||
field42: StructField,
|
|
||||||
field43: StructField,
|
|
||||||
field44: StructField,
|
|
||||||
field45: StructField,
|
|
||||||
field46: StructField,
|
|
||||||
field47: StructField,
|
|
||||||
field48: StructField,
|
|
||||||
field49: StructField,
|
|
||||||
field50: StructField,
|
|
||||||
field51: StructField,
|
|
||||||
field52: StructField,
|
|
||||||
field53: StructField,
|
|
||||||
field54: StructField,
|
|
||||||
field55: StructField,
|
|
||||||
field56: StructField,
|
|
||||||
field57: StructField,
|
|
||||||
field58: StructField,
|
|
||||||
field59: StructField,
|
|
||||||
field60: StructField,
|
|
||||||
field61: StructField,
|
|
||||||
field62: StructField,
|
|
||||||
field63: StructField,
|
|
||||||
// Enum variants (up to 8)
|
|
||||||
variantCount: int,
|
|
||||||
variant0: EnumVariant,
|
|
||||||
variant1: EnumVariant,
|
|
||||||
variant2: EnumVariant,
|
|
||||||
variant3: EnumVariant,
|
|
||||||
variant4: EnumVariant,
|
|
||||||
variant5: EnumVariant,
|
|
||||||
variant6: EnumVariant,
|
|
||||||
variant7: EnumVariant,
|
|
||||||
// Impl methods (up to 4)
|
|
||||||
methodCount: int,
|
|
||||||
// Use/import
|
|
||||||
useKind: int,
|
|
||||||
usePath: String,
|
|
||||||
useNames: String, // joined names for multi-import
|
|
||||||
// Const
|
|
||||||
constType: *TypeExpr,
|
|
||||||
constValue: *Expr,
|
|
||||||
// Type alias
|
|
||||||
aliasType: *TypeExpr,
|
|
||||||
// Extern func
|
|
||||||
extFuncDll: String,
|
|
||||||
extFuncVariadic: bool,
|
|
||||||
extFuncRetType: *TypeExpr,
|
|
||||||
// Children
|
|
||||||
childDecl1: *Decl, // linked list of decls (for module items, impl methods)
|
|
||||||
childDecl2: *Decl,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Module — AST root
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
struct Module {
|
|
||||||
name: String,
|
|
||||||
path: String, // path segments joined
|
|
||||||
itemCount: int,
|
|
||||||
firstItem: *Decl,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Constructor helpers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Ast_MakeExpr(kind: int, line: uint32, col: uint32) -> Expr {
|
|
||||||
return Expr { kind: kind, line: line, column: col,
|
|
||||||
strValue: "", intValue: 0, boolValue: false,
|
|
||||||
tokKind: 0, tokText: "",
|
|
||||||
child1: null as *Expr, child2: null as *Expr, child3: null as *Expr,
|
|
||||||
refType: null as *TypeExpr, refBlock: null as *Block,
|
|
||||||
genericCallee: "", genericTypeArg0: "", genericTypeArg1: "", genericTypeArgCount: 0,
|
|
||||||
structName: "", structFieldCount: 0,
|
|
||||||
callArgs: null as *ExprList, callArgCount: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
func Ast_MakeIdent(name: String, line: uint32, col: uint32) -> Expr {
|
|
||||||
var e: Expr = Ast_MakeExpr(ekIdent, line, col);
|
|
||||||
e.strValue = name;
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Ast_MakeLiteral(tokKind: int, text: String, line: uint32, col: uint32) -> Expr {
|
|
||||||
var e: Expr = Ast_MakeExpr(ekLiteral, line, col);
|
|
||||||
e.tokKind = tokKind;
|
|
||||||
e.tokText = text;
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Ast_MakeBinary(op: int, left: *Expr, right: *Expr, line: uint32, col: uint32) -> Expr {
|
|
||||||
var e: Expr = Ast_MakeExpr(ekBinary, line, col);
|
|
||||||
e.intValue = op;
|
|
||||||
e.child1 = left;
|
|
||||||
e.child2 = right;
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Ast_MakeCall(callee: *Expr, line: uint32, col: uint32) -> Expr {
|
|
||||||
var e: Expr = Ast_MakeExpr(ekCall, line, col);
|
|
||||||
e.child1 = callee;
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Ast_MakeStmt(kind: int, line: uint32, col: uint32) -> Stmt {
|
|
||||||
return Stmt { kind: kind, line: line, column: col,
|
|
||||||
strValue: "", boolValue: false,
|
|
||||||
child1: null as *Expr, child2: null as *Expr, child3: null as *Expr,
|
|
||||||
refStmtType: null as *TypeExpr, refStmtPattern: null as *Pattern,
|
|
||||||
refStmtDecl: null as *Decl, refStmtBlock: null as *Block, refStmtElse: null as *Block,
|
|
||||||
elseIfCount: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
func Ast_MakeDecl(kind: int, line: uint32, col: uint32) -> Decl {
|
|
||||||
return Decl { kind: kind, line: line, column: col, isPublic: false,
|
|
||||||
strValue: "", strValue2: "",
|
|
||||||
typeParam0: "", typeParam1: "", typeParamCount: 0,
|
|
||||||
paramCount: 0,
|
|
||||||
retType: null as *TypeExpr,
|
|
||||||
refBody: null as *Block,
|
|
||||||
fieldCount: 0,
|
|
||||||
variantCount: 0,
|
|
||||||
methodCount: 0,
|
|
||||||
useKind: 0, usePath: "", useNames: "",
|
|
||||||
constType: null as *TypeExpr, constValue: null as *Expr,
|
|
||||||
aliasType: null as *TypeExpr,
|
|
||||||
extFuncDll: "", extFuncVariadic: false, extFuncRetType: null as *TypeExpr,
|
|
||||||
childDecl1: null as *Decl, childDecl2: null as *Decl };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,689 +0,0 @@
|
|||||||
// c_backend.bux — C transpiler backend (ported from c_backend.nim)
|
|
||||||
// Generates C code from the HIR.
|
|
||||||
module CBackend {
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Type → C type name
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func CBackend_TypeToC(kind: int) -> String {
|
|
||||||
if kind == tyVoid { return "void"; }
|
|
||||||
if kind == tyBool { return "bool"; }
|
|
||||||
if kind == tyBool8 { return "bool"; }
|
|
||||||
if kind == tyBool16 { return "bool"; }
|
|
||||||
if kind == tyBool32 { return "bool"; }
|
|
||||||
if kind == tyChar8 { return "char"; }
|
|
||||||
if kind == tyChar16 { return "uint16"; }
|
|
||||||
if kind == tyChar32 { return "uint32"; }
|
|
||||||
if kind == tyStr { return "String"; }
|
|
||||||
if kind == tyInt8 { return "int8"; }
|
|
||||||
if kind == tyInt16 { return "int16"; }
|
|
||||||
if kind == tyInt32 { return "int32"; }
|
|
||||||
if kind == tyInt64 { return "int64"; }
|
|
||||||
if kind == tyInt{ return "int"; }
|
|
||||||
if kind == tyUInt8 { return "uint8"; }
|
|
||||||
if kind == tyUInt16 { return "uint16"; }
|
|
||||||
if kind == tyUInt32 { return "uint32"; }
|
|
||||||
if kind == tyUInt64 { return "uint64"; }
|
|
||||||
if kind == tyUInt { return "uint"; }
|
|
||||||
if kind == tyFloat32 { return "float32"; }
|
|
||||||
if kind == tyFloat64 { return "float64"; }
|
|
||||||
if kind == tyPointer { return "void*"; }
|
|
||||||
if kind == tyNamed { return "int"; }
|
|
||||||
return "int";
|
|
||||||
}
|
|
||||||
|
|
||||||
func CBackend_OpToC(op: int) -> String {
|
|
||||||
if op == tkPlus { return "+"; }
|
|
||||||
if op == tkMinus { return "-"; }
|
|
||||||
if op == tkStar { return "*"; }
|
|
||||||
if op == tkSlash { return "/"; }
|
|
||||||
if op == tkPercent { return "%"; }
|
|
||||||
if op == tkEq { return "=="; }
|
|
||||||
if op == tkNe { return "!="; }
|
|
||||||
if op == tkLt { return "<"; }
|
|
||||||
if op == tkLe { return "<="; }
|
|
||||||
if op == tkGt { return ">"; }
|
|
||||||
if op == tkGe { return ">="; }
|
|
||||||
if op == tkAmpAmp { return "&&"; }
|
|
||||||
if op == tkPipePipe { return "||"; }
|
|
||||||
if op == tkBang { return "!"; }
|
|
||||||
if op == tkAmp { return "&"; }
|
|
||||||
if op == tkPipe { return "|"; }
|
|
||||||
if op == tkCaret { return "^"; }
|
|
||||||
if op == tkShl { return "<<"; }
|
|
||||||
if op == tkShr { return ">>"; }
|
|
||||||
if op == tkAssign { return "="; }
|
|
||||||
return "?";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// StringBuilder-based C emitter
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
struct CEmitter {
|
|
||||||
sb: StringBuilder,
|
|
||||||
indent: int,
|
|
||||||
}
|
|
||||||
|
|
||||||
func CBE_Init() -> CEmitter {
|
|
||||||
var sb: StringBuilder = StringBuilder_NewCap(4096);
|
|
||||||
return CEmitter { sb: sb, indent: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
func CBE_Emit(cbe: *CEmitter, text: String) {
|
|
||||||
var i: int = 0;
|
|
||||||
while i < cbe.indent {
|
|
||||||
StringBuilder_Append(&cbe.sb, " ");
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
func CBE_EmitLine(cbe: *CEmitter, text: String) {
|
|
||||||
CBE_Emit(cbe, text);
|
|
||||||
StringBuilder_Append(&cbe.sb, "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
func CBE_Build(cbe: *CEmitter) -> String {
|
|
||||||
return StringBuilder_Build(&cbe.sb);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Emit HIR node
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) {
|
|
||||||
if node == null as *HirNode { return; }
|
|
||||||
let kind: int = node.kind;
|
|
||||||
|
|
||||||
// Literal
|
|
||||||
if kind == hLit {
|
|
||||||
if node.intValue == tkNull {
|
|
||||||
StringBuilder_Append(&cbe.sb, "0");
|
|
||||||
} else {
|
|
||||||
StringBuilder_Append(&cbe.sb, node.strValue);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Variable
|
|
||||||
if kind == hVar {
|
|
||||||
StringBuilder_Append(&cbe.sb, node.strValue);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Binary
|
|
||||||
if kind == hBinary {
|
|
||||||
CBE_EmitExpr(cbe, node.child1);
|
|
||||||
StringBuilder_Append(&cbe.sb, " ");
|
|
||||||
StringBuilder_Append(&cbe.sb, CBackend_OpToC(node.intValue));
|
|
||||||
StringBuilder_Append(&cbe.sb, " ");
|
|
||||||
CBE_EmitExpr(cbe, node.child2);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unary
|
|
||||||
if kind == hUnary {
|
|
||||||
StringBuilder_Append(&cbe.sb, CBackend_OpToC(node.intValue));
|
|
||||||
CBE_EmitExpr(cbe, node.child1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call
|
|
||||||
if kind == hCall {
|
|
||||||
StringBuilder_Append(&cbe.sb, node.strValue);
|
|
||||||
StringBuilder_Append(&cbe.sb, "(");
|
|
||||||
var needsComma: bool = false;
|
|
||||||
if node.child1 != null as *HirNode {
|
|
||||||
CBE_EmitExpr(cbe, node.child1);
|
|
||||||
needsComma = true;
|
|
||||||
}
|
|
||||||
if node.child2 != null as *HirNode {
|
|
||||||
if needsComma {
|
|
||||||
StringBuilder_Append(&cbe.sb, ", ");
|
|
||||||
}
|
|
||||||
CBE_EmitExpr(cbe, node.child2);
|
|
||||||
needsComma = true;
|
|
||||||
}
|
|
||||||
// Emit extra args from linked list
|
|
||||||
var ai: int = 0;
|
|
||||||
var curExtra: *HirArgList = node.extraData as *HirArgList;
|
|
||||||
while ai < node.extraCount {
|
|
||||||
if needsComma {
|
|
||||||
StringBuilder_Append(&cbe.sb, ", ");
|
|
||||||
}
|
|
||||||
CBE_EmitExpr(cbe, curExtra.node);
|
|
||||||
needsComma = true;
|
|
||||||
curExtra = curExtra.next;
|
|
||||||
ai = ai + 1;
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, ")");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// spawn Callee(args)
|
|
||||||
if kind == hSpawn {
|
|
||||||
StringBuilder_Append(&cbe.sb, "bux_async_spawn(");
|
|
||||||
StringBuilder_Append(&cbe.sb, node.strValue);
|
|
||||||
if node.child1 != null as *HirNode {
|
|
||||||
StringBuilder_Append(&cbe.sb, ", (void*)");
|
|
||||||
CBE_EmitExpr(cbe, node.child1);
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, ")");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// await
|
|
||||||
if kind == hAwait {
|
|
||||||
StringBuilder_Append(&cbe.sb, "bux_async_await(");
|
|
||||||
CBE_EmitExpr(cbe, node.child1);
|
|
||||||
StringBuilder_Append(&cbe.sb, ")");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return
|
|
||||||
if kind == hReturn {
|
|
||||||
StringBuilder_Append(&cbe.sb, "return");
|
|
||||||
if node.child1 != null as *HirNode {
|
|
||||||
StringBuilder_Append(&cbe.sb, " ");
|
|
||||||
CBE_EmitExpr(cbe, node.child1);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alloca
|
|
||||||
if kind == hAlloca {
|
|
||||||
if !String_Eq(node.typeName, "") {
|
|
||||||
StringBuilder_Append(&cbe.sb, node.typeName);
|
|
||||||
} else {
|
|
||||||
StringBuilder_Append(&cbe.sb, "int");
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, " ");
|
|
||||||
StringBuilder_Append(&cbe.sb, node.strValue);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store: combine alloca + value into single declaration
|
|
||||||
if kind == hStore {
|
|
||||||
if node.child1 != null as *HirNode && node.child1.kind == hAlloca {
|
|
||||||
// Declaration with initializer: Type x = value;
|
|
||||||
if !String_Eq(node.child1.typeName, "") {
|
|
||||||
StringBuilder_Append(&cbe.sb, node.child1.typeName);
|
|
||||||
} else {
|
|
||||||
StringBuilder_Append(&cbe.sb, CBackend_TypeToC(node.child1.intValue));
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, " ");
|
|
||||||
StringBuilder_Append(&cbe.sb, node.child1.strValue);
|
|
||||||
StringBuilder_Append(&cbe.sb, " = ");
|
|
||||||
CBE_EmitExpr(cbe, node.child2);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Plain assignment
|
|
||||||
CBE_EmitExpr(cbe, node.child1);
|
|
||||||
StringBuilder_Append(&cbe.sb, " = ");
|
|
||||||
CBE_EmitExpr(cbe, node.child2);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If
|
|
||||||
if kind == hIf {
|
|
||||||
StringBuilder_Append(&cbe.sb, "if (");
|
|
||||||
CBE_EmitExpr(cbe, node.child1);
|
|
||||||
StringBuilder_Append(&cbe.sb, ") {\n");
|
|
||||||
if node.child2 != null as *HirNode {
|
|
||||||
cbe.indent = cbe.indent + 1;
|
|
||||||
CBE_EmitExpr(cbe, node.child2);
|
|
||||||
cbe.indent = cbe.indent - 1;
|
|
||||||
}
|
|
||||||
var sp: int = 0;
|
|
||||||
while sp < cbe.indent {
|
|
||||||
StringBuilder_Append(&cbe.sb, " ");
|
|
||||||
sp = sp + 1;
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, "}");
|
|
||||||
let elseBlock: *HirNode = node.extraData as *HirNode;
|
|
||||||
if elseBlock != null as *HirNode {
|
|
||||||
StringBuilder_Append(&cbe.sb, " else {\n");
|
|
||||||
cbe.indent = cbe.indent + 1;
|
|
||||||
CBE_EmitExpr(cbe, elseBlock);
|
|
||||||
cbe.indent = cbe.indent - 1;
|
|
||||||
sp = 0;
|
|
||||||
while sp < cbe.indent {
|
|
||||||
StringBuilder_Append(&cbe.sb, " ");
|
|
||||||
sp = sp + 1;
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, "}\n");
|
|
||||||
} else {
|
|
||||||
StringBuilder_Append(&cbe.sb, "\n");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// While
|
|
||||||
if kind == hWhile {
|
|
||||||
StringBuilder_Append(&cbe.sb, "while (");
|
|
||||||
CBE_EmitExpr(cbe, node.child1);
|
|
||||||
StringBuilder_Append(&cbe.sb, ") {\n");
|
|
||||||
if node.child2 != null as *HirNode {
|
|
||||||
cbe.indent = cbe.indent + 1;
|
|
||||||
CBE_EmitExpr(cbe, node.child2);
|
|
||||||
cbe.indent = cbe.indent - 1;
|
|
||||||
}
|
|
||||||
var sp: int = 0;
|
|
||||||
while sp < cbe.indent {
|
|
||||||
StringBuilder_Append(&cbe.sb, " ");
|
|
||||||
sp = sp + 1;
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, "}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop (infinite)
|
|
||||||
if kind == hLoop {
|
|
||||||
StringBuilder_Append(&cbe.sb, "while (1) {\n");
|
|
||||||
if node.child1 != null as *HirNode {
|
|
||||||
cbe.indent = cbe.indent + 1;
|
|
||||||
CBE_EmitExpr(cbe, node.child1);
|
|
||||||
cbe.indent = cbe.indent - 1;
|
|
||||||
}
|
|
||||||
var sp: int = 0;
|
|
||||||
while sp < cbe.indent {
|
|
||||||
StringBuilder_Append(&cbe.sb, " ");
|
|
||||||
sp = sp + 1;
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, "}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Field access: obj.field — emit as obj->field (since most are pointers)
|
|
||||||
if kind == hFieldPtr {
|
|
||||||
CBE_EmitExpr(cbe, node.child1);
|
|
||||||
StringBuilder_Append(&cbe.sb, "->");
|
|
||||||
StringBuilder_Append(&cbe.sb, node.strValue);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sizeof: sizeof(Type) — emit as sizeof(Type)
|
|
||||||
if kind == hSizeOf {
|
|
||||||
StringBuilder_Append(&cbe.sb, "sizeof(");
|
|
||||||
if !String_Eq(node.typeName, "") {
|
|
||||||
StringBuilder_Append(&cbe.sb, node.typeName);
|
|
||||||
} else {
|
|
||||||
StringBuilder_Append(&cbe.sb, "int");
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, ")");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Index: arr[idx] — emit as arr[idx]
|
|
||||||
if kind == hIndexPtr {
|
|
||||||
CBE_EmitExpr(cbe, node.child1);
|
|
||||||
StringBuilder_Append(&cbe.sb, "[");
|
|
||||||
CBE_EmitExpr(cbe, node.child2);
|
|
||||||
StringBuilder_Append(&cbe.sb, "]");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load: *ptr — emit as *ptr (dereference)
|
|
||||||
if kind == hLoad {
|
|
||||||
StringBuilder_Append(&cbe.sb, "(*");
|
|
||||||
CBE_EmitExpr(cbe, node.child1);
|
|
||||||
StringBuilder_Append(&cbe.sb, ")");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign: target = value
|
|
||||||
if kind == hAssign {
|
|
||||||
CBE_EmitExpr(cbe, node.child1);
|
|
||||||
StringBuilder_Append(&cbe.sb, " = ");
|
|
||||||
CBE_EmitExpr(cbe, node.child2);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cast
|
|
||||||
if kind == hCast {
|
|
||||||
StringBuilder_Append(&cbe.sb, "((");
|
|
||||||
if !String_Eq(node.typeName, "") {
|
|
||||||
StringBuilder_Append(&cbe.sb, node.typeName);
|
|
||||||
} else {
|
|
||||||
StringBuilder_Append(&cbe.sb, CBackend_TypeToC(node.typeKind));
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, ")");
|
|
||||||
CBE_EmitExpr(cbe, node.child1);
|
|
||||||
StringBuilder_Append(&cbe.sb, ")");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Struct init: ((TypeName){.field = value, ...})
|
|
||||||
if kind == hStructInit {
|
|
||||||
StringBuilder_Append(&cbe.sb, "((");
|
|
||||||
StringBuilder_Append(&cbe.sb, node.strValue);
|
|
||||||
StringBuilder_Append(&cbe.sb, "){");
|
|
||||||
// Emit fields (chained via child3)
|
|
||||||
var field: *HirNode = node.child1;
|
|
||||||
var first: bool = true;
|
|
||||||
while field != null as *HirNode {
|
|
||||||
if !first {
|
|
||||||
StringBuilder_Append(&cbe.sb, ", ");
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, ".");
|
|
||||||
StringBuilder_Append(&cbe.sb, field.strValue);
|
|
||||||
StringBuilder_Append(&cbe.sb, " = ");
|
|
||||||
CBE_EmitExpr(cbe, field.child1);
|
|
||||||
first = false;
|
|
||||||
field = field.child3;
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, "})");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block — emit each statement via child3 linked list
|
|
||||||
if kind == hBlock {
|
|
||||||
var child: *HirNode = node.child1;
|
|
||||||
while child != null as *HirNode {
|
|
||||||
// Indent
|
|
||||||
var sp: int = 0;
|
|
||||||
while sp < cbe.indent {
|
|
||||||
StringBuilder_Append(&cbe.sb, " ");
|
|
||||||
sp = sp + 1;
|
|
||||||
}
|
|
||||||
CBE_EmitExpr(cbe, child);
|
|
||||||
// Add semicolon for statements that need it
|
|
||||||
if child.kind != hBlock && child.kind != hIf && child.kind != hWhile && child.kind != hLoop {
|
|
||||||
StringBuilder_Append(&cbe.sb, ";");
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, "\n");
|
|
||||||
child = child.child3;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Emit function declaration
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func CBE_EmitFuncDecl(cbe: *CEmitter, f: *HirFunc) {
|
|
||||||
// Return type
|
|
||||||
if String_Eq(f.retTypeName, "") || String_Eq(f.retTypeName, "void") {
|
|
||||||
StringBuilder_Append(&cbe.sb, "void ");
|
|
||||||
} else {
|
|
||||||
StringBuilder_Append(&cbe.sb, f.retTypeName);
|
|
||||||
StringBuilder_Append(&cbe.sb, " ");
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder_Append(&cbe.sb, f.name);
|
|
||||||
StringBuilder_Append(&cbe.sb, "(");
|
|
||||||
|
|
||||||
// Parameters
|
|
||||||
var i: int = 0;
|
|
||||||
while i < f.paramCount {
|
|
||||||
if i > 0 {
|
|
||||||
StringBuilder_Append(&cbe.sb, ", ");
|
|
||||||
}
|
|
||||||
// Use param type info
|
|
||||||
var pname: String = "";
|
|
||||||
var ptype: String = "";
|
|
||||||
if i == 0 { pname = f.param0.name; ptype = f.param0.typeName; }
|
|
||||||
if i == 1 { pname = f.param1.name; ptype = f.param1.typeName; }
|
|
||||||
if i == 2 { pname = f.param2.name; ptype = f.param2.typeName; }
|
|
||||||
if i == 3 { pname = f.param3.name; ptype = f.param3.typeName; }
|
|
||||||
if i == 4 { pname = f.param4.name; ptype = f.param4.typeName; }
|
|
||||||
if i == 5 { pname = f.param5.name; ptype = f.param5.typeName; }
|
|
||||||
if i == 6 { pname = f.param6.name; ptype = f.param6.typeName; }
|
|
||||||
if i == 7 { pname = f.param7.name; ptype = f.param7.typeName; }
|
|
||||||
if i == 8 { pname = f.param8.name; ptype = f.param8.typeName; }
|
|
||||||
// Emit type
|
|
||||||
if String_Eq(ptype, "String") || String_Eq(ptype, "str") {
|
|
||||||
StringBuilder_Append(&cbe.sb, "String ");
|
|
||||||
} else if String_Eq(ptype, "bool") {
|
|
||||||
StringBuilder_Append(&cbe.sb, "bool ");
|
|
||||||
} else if String_Eq(ptype, "int") {
|
|
||||||
StringBuilder_Append(&cbe.sb, "int ");
|
|
||||||
} else if String_Eq(ptype, "int64") {
|
|
||||||
StringBuilder_Append(&cbe.sb, "int64 ");
|
|
||||||
} else if String_Eq(ptype, "uint") {
|
|
||||||
StringBuilder_Append(&cbe.sb, "uint ");
|
|
||||||
} else if String_Eq(ptype, "float64") {
|
|
||||||
StringBuilder_Append(&cbe.sb, "float64 ");
|
|
||||||
} else if !String_Eq(ptype, "") {
|
|
||||||
StringBuilder_Append(&cbe.sb, ptype);
|
|
||||||
StringBuilder_Append(&cbe.sb, " ");
|
|
||||||
} else {
|
|
||||||
StringBuilder_Append(&cbe.sb, "int "); // fallback
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, pname);
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder_Append(&cbe.sb, ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Helpers for detecting generic declarations (not yet monomorphized)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func CBE_IsGenericTypeName(name: String) -> bool {
|
|
||||||
if String_Eq(name, "T") || String_Eq(name, "K") || String_Eq(name, "V") { return true; }
|
|
||||||
if String_Eq(name, "T*") || String_Eq(name, "K*") || String_Eq(name, "V*") { return true; }
|
|
||||||
// Generic container structs and their pointer variants
|
|
||||||
if String_Eq(name, "Array") || String_Eq(name, "Array*") { return true; }
|
|
||||||
if String_Eq(name, "SetEntry") || String_Eq(name, "SetEntry*") { return true; }
|
|
||||||
if String_Eq(name, "StringMapEntry") || String_Eq(name, "StringMapEntry*") { return true; }
|
|
||||||
if String_Eq(name, "MapEntry") || String_Eq(name, "MapEntry*") { return true; }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
func CBE_StructHasGeneric(st: *HirStruct) -> bool {
|
|
||||||
var fi: int = 0;
|
|
||||||
while fi < st.fieldCount {
|
|
||||||
if CBE_IsGenericTypeName(st.fields[fi].typeName) { return true; }
|
|
||||||
fi = fi + 1;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
func CBE_FuncHasGeneric(f: *HirFunc) -> bool {
|
|
||||||
var pi: int = 0;
|
|
||||||
while pi < f.paramCount {
|
|
||||||
var ptype: String = "";
|
|
||||||
if pi == 0 { ptype = f.param0.typeName; }
|
|
||||||
if pi == 1 { ptype = f.param1.typeName; }
|
|
||||||
if pi == 2 { ptype = f.param2.typeName; }
|
|
||||||
if pi == 3 { ptype = f.param3.typeName; }
|
|
||||||
if pi == 4 { ptype = f.param4.typeName; }
|
|
||||||
if pi == 5 { ptype = f.param5.typeName; }
|
|
||||||
if pi == 6 { ptype = f.param6.typeName; }
|
|
||||||
if pi == 7 { ptype = f.param7.typeName; }
|
|
||||||
if pi == 8 { ptype = f.param8.typeName; }
|
|
||||||
if CBE_IsGenericTypeName(ptype) { return true; }
|
|
||||||
pi = pi + 1;
|
|
||||||
}
|
|
||||||
if CBE_IsGenericTypeName(f.retTypeName) { return true; }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Generate complete C module
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func CBackend_Generate(mod: *HirModule) -> String {
|
|
||||||
let cbe: *CEmitter = bux_alloc(sizeof(CEmitter)) as *CEmitter;
|
|
||||||
cbe.sb = StringBuilder_NewCap(8192);
|
|
||||||
cbe.indent = 0;
|
|
||||||
|
|
||||||
// Header
|
|
||||||
StringBuilder_Append(&cbe.sb, "// Generated by Bux C Backend\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, "#include <stdint.h>\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, "#include <stdbool.h>\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, "#include <string.h>\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, "#include <stdio.h>\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, "#include <stdlib.h>\n\n");
|
|
||||||
// Type aliases
|
|
||||||
StringBuilder_Append(&cbe.sb, "typedef const char* String;\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, "typedef unsigned char uint8;\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, "typedef unsigned short uint16;\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, "typedef unsigned int uint32;\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, "typedef unsigned long long uint64;\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, "typedef signed char int8;\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, "typedef short int16;\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, "typedef long long int64;\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, "typedef float float32;\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, "typedef double float64;\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, "typedef char char8;\n\n");
|
|
||||||
// Runtime declarations
|
|
||||||
StringBuilder_Append(&cbe.sb, "void* bux_alloc(unsigned int size);\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, "void bux_free(void* ptr);\n\n");
|
|
||||||
|
|
||||||
// Forward declare all struct types (skip generics)
|
|
||||||
var si: int = 0;
|
|
||||||
while si < mod.structCount {
|
|
||||||
if !CBE_StructHasGeneric(&mod.structs[si]) {
|
|
||||||
StringBuilder_Append(&cbe.sb, "typedef struct ");
|
|
||||||
StringBuilder_Append(&cbe.sb, mod.structs[si].name);
|
|
||||||
StringBuilder_Append(&cbe.sb, " ");
|
|
||||||
StringBuilder_Append(&cbe.sb, mod.structs[si].name);
|
|
||||||
StringBuilder_Append(&cbe.sb, ";\n");
|
|
||||||
}
|
|
||||||
si = si + 1;
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, "\n");
|
|
||||||
|
|
||||||
// Enum definitions (typedef to int)
|
|
||||||
var ei: int = 0;
|
|
||||||
while ei < mod.enumCount {
|
|
||||||
StringBuilder_Append(&cbe.sb, "typedef int ");
|
|
||||||
StringBuilder_Append(&cbe.sb, mod.enums[ei].name);
|
|
||||||
StringBuilder_Append(&cbe.sb, ";\n");
|
|
||||||
ei = ei + 1;
|
|
||||||
}
|
|
||||||
if mod.enumCount > 0 {
|
|
||||||
StringBuilder_Append(&cbe.sb, "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constant definitions
|
|
||||||
var ci: int = 0;
|
|
||||||
while ci < mod.constCount {
|
|
||||||
StringBuilder_Append(&cbe.sb, "#define ");
|
|
||||||
StringBuilder_Append(&cbe.sb, mod.consts[ci].name);
|
|
||||||
StringBuilder_Append(&cbe.sb, " ");
|
|
||||||
StringBuilder_Append(&cbe.sb, String_FromInt(mod.consts[ci].value as int64));
|
|
||||||
StringBuilder_Append(&cbe.sb, "\n");
|
|
||||||
ci = ci + 1;
|
|
||||||
}
|
|
||||||
if mod.constCount > 0 {
|
|
||||||
StringBuilder_Append(&cbe.sb, "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Struct definitions (skip generics)
|
|
||||||
si = 0;
|
|
||||||
while si < mod.structCount {
|
|
||||||
if CBE_StructHasGeneric(&mod.structs[si]) {
|
|
||||||
si = si + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, "struct ");
|
|
||||||
StringBuilder_Append(&cbe.sb, mod.structs[si].name);
|
|
||||||
StringBuilder_Append(&cbe.sb, " {\n");
|
|
||||||
var fi: int = 0;
|
|
||||||
while fi < mod.structs[si].fieldCount {
|
|
||||||
StringBuilder_Append(&cbe.sb, " ");
|
|
||||||
var ft: String = mod.structs[si].fields[fi].typeName;
|
|
||||||
if String_Eq(ft, "int") || String_Eq(ft, "") {
|
|
||||||
StringBuilder_Append(&cbe.sb, "int");
|
|
||||||
} else if String_Eq(ft, "String") {
|
|
||||||
StringBuilder_Append(&cbe.sb, "String");
|
|
||||||
} else if String_Eq(ft, "bool") {
|
|
||||||
StringBuilder_Append(&cbe.sb, "bool");
|
|
||||||
} else if String_Eq(ft, "uint32") {
|
|
||||||
StringBuilder_Append(&cbe.sb, "uint32");
|
|
||||||
} else {
|
|
||||||
// Use the type name directly (handles "Foo*", "Bar", etc.)
|
|
||||||
StringBuilder_Append(&cbe.sb, ft);
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, " ");
|
|
||||||
StringBuilder_Append(&cbe.sb, mod.structs[si].fields[fi].name);
|
|
||||||
StringBuilder_Append(&cbe.sb, ";\n");
|
|
||||||
fi = fi + 1;
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, "};\n\n");
|
|
||||||
si = si + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forward declarations for all functions (skip generics)
|
|
||||||
var i: int = 0;
|
|
||||||
while i < mod.funcCount {
|
|
||||||
if !CBE_FuncHasGeneric(&mod.funcs[i]) {
|
|
||||||
CBE_EmitFuncDecl(cbe, &mod.funcs[i]);
|
|
||||||
StringBuilder_Append(&cbe.sb, ";\n");
|
|
||||||
}
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, "\n");
|
|
||||||
|
|
||||||
// Extern declarations
|
|
||||||
i = 0;
|
|
||||||
while i < mod.externCount {
|
|
||||||
CBE_EmitFuncDecl(cbe, &mod.externFuncs[i]);
|
|
||||||
StringBuilder_Append(&cbe.sb, ";\n");
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, "\n");
|
|
||||||
|
|
||||||
// Function definitions (skip generics)
|
|
||||||
var hasMain: bool = false;
|
|
||||||
i = 0;
|
|
||||||
while i < mod.funcCount {
|
|
||||||
if String_Eq(mod.funcs[i].name, "Main") { hasMain = true; }
|
|
||||||
// Skip generic functions
|
|
||||||
if CBE_FuncHasGeneric(&mod.funcs[i]) {
|
|
||||||
i = i + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Skip forward declarations (functions without body)
|
|
||||||
let body: *HirNode = mod.funcs[i].body;
|
|
||||||
if body == null as *HirNode {
|
|
||||||
i = i + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
CBE_EmitFuncDecl(cbe, &mod.funcs[i]);
|
|
||||||
StringBuilder_Append(&cbe.sb, " {\n");
|
|
||||||
// Body
|
|
||||||
var hasReturn: bool = false;
|
|
||||||
cbe.indent = 1;
|
|
||||||
CBE_EmitExpr(cbe, body);
|
|
||||||
cbe.indent = 0;
|
|
||||||
// Check if body has a return statement
|
|
||||||
var stmt: *HirNode = body.child1;
|
|
||||||
while stmt != null as *HirNode {
|
|
||||||
if stmt.kind == hReturn { hasReturn = true; }
|
|
||||||
stmt = stmt.child3;
|
|
||||||
}
|
|
||||||
// Add default return 0 only if function returns int and has no explicit return
|
|
||||||
if String_Eq(mod.funcs[i].retTypeName, "int") && !hasReturn {
|
|
||||||
StringBuilder_Append(&cbe.sb, " return 0;\n");
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cbe.sb, "}\n\n");
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate C main wrapper if Main function exists
|
|
||||||
if hasMain {
|
|
||||||
StringBuilder_Append(&cbe.sb, "extern int g_argc;\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, "extern char** g_argv;\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, "int main(int argc, char** argv) {\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, " g_argc = argc;\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, " g_argv = argv;\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, " return Main();\n");
|
|
||||||
StringBuilder_Append(&cbe.sb, "}\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
return StringBuilder_Build(&cbe.sb);
|
|
||||||
}
|
|
||||||
|
|
||||||
func CBackend_Free(cbe: *CEmitter) {
|
|
||||||
StringBuilder_Free(&cbe.sb);
|
|
||||||
bux_free(cbe as *void);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,761 +0,0 @@
|
|||||||
// cli.bux — CLI driver for the Bux self-hosting compiler
|
|
||||||
// Wires together: Lexer → Parser → Sema → HirLower → CBackend
|
|
||||||
module Cli {
|
|
||||||
|
|
||||||
extern func PrintLine(s: String);
|
|
||||||
extern func Print(s: String);
|
|
||||||
extern func bux_read_file(path: String) -> String;
|
|
||||||
extern func bux_write_file(path: String, content: String) -> bool;
|
|
||||||
extern func bux_file_exists(path: String) -> int;
|
|
||||||
extern func bux_dir_exists(path: String) -> int;
|
|
||||||
extern func bux_path_join(a: String, b: String) -> String;
|
|
||||||
extern func bux_mkdir_if_needed(path: String) -> int;
|
|
||||||
extern func bux_run_nim(nim_file: String, out_bin: String) -> int;
|
|
||||||
extern func bux_list_dir(dir: String, ext: String, out_count: *int) -> *String;
|
|
||||||
extern func bux_system(cmd: String) -> int;
|
|
||||||
|
|
||||||
func ReadFile(path: String) -> String {
|
|
||||||
return bux_read_file(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
func WriteFile(path: String, content: String) -> bool {
|
|
||||||
return bux_write_file(path, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
func FileExists(path: String) -> bool {
|
|
||||||
return bux_file_exists(path) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
func DirExists(path: String) -> bool {
|
|
||||||
return bux_dir_exists(path) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import the compiler pipeline
|
|
||||||
// In self-hosting mode, these are compiled together from compiler/selfhost/
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Compile a single .bux source file
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Cli_Compile(source: String, sourceName: String) -> String {
|
|
||||||
// Phase 1: Lex
|
|
||||||
PrintLine(" Lexing...");
|
|
||||||
let lex: *Lexer = Lexer_Tokenize(source);
|
|
||||||
PrintLine(" Lex done");
|
|
||||||
if Lexer_DiagCount(lex) > 0 {
|
|
||||||
PrintLine("Lex errors:");
|
|
||||||
var i: int = 0;
|
|
||||||
while i < Lexer_DiagCount(lex) {
|
|
||||||
Print(" ");
|
|
||||||
PrintLine(lex.diags[i].message);
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 2: Parse
|
|
||||||
PrintLine(" Parsing...");
|
|
||||||
let mod: *Module = Parser_Parse(lex.tokens, lex.tokenCount);
|
|
||||||
if mod == null as *Module {
|
|
||||||
PrintLine("Parse failed");
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
PrintLine(" Parse done");
|
|
||||||
|
|
||||||
// Flatten module wrappers: find module decl and hoist its children
|
|
||||||
var decl: *Decl = mod.firstItem;
|
|
||||||
while decl != null as *Decl {
|
|
||||||
if decl.kind == dkModule && decl.childDecl1 != null as *Decl {
|
|
||||||
mod.firstItem = decl.childDecl1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
decl = decl.childDecl2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 3: Semantic analysis
|
|
||||||
PrintLine(" Sema...");
|
|
||||||
let sema: *Sema = Sema_Analyze(mod);
|
|
||||||
if Sema_HasError(sema) {
|
|
||||||
PrintLine("Sema errors:");
|
|
||||||
var i: int = 0;
|
|
||||||
while i < Sema_DiagCount(sema) {
|
|
||||||
Print(" ");
|
|
||||||
PrintLine(sema.diags[i].message);
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
PrintLine(" Sema done");
|
|
||||||
|
|
||||||
// Phase 4: HIR lowering
|
|
||||||
PrintLine(" HIR lowering...");
|
|
||||||
let hirMod: *HirModule = HirLower_LowerModule(mod, sema);
|
|
||||||
if hirMod == null as *HirModule {
|
|
||||||
PrintLine("HIR lowering failed");
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
Print("HIR funcCount=");
|
|
||||||
PrintInt(hirMod.funcCount);
|
|
||||||
|
|
||||||
// Phase 5: C code generation
|
|
||||||
let cCode: String = CBackend_Generate(hirMod);
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
Lexer_Free(lex);
|
|
||||||
Sema_Free(sema);
|
|
||||||
|
|
||||||
return cCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Build command
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Cli_Build(srcPath: String, outPath: String) -> int {
|
|
||||||
let source: String = ReadFile(srcPath);
|
|
||||||
if source == null as String || String_Eq(source, "") || !FileExists(srcPath) {
|
|
||||||
Print("Error: cannot read source file: ");
|
|
||||||
PrintLine(srcPath);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Print("Compiling ");
|
|
||||||
PrintLine(srcPath);
|
|
||||||
|
|
||||||
let cCode: String = Cli_Compile(source, srcPath);
|
|
||||||
|
|
||||||
if String_Eq(cCode, "") {
|
|
||||||
PrintLine("Compilation failed");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write C output
|
|
||||||
let cFile: String = String_Concat(outPath, ".c");
|
|
||||||
let ok: bool = WriteFile(cFile, cCode);
|
|
||||||
if !ok {
|
|
||||||
PrintLine("Error: cannot write output file");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
Print(" → C written to ");
|
|
||||||
PrintLine(cFile);
|
|
||||||
|
|
||||||
// Find runtime.c and io.c for linking
|
|
||||||
var rtPath: String = "library/runtime/runtime.c";
|
|
||||||
var ioPath: String = "library/runtime/io.c";
|
|
||||||
if !FileExists(rtPath) {
|
|
||||||
rtPath = "../library/runtime/runtime.c";
|
|
||||||
}
|
|
||||||
if !FileExists(ioPath) {
|
|
||||||
ioPath = "../library/runtime/io.c";
|
|
||||||
}
|
|
||||||
if !FileExists(rtPath) {
|
|
||||||
rtPath = "/home/ziko/z-git/bux/bux/library/runtime/runtime.c";
|
|
||||||
}
|
|
||||||
if !FileExists(ioPath) {
|
|
||||||
ioPath = "/home/ziko/z-git/bux/bux/library/runtime/io.c";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile with cc
|
|
||||||
PrintLine("Compiling C...");
|
|
||||||
var cmdBuf: StringBuilder = StringBuilder_NewCap(512);
|
|
||||||
StringBuilder_Append(&cmdBuf, "cc -O2 -pthread -o ");
|
|
||||||
StringBuilder_Append(&cmdBuf, outPath);
|
|
||||||
StringBuilder_Append(&cmdBuf, " ");
|
|
||||||
StringBuilder_Append(&cmdBuf, cFile);
|
|
||||||
StringBuilder_Append(&cmdBuf, " ");
|
|
||||||
if FileExists(rtPath) {
|
|
||||||
StringBuilder_Append(&cmdBuf, rtPath);
|
|
||||||
StringBuilder_Append(&cmdBuf, " ");
|
|
||||||
}
|
|
||||||
if FileExists(ioPath) {
|
|
||||||
StringBuilder_Append(&cmdBuf, ioPath);
|
|
||||||
StringBuilder_Append(&cmdBuf, " ");
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&cmdBuf, "-lm");
|
|
||||||
let ccRc: int = bux_system(StringBuilder_Build(&cmdBuf));
|
|
||||||
if ccRc != 0 {
|
|
||||||
PrintLine("Error: C compilation failed");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
Print(" → Binary: ");
|
|
||||||
PrintLine(outPath);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Check command (compile only, no output)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Cli_Check(srcPath: String) -> int {
|
|
||||||
Print("Check: ");
|
|
||||||
PrintLine(srcPath);
|
|
||||||
let source: String = ReadFile(srcPath);
|
|
||||||
if source == null as String || String_Eq(source, "") {
|
|
||||||
PrintLine("Error: cannot read source file");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
PrintLine(" File read OK");
|
|
||||||
|
|
||||||
// Phase 1: Lex
|
|
||||||
PrintLine(" Lexing...");
|
|
||||||
let lex: *Lexer = Lexer_Tokenize(source);
|
|
||||||
PrintLine(" Lex done");
|
|
||||||
if Lexer_DiagCount(lex) > 0 {
|
|
||||||
PrintLine("Lex errors found");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 2: Parse
|
|
||||||
let mod: *Module = Parser_Parse(lex.tokens, lex.tokenCount);
|
|
||||||
if mod == null as *Module {
|
|
||||||
PrintLine("Parse failed");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
PrintLine(" Parse done");
|
|
||||||
|
|
||||||
// Flatten module wrappers
|
|
||||||
var decl2: *Decl = mod.firstItem;
|
|
||||||
while decl2 != null as *Decl {
|
|
||||||
if decl2.kind == dkModule && decl2.childDecl1 != null as *Decl {
|
|
||||||
mod.firstItem = decl2.childDecl1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
decl2 = decl2.childDecl2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 3: Sema
|
|
||||||
let sema: *Sema = Sema_Analyze(mod);
|
|
||||||
if Sema_HasError(sema) {
|
|
||||||
PrintLine("Sema errors found");
|
|
||||||
var i: int = 0;
|
|
||||||
while i < Sema_DiagCount(sema) {
|
|
||||||
Print(" line ");
|
|
||||||
PrintInt(sema.diags[i].line as int64);
|
|
||||||
Print(": ");
|
|
||||||
PrintLine(sema.diags[i].message);
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 4: HIR lowering
|
|
||||||
let hirMod: *HirModule = HirLower_LowerModule(mod, sema);
|
|
||||||
|
|
||||||
var dc: *Decl = mod.firstItem;
|
|
||||||
while dc != null as *Decl {
|
|
||||||
if dc.kind == dkFunc {
|
|
||||||
Print("check func ");
|
|
||||||
PrintLine(dc.strValue);
|
|
||||||
}
|
|
||||||
dc = dc.childDecl2;
|
|
||||||
}
|
|
||||||
PrintLine("Check passed");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Project build — compile all .bux files in src/ directory
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Cli_CompileSource(source: String, sourceName: String) -> *HirModule {
|
|
||||||
// Phase 1: Lex
|
|
||||||
PrintLine(" Lexing...");
|
|
||||||
let lex: *Lexer = Lexer_Tokenize(source);
|
|
||||||
PrintLine(" Lex done");
|
|
||||||
if Lexer_DiagCount(lex) > 0 {
|
|
||||||
Print("Lex errors in ");
|
|
||||||
PrintLine(sourceName);
|
|
||||||
return null as *HirModule;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 2: Parse
|
|
||||||
let mod: *Module = Parser_Parse(lex.tokens, lex.tokenCount);
|
|
||||||
if mod == null as *Module {
|
|
||||||
Print("Parse failed for ");
|
|
||||||
PrintLine(sourceName);
|
|
||||||
return null as *HirModule;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 3: Semantic analysis
|
|
||||||
let sema: *Sema = Sema_Analyze(mod);
|
|
||||||
if Sema_HasError(sema) {
|
|
||||||
Print("Sema errors in ");
|
|
||||||
PrintLine(sourceName);
|
|
||||||
return null as *HirModule;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 4: HIR lowering
|
|
||||||
let hirMod: *HirModule = HirLower_LowerModule(mod, sema);
|
|
||||||
return hirMod;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Stdlib discovery and merging
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Cli_FindStdlibDir(projectDir: String) -> String {
|
|
||||||
var path: String = bux_path_join(projectDir, "stdlib");
|
|
||||||
if DirExists(path) { return path; }
|
|
||||||
path = bux_path_join(projectDir, "../stdlib");
|
|
||||||
if DirExists(path) { return path; }
|
|
||||||
path = "/home/ziko/z-git/bux/bux/stdlib";
|
|
||||||
if DirExists(path) { return path; }
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
func Cli_DeclName(decl: *Decl) -> String {
|
|
||||||
if decl == null as *Decl { return ""; }
|
|
||||||
return decl.strValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Cli_NameInList(name: String, names: *String, count: int) -> bool {
|
|
||||||
if String_Eq(name, "") { return false; }
|
|
||||||
var i: int = 0;
|
|
||||||
while i < count {
|
|
||||||
if String_Eq(names[i], name) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Cli_CollectNames(mod: *Module, names: *String, maxCount: int) -> int {
|
|
||||||
if mod == null as *Module { return 0; }
|
|
||||||
var count: int = 0;
|
|
||||||
var decl: *Decl = mod.firstItem;
|
|
||||||
while decl != null as *Decl && count < maxCount {
|
|
||||||
let next: *Decl = decl.childDecl2;
|
|
||||||
if decl.kind == dkModule {
|
|
||||||
var inner: *Decl = decl.childDecl1;
|
|
||||||
while inner != null as *Decl && count < maxCount {
|
|
||||||
let iname: String = Cli_DeclName(inner);
|
|
||||||
if !String_Eq(iname, "") {
|
|
||||||
names[count] = iname;
|
|
||||||
count = count + 1;
|
|
||||||
}
|
|
||||||
inner = inner.childDecl2;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let dname: String = Cli_DeclName(decl);
|
|
||||||
if !String_Eq(dname, "") {
|
|
||||||
names[count] = dname;
|
|
||||||
count = count + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
decl = next;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: convert single char int to String
|
|
||||||
func String_FromChar(c: int) -> String {
|
|
||||||
var buf: *char8 = bux_alloc(2) as *char8;
|
|
||||||
buf[0] = c as char8;
|
|
||||||
buf[1] = 0 as char8;
|
|
||||||
return buf as String;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect stdlib module paths imported by user code.
|
|
||||||
// Returns number of unique stdlib files to merge (paths written into outPaths).
|
|
||||||
func Cli_CollectStdlibImports(mod: *Module, outPaths: *String, maxCount: int, stdlibDir: String) -> int {
|
|
||||||
if mod == null as *Module { return 0; }
|
|
||||||
var count: int = 0;
|
|
||||||
var decl: *Decl = mod.firstItem;
|
|
||||||
while decl != null as *Decl && count < maxCount {
|
|
||||||
if decl.kind == dkUse {
|
|
||||||
let path: String = decl.usePath;
|
|
||||||
// Only handle Std::* imports
|
|
||||||
if String_StartsWith(path, "Std::") {
|
|
||||||
// Extract module name after Std:: (e.g., "Std::Io" -> "Io")
|
|
||||||
var j: int = 5; // skip "Std::"
|
|
||||||
var modName: String = "";
|
|
||||||
while j < 100 {
|
|
||||||
let c: int = path[j] as int;
|
|
||||||
if c == 0 || c == 58 { break; } // null or ':'
|
|
||||||
modName = String_Concat(modName, String_FromChar(c));
|
|
||||||
j = j + 1;
|
|
||||||
}
|
|
||||||
if !String_Eq(modName, "") {
|
|
||||||
let filePath: String = bux_path_join(bux_path_join(stdlibDir, "Std"), String_Concat(modName, ".bux"));
|
|
||||||
// Check for duplicates
|
|
||||||
var dup: bool = false;
|
|
||||||
var di: int = 0;
|
|
||||||
while di < count {
|
|
||||||
if String_Eq(outPaths[di], filePath) { dup = true; }
|
|
||||||
di = di + 1;
|
|
||||||
}
|
|
||||||
if !dup {
|
|
||||||
outPaths[count] = filePath;
|
|
||||||
count = count + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
decl = decl.childDecl2;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Cli_MergeFileInto(target: *Module, path: String, skipNames: *String, skipCount: int) -> int {
|
|
||||||
if !FileExists(path) {
|
|
||||||
Print("Error: stdlib file not found: ");
|
|
||||||
PrintLine(path);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let source: String = ReadFile(path);
|
|
||||||
if source == null as String || String_Eq(source, "") { return 0; }
|
|
||||||
let lex: *Lexer = Lexer_Tokenize(source);
|
|
||||||
if Lexer_DiagCount(lex) > 0 { return 0; }
|
|
||||||
let mod: *Module = Parser_Parse(lex.tokens, lex.tokenCount);
|
|
||||||
if mod == null as *Module { return 0; }
|
|
||||||
|
|
||||||
var added: int = 0;
|
|
||||||
var decl: *Decl = mod.firstItem;
|
|
||||||
while decl != null as *Decl {
|
|
||||||
let next: *Decl = decl.childDecl2;
|
|
||||||
decl.childDecl2 = null as *Decl;
|
|
||||||
if decl.kind == dkModule {
|
|
||||||
var inner: *Decl = decl.childDecl1;
|
|
||||||
while inner != null as *Decl {
|
|
||||||
let innerNext: *Decl = inner.childDecl2;
|
|
||||||
let iname: String = Cli_DeclName(inner);
|
|
||||||
if !String_Eq(iname, "") && Cli_NameInList(iname, skipNames, skipCount) {
|
|
||||||
// Skip shadowed stdlib decl
|
|
||||||
} else {
|
|
||||||
inner.childDecl2 = target.firstItem;
|
|
||||||
target.firstItem = inner;
|
|
||||||
target.itemCount = target.itemCount + 1;
|
|
||||||
added = added + 1;
|
|
||||||
}
|
|
||||||
inner = innerNext;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let dname: String = Cli_DeclName(decl);
|
|
||||||
if !String_Eq(dname, "") && Cli_NameInList(dname, skipNames, skipCount) {
|
|
||||||
// Skip shadowed stdlib decl
|
|
||||||
} else {
|
|
||||||
decl.childDecl2 = target.firstItem;
|
|
||||||
target.firstItem = decl;
|
|
||||||
target.itemCount = target.itemCount + 1;
|
|
||||||
added = added + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
decl = next;
|
|
||||||
}
|
|
||||||
return added;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Cli_CopyModuleDecls(target: *Module, source: *Module) {
|
|
||||||
if source == null as *Module || source.firstItem == null as *Decl { return; }
|
|
||||||
var decl: *Decl = source.firstItem;
|
|
||||||
var limit: int = 0;
|
|
||||||
while decl != null as *Decl && limit < 10000 {
|
|
||||||
let next: *Decl = decl.childDecl2;
|
|
||||||
decl.childDecl2 = target.firstItem;
|
|
||||||
target.firstItem = decl;
|
|
||||||
target.itemCount = target.itemCount + 1;
|
|
||||||
decl = next;
|
|
||||||
limit = limit + 1;
|
|
||||||
}
|
|
||||||
source.firstItem = null as *Decl;
|
|
||||||
source.itemCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Cli_BuildProject(projectDir: String) -> int {
|
|
||||||
let man: Manifest = Manifest_Load(bux_path_join(projectDir, "bux.toml"));
|
|
||||||
var outName: String = man.name;
|
|
||||||
if String_Eq(outName, "") {
|
|
||||||
outName = "bux_out";
|
|
||||||
}
|
|
||||||
let srcDir: String = bux_path_join(projectDir, "src");
|
|
||||||
if !DirExists(srcDir) {
|
|
||||||
Print("Error: no src/ directory in ");
|
|
||||||
PrintLine(projectDir);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Print("Scanning ");
|
|
||||||
PrintLine(srcDir);
|
|
||||||
|
|
||||||
// List all .bux files in src/
|
|
||||||
var fileCount: int = 0;
|
|
||||||
let files: *String = bux_list_dir(srcDir, ".bux", &fileCount);
|
|
||||||
if fileCount == 0 {
|
|
||||||
PrintLine("Error: no .bux files found in src/");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Print("Found ");
|
|
||||||
PrintInt(fileCount);
|
|
||||||
PrintLine(" source files");
|
|
||||||
|
|
||||||
// Create user merged module (collect user decls first)
|
|
||||||
let userMerged: *Module = bux_alloc(sizeof(Module)) as *Module;
|
|
||||||
userMerged.name = "main";
|
|
||||||
userMerged.path = "";
|
|
||||||
userMerged.itemCount = 0;
|
|
||||||
userMerged.firstItem = null as *Decl;
|
|
||||||
|
|
||||||
// Parse each file and merge declarations into userMerged module
|
|
||||||
var i: int = 0;
|
|
||||||
while i < fileCount {
|
|
||||||
let path: String = files[i];
|
|
||||||
Print(" Parsing ");
|
|
||||||
PrintLine(path);
|
|
||||||
let source: String = ReadFile(path);
|
|
||||||
if String_Eq(source, "") {
|
|
||||||
Print(" Error: cannot read ");
|
|
||||||
PrintLine(path);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
let lex: *Lexer = Lexer_Tokenize(source);
|
|
||||||
if Lexer_DiagCount(lex) > 0 {
|
|
||||||
Print(" Lex errors in ");
|
|
||||||
PrintLine(path);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
let mod: *Module = Parser_Parse(lex.tokens, lex.tokenCount);
|
|
||||||
if mod == null as *Module {
|
|
||||||
Print(" Parse failed for ");
|
|
||||||
PrintLine(path);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
// Merge declarations from this module into userMerged
|
|
||||||
var decl: *Decl = mod.firstItem;
|
|
||||||
while decl != null as *Decl {
|
|
||||||
let next: *Decl = decl.childDecl2;
|
|
||||||
decl.childDecl2 = null as *Decl;
|
|
||||||
// Flatten module declarations
|
|
||||||
if decl.kind == dkModule {
|
|
||||||
var inner: *Decl = decl.childDecl1;
|
|
||||||
while inner != null as *Decl {
|
|
||||||
let innerNext: *Decl = inner.childDecl2;
|
|
||||||
inner.childDecl2 = userMerged.firstItem;
|
|
||||||
userMerged.firstItem = inner;
|
|
||||||
userMerged.itemCount = userMerged.itemCount + 1;
|
|
||||||
inner = innerNext;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
decl.childDecl2 = userMerged.firstItem;
|
|
||||||
userMerged.firstItem = decl;
|
|
||||||
userMerged.itemCount = userMerged.itemCount + 1;
|
|
||||||
}
|
|
||||||
decl = next;
|
|
||||||
}
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect user declaration names for shadow detection
|
|
||||||
let maxNames: int = 2048;
|
|
||||||
let userNames: *String = bux_alloc(maxNames * 8) as *String;
|
|
||||||
let userNameCount: int = Cli_CollectNames(userMerged, userNames, maxNames);
|
|
||||||
|
|
||||||
// Create final merged module
|
|
||||||
let merged: *Module = bux_alloc(sizeof(Module)) as *Module;
|
|
||||||
merged.name = "main";
|
|
||||||
merged.path = "";
|
|
||||||
merged.itemCount = 0;
|
|
||||||
merged.firstItem = null as *Decl;
|
|
||||||
|
|
||||||
// Find and merge stdlib declarations (only imported modules)
|
|
||||||
let stdlibDir: String = Cli_FindStdlibDir(projectDir);
|
|
||||||
if !String_Eq(stdlibDir, "") {
|
|
||||||
Print("Stdlib found: ");
|
|
||||||
PrintLine(stdlibDir);
|
|
||||||
// Collect imported stdlib modules from user code
|
|
||||||
let maxImports: int = 64;
|
|
||||||
let importPaths: *String = bux_alloc(maxImports * 8) as *String;
|
|
||||||
let importCount: int = Cli_CollectStdlibImports(userMerged, importPaths, maxImports, stdlibDir);
|
|
||||||
Print("Imported stdlib modules: ");
|
|
||||||
PrintInt(importCount);
|
|
||||||
PrintLine("");
|
|
||||||
if importCount > 0 {
|
|
||||||
var si: int = 0;
|
|
||||||
var stdAdded: int = 0;
|
|
||||||
while si < importCount {
|
|
||||||
Print(" Merging ");
|
|
||||||
PrintLine(importPaths[si]);
|
|
||||||
let added: int = Cli_MergeFileInto(merged, importPaths[si], userNames, userNameCount);
|
|
||||||
stdAdded = stdAdded + added;
|
|
||||||
si = si + 1;
|
|
||||||
}
|
|
||||||
Print("Stdlib declarations added: ");
|
|
||||||
PrintInt(stdAdded);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy user declarations into merged (user shadows stdlib)
|
|
||||||
Cli_CopyModuleDecls(merged, userMerged);
|
|
||||||
|
|
||||||
Print("Merged ");
|
|
||||||
PrintInt(merged.itemCount);
|
|
||||||
PrintLine(" declarations");
|
|
||||||
|
|
||||||
// Semantic analysis
|
|
||||||
PrintLine("Running sema...");
|
|
||||||
let sema: *Sema = Sema_Analyze(merged);
|
|
||||||
if Sema_HasError(sema) {
|
|
||||||
PrintLine("Sema errors found");
|
|
||||||
var i: int = 0;
|
|
||||||
while i < Sema_DiagCount(sema) {
|
|
||||||
Print(" line ");
|
|
||||||
PrintInt(sema.diags[i].line as int64);
|
|
||||||
Print(": ");
|
|
||||||
PrintLine(sema.diags[i].message);
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// HIR lowering
|
|
||||||
PrintLine("Lowering to HIR...");
|
|
||||||
let hirMod: *HirModule = HirLower_LowerModule(merged, sema);
|
|
||||||
|
|
||||||
// C code generation
|
|
||||||
PrintLine("Generating C code...");
|
|
||||||
let cCode: String = CBackend_Generate(hirMod);
|
|
||||||
|
|
||||||
// Create build directory
|
|
||||||
let buildDir: String = bux_path_join(projectDir, "build");
|
|
||||||
discard bux_mkdir_if_needed(buildDir);
|
|
||||||
|
|
||||||
// Write C output
|
|
||||||
let cFile: String = bux_path_join(buildDir, "main.c");
|
|
||||||
if !WriteFile(cFile, cCode) {
|
|
||||||
PrintLine("Error: cannot write main.c");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
Print("C code written to ");
|
|
||||||
PrintLine(cFile);
|
|
||||||
|
|
||||||
// Find runtime.c and io.c (use stdlibDir if available)
|
|
||||||
var rtPath: String = bux_path_join(stdlibDir, "runtime.c");
|
|
||||||
var ioPath: String = bux_path_join(stdlibDir, "io.c");
|
|
||||||
if !FileExists(rtPath) {
|
|
||||||
rtPath = bux_path_join(projectDir, "library/runtime/runtime.c");
|
|
||||||
}
|
|
||||||
if !FileExists(ioPath) {
|
|
||||||
ioPath = bux_path_join(projectDir, "library/runtime/io.c");
|
|
||||||
}
|
|
||||||
if !FileExists(rtPath) {
|
|
||||||
rtPath = bux_path_join(projectDir, "../library/runtime/runtime.c");
|
|
||||||
}
|
|
||||||
if !FileExists(ioPath) {
|
|
||||||
ioPath = bux_path_join(projectDir, "../library/runtime/io.c");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile with cc
|
|
||||||
PrintLine("Compiling C...");
|
|
||||||
let outBin: String = bux_path_join(buildDir, outName);
|
|
||||||
var ccBuf: StringBuilder = StringBuilder_NewCap(512);
|
|
||||||
StringBuilder_Append(&ccBuf, "cc -O2 -pthread -o ");
|
|
||||||
StringBuilder_Append(&ccBuf, outBin);
|
|
||||||
StringBuilder_Append(&ccBuf, " ");
|
|
||||||
StringBuilder_Append(&ccBuf, cFile);
|
|
||||||
StringBuilder_Append(&ccBuf, " ");
|
|
||||||
if FileExists(rtPath) {
|
|
||||||
StringBuilder_Append(&ccBuf, rtPath);
|
|
||||||
StringBuilder_Append(&ccBuf, " ");
|
|
||||||
}
|
|
||||||
if FileExists(ioPath) {
|
|
||||||
StringBuilder_Append(&ccBuf, ioPath);
|
|
||||||
StringBuilder_Append(&ccBuf, " ");
|
|
||||||
}
|
|
||||||
StringBuilder_Append(&ccBuf, "-lm");
|
|
||||||
let ccRc: int = bux_system(StringBuilder_Build(&ccBuf));
|
|
||||||
if ccRc != 0 {
|
|
||||||
PrintLine("Error: C compilation failed");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Print("Build successful: ");
|
|
||||||
PrintLine(outBin);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Run command — build project and execute
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Cli_RunProject(projectDir: String) -> int {
|
|
||||||
let rc: int = Cli_BuildProject(projectDir);
|
|
||||||
if rc != 0 {
|
|
||||||
return rc;
|
|
||||||
}
|
|
||||||
let man: Manifest = Manifest_Load(bux_path_join(projectDir, "bux.toml"));
|
|
||||||
var outName: String = man.name;
|
|
||||||
if String_Eq(outName, "") {
|
|
||||||
outName = "bux_out";
|
|
||||||
}
|
|
||||||
let outBin: String = bux_path_join(bux_path_join(projectDir, "build"), outName);
|
|
||||||
Print("Running: ");
|
|
||||||
PrintLine(outBin);
|
|
||||||
return bux_system(outBin);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Main entry — dispatch based on args
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Cli_Run(args: *String, argCount: int) -> int {
|
|
||||||
if argCount < 2 {
|
|
||||||
PrintLine("Bux Self-Hosting Compiler v0.2.0");
|
|
||||||
PrintLine("Usage: buxc <command> [args]");
|
|
||||||
PrintLine("Commands: build, check, run, project, help, version");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cmd: String = args[1];
|
|
||||||
if String_Eq(cmd, "version") || String_Eq(cmd, "--version") || String_Eq(cmd, "-v") {
|
|
||||||
PrintLine("Bux 0.2.0 (self-hosting bootstrap)");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if String_Eq(cmd, "help") || String_Eq(cmd, "--help") || String_Eq(cmd, "-h") {
|
|
||||||
PrintLine("Bux Self-Hosting Compiler v0.2.0");
|
|
||||||
PrintLine("Usage: buxc <command> [args]");
|
|
||||||
PrintLine("Commands: build, check, run, project, help, version");
|
|
||||||
PrintLine("Pipeline modules:");
|
|
||||||
PrintLine(" Lexer ✅ 695 lines");
|
|
||||||
PrintLine(" Parser ✅ 1004 lines");
|
|
||||||
PrintLine(" Sema ✅ 393 lines");
|
|
||||||
PrintLine(" HirLower ✅ 307 lines");
|
|
||||||
PrintLine(" CBackend ✅ 585 lines");
|
|
||||||
PrintLine(" Total: 3830 lines of Bux");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if String_Eq(cmd, "check") {
|
|
||||||
if argCount < 3 {
|
|
||||||
PrintLine("Usage: buxc check <file.bux>");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return Cli_Check(args[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if String_Eq(cmd, "build") {
|
|
||||||
let src: String = "src/Main.bux";
|
|
||||||
let out: String = "build/main";
|
|
||||||
if argCount >= 3 { src = args[2]; }
|
|
||||||
if argCount >= 4 { out = args[3]; }
|
|
||||||
return Cli_Build(src, out);
|
|
||||||
}
|
|
||||||
|
|
||||||
if String_Eq(cmd, "project") {
|
|
||||||
let dir: String = ".";
|
|
||||||
if argCount >= 3 { dir = args[2]; }
|
|
||||||
return Cli_BuildProject(dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
if String_Eq(cmd, "run") {
|
|
||||||
let dir: String = ".";
|
|
||||||
if argCount >= 3 { dir = args[2]; }
|
|
||||||
return Cli_RunProject(dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
Print("Unknown command: ");
|
|
||||||
PrintLine(cmd);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
// hir.bux — HIR (High-level Intermediate Representation) node types
|
|
||||||
module Hir {
|
|
||||||
|
|
||||||
// HIR node kinds
|
|
||||||
const hLit: int = 0;
|
|
||||||
const hVar: int = 1;
|
|
||||||
const hSelf: int = 2;
|
|
||||||
const hUnary: int = 3;
|
|
||||||
const hBinary: int = 4;
|
|
||||||
const hAssign: int = 5;
|
|
||||||
const hIf: int = 6;
|
|
||||||
const hWhile: int = 7;
|
|
||||||
const hLoop: int = 8;
|
|
||||||
const hBreak: int = 9;
|
|
||||||
const hContinue: int = 10;
|
|
||||||
const hReturn: int = 11;
|
|
||||||
const hAlloca: int = 12;
|
|
||||||
const hLoad: int = 13;
|
|
||||||
const hStore: int = 14;
|
|
||||||
const hFieldPtr: int = 15;
|
|
||||||
const hArrowField: int = 16;
|
|
||||||
const hIndexPtr: int = 17;
|
|
||||||
const hCall: int = 18;
|
|
||||||
const hCallIndirect: int = 19;
|
|
||||||
const hCast: int = 20;
|
|
||||||
const hIs: int = 21;
|
|
||||||
const hSizeOf: int = 22;
|
|
||||||
const hBlock: int = 23;
|
|
||||||
const hStructInit: int = 24;
|
|
||||||
const hSliceInit: int = 25;
|
|
||||||
const hRange: int = 26;
|
|
||||||
const hTupleInit: int = 27;
|
|
||||||
const hMatch: int = 28;
|
|
||||||
const hSpawn: int = 29;
|
|
||||||
const hAwait: int = 30;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// HirArgList — linked list for call arguments beyond 2
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
struct HirArgList {
|
|
||||||
node: *HirNode,
|
|
||||||
next: *HirArgList,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// HirNode — unified struct with tagged union pattern
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
struct HirNode {
|
|
||||||
kind: int;
|
|
||||||
line: uint32;
|
|
||||||
column: uint32;
|
|
||||||
typeKind: int;
|
|
||||||
typeName: String;
|
|
||||||
// Common fields (used by multiple kinds)
|
|
||||||
strValue: String; // var name; callee name; field name; label
|
|
||||||
intValue: int; // token kind (for lit; unary op; binary op)
|
|
||||||
boolValue: bool; // range inclusive; isScope
|
|
||||||
// Child nodes (up to 3)
|
|
||||||
child1: *HirNode; // left/operand/condition/base
|
|
||||||
child2: *HirNode; // right/value/then/body
|
|
||||||
child3: *HirNode; // else/third
|
|
||||||
// Extra data pointer (for children arrays, field lists, etc.)
|
|
||||||
extraData: *void;
|
|
||||||
extraCount: int;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// HirFunc
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
struct HirParam {
|
|
||||||
name: String;
|
|
||||||
typeKind: int;
|
|
||||||
typeName: String;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct HirFunc {
|
|
||||||
name: String;
|
|
||||||
paramCount: int;
|
|
||||||
param0: HirParam;
|
|
||||||
param1: HirParam;
|
|
||||||
param2: HirParam;
|
|
||||||
param3: HirParam;
|
|
||||||
param4: HirParam;
|
|
||||||
param5: HirParam;
|
|
||||||
param6: HirParam;
|
|
||||||
param7: HirParam;
|
|
||||||
param8: HirParam;
|
|
||||||
retTypeKind: int;
|
|
||||||
retTypeName: String;
|
|
||||||
body: *HirNode;
|
|
||||||
isPublic: bool;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// HirEnumVariant
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
struct HirEnumVariant {
|
|
||||||
name: String;
|
|
||||||
fieldCount: int;
|
|
||||||
fieldType0: int;
|
|
||||||
fieldName0: String;
|
|
||||||
fieldType1: int;
|
|
||||||
fieldName1: String;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// HirModule
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
struct HirStructField {
|
|
||||||
name: String;
|
|
||||||
typeName: String;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct HirStruct {
|
|
||||||
name: String;
|
|
||||||
fieldCount: int;
|
|
||||||
fields: *HirStructField;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct HirConst {
|
|
||||||
name: String;
|
|
||||||
value: int;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct HirEnum {
|
|
||||||
name: String;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct HirModule {
|
|
||||||
funcCount: int;
|
|
||||||
funcs: *HirFunc;
|
|
||||||
externCount: int;
|
|
||||||
externFuncs: *HirFunc;
|
|
||||||
structCount: int;
|
|
||||||
structs: *HirStruct;
|
|
||||||
enumCount: int;
|
|
||||||
enums: *HirEnum;
|
|
||||||
constCount: int;
|
|
||||||
consts: *HirConst;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Constructor helpers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Hir_MakeNode(kind: int, line: uint32, column: uint32) -> HirNode {
|
|
||||||
return HirNode { kind: kind, line: line, column: column,
|
|
||||||
typeKind: 0, typeName: "",
|
|
||||||
strValue: "", intValue: 0, boolValue: false,
|
|
||||||
child1: null as *HirNode, child2: null as *HirNode, child3: null as *HirNode,
|
|
||||||
extraData: null as *void, extraCount: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
func Hir_MakeLit(tokKind: int, tokText: String, line: uint32, col: uint32) -> HirNode {
|
|
||||||
var n: HirNode = Hir_MakeNode(hLit, line, col);
|
|
||||||
n.intValue = tokKind;
|
|
||||||
n.strValue = tokText;
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Hir_MakeVar(name: String, line: uint32, col: uint32) -> HirNode {
|
|
||||||
var n: HirNode = Hir_MakeNode(hVar, line, col);
|
|
||||||
n.strValue = name;
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Hir_MakeBinary(op: int, left: *HirNode, right: *HirNode, line: uint32, col: uint32) -> HirNode {
|
|
||||||
var n: HirNode = Hir_MakeNode(hBinary, line, col);
|
|
||||||
n.intValue = op;
|
|
||||||
n.child1 = left;
|
|
||||||
n.child2 = right;
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Hir_MakeCall(callee: String, line: uint32, col: uint32) -> HirNode {
|
|
||||||
var n: HirNode = Hir_MakeNode(hCall, line, col);
|
|
||||||
n.strValue = callee;
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Hir_MakeReturn(value: *HirNode, line: uint32, col: uint32) -> HirNode {
|
|
||||||
var n: HirNode = Hir_MakeNode(hReturn, line, col);
|
|
||||||
n.child1 = value;
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Hir_MakeBlock(line: uint32, col: uint32) -> HirNode {
|
|
||||||
return Hir_MakeNode(hBlock, line, col);
|
|
||||||
}
|
|
||||||
|
|
||||||
func Hir_MakeIf(cond: *HirNode, thenBody: *HirNode, elseBody: *HirNode, line: uint32, col: uint32) -> HirNode {
|
|
||||||
var n: HirNode = Hir_MakeNode(hIf, line, col);
|
|
||||||
n.child1 = cond;
|
|
||||||
n.child2 = thenBody;
|
|
||||||
n.child3 = elseBody;
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Hir_MakeWhile(cond: *HirNode, body: *HirNode, line: uint32, col: uint32) -> HirNode {
|
|
||||||
var n: HirNode = Hir_MakeNode(hWhile, line, col);
|
|
||||||
n.child1 = cond;
|
|
||||||
n.child2 = body;
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Hir_MakeAlloca(name: String, line: uint32, col: uint32) -> HirNode {
|
|
||||||
var n: HirNode = Hir_MakeNode(hAlloca, line, col);
|
|
||||||
n.strValue = name;
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Hir_MakeLoad(ptr: *HirNode, line: uint32, col: uint32) -> HirNode {
|
|
||||||
var n: HirNode = Hir_MakeNode(hLoad, line, col);
|
|
||||||
n.child1 = ptr;
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Hir_MakeStore(ptr: *HirNode, value: *HirNode, line: uint32, col: uint32) -> HirNode {
|
|
||||||
var n: HirNode = Hir_MakeNode(hStore, line, col);
|
|
||||||
n.child1 = ptr;
|
|
||||||
n.child2 = value;
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,740 +0,0 @@
|
|||||||
// hir_lower.bux — HIR lowering: AST → HIR transformation (ported from hir_lower.nim)
|
|
||||||
// Transforms the typed AST into a lower-level IR suitable for code generation.
|
|
||||||
module HirLower {
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Lowering context
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
struct LowerCtx {
|
|
||||||
module: *Module,
|
|
||||||
scope: *Scope,
|
|
||||||
funcs: *HirFunc,
|
|
||||||
funcCount: int,
|
|
||||||
externFuncs: *HirFunc,
|
|
||||||
externCount: int,
|
|
||||||
varCounter: int,
|
|
||||||
tryCounter: int,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// TypeExpr.kind → Type.kind resolver
|
|
||||||
// TypeExpr.kind values (0-5) overlap with Type.kind values — this
|
|
||||||
// resolves the correct Type.kind for codegen.
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Lcx_ResolveTypeKind(te: *TypeExpr) -> int {
|
|
||||||
if te == null as *TypeExpr { return tyUnknown; }
|
|
||||||
let name: String = te.typeName;
|
|
||||||
|
|
||||||
if te.kind == tekPointer { return tyPointer; }
|
|
||||||
if te.kind == tekSlice { return tySlice; }
|
|
||||||
if te.kind == tekTuple { return tyTuple; }
|
|
||||||
|
|
||||||
// Named types: resolve by name
|
|
||||||
if String_Eq(name, "void") { return tyVoid; }
|
|
||||||
if String_Eq(name, "bool") { return tyBool; }
|
|
||||||
if String_Eq(name, "bool8") { return tyBool8; }
|
|
||||||
if String_Eq(name, "bool16") { return tyBool16; }
|
|
||||||
if String_Eq(name, "bool32") { return tyBool32; }
|
|
||||||
if String_Eq(name, "char8") { return tyChar8; }
|
|
||||||
if String_Eq(name, "char16") { return tyChar16; }
|
|
||||||
if String_Eq(name, "char32") { return tyChar32; }
|
|
||||||
if String_Eq(name, "String") { return tyStr; }
|
|
||||||
if String_Eq(name, "str") { return tyStr; }
|
|
||||||
if String_Eq(name, "int8") { return tyInt8; }
|
|
||||||
if String_Eq(name, "int16") { return tyInt16; }
|
|
||||||
if String_Eq(name, "int32") { return tyInt32; }
|
|
||||||
if String_Eq(name, "int64") { return tyInt64; }
|
|
||||||
if String_Eq(name, "int") { return tyInt; }
|
|
||||||
if String_Eq(name, "uint8") { return tyUInt8; }
|
|
||||||
if String_Eq(name, "uint16") { return tyUInt16; }
|
|
||||||
if String_Eq(name, "uint32") { return tyUInt32; }
|
|
||||||
if String_Eq(name, "uint64") { return tyUInt64; }
|
|
||||||
if String_Eq(name, "uint") { return tyUInt; }
|
|
||||||
if String_Eq(name, "float32") { return tyFloat32; }
|
|
||||||
if String_Eq(name, "float64") { return tyFloat64; }
|
|
||||||
if String_Eq(name, "float") { return tyFloat64; }
|
|
||||||
|
|
||||||
return tyNamed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Fresh name generation
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Lcx_FreshName(ctx: *LowerCtx) -> String {
|
|
||||||
ctx.varCounter = ctx.varCounter + 1;
|
|
||||||
return String_FromInt(ctx.varCounter as int64);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Type → HIR type
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Lcx_LowerType(typeKind: int, typeName: String) -> int {
|
|
||||||
return typeKind;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Expression lowering
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
|
|
||||||
if expr == null as *Expr { return null as *HirNode; }
|
|
||||||
|
|
||||||
let line: uint32 = expr.line;
|
|
||||||
let col: uint32 = expr.column;
|
|
||||||
let n: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
|
||||||
n.kind = hBlock;
|
|
||||||
n.line = line;
|
|
||||||
n.column = col;
|
|
||||||
|
|
||||||
let kind: int = expr.kind;
|
|
||||||
|
|
||||||
// Literal
|
|
||||||
if kind == ekLiteral {
|
|
||||||
n.kind = hLit;
|
|
||||||
n.intValue = expr.tokKind;
|
|
||||||
n.strValue = expr.tokText;
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Identifier → variable reference
|
|
||||||
if kind == ekIdent {
|
|
||||||
n.kind = hVar;
|
|
||||||
n.strValue = expr.strValue;
|
|
||||||
let sym: Symbol = Scope_Lookup(ctx.scope, expr.strValue);
|
|
||||||
n.typeKind = sym.typeKind;
|
|
||||||
if expr.refType != null as *TypeExpr {
|
|
||||||
n.typeName = expr.refType.typeName;
|
|
||||||
}
|
|
||||||
if sym.typeName != null as String && !String_Eq(sym.typeName, "") {
|
|
||||||
n.typeName = sym.typeName;
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// self → variable reference named "self"
|
|
||||||
if kind == ekSelf {
|
|
||||||
n.kind = hVar;
|
|
||||||
n.strValue = "self";
|
|
||||||
let sym: Symbol = Scope_Lookup(ctx.scope, "self");
|
|
||||||
n.typeKind = sym.typeKind;
|
|
||||||
if sym.typeName != null as String && !String_Eq(sym.typeName, "") {
|
|
||||||
n.typeName = sym.typeName;
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Binary
|
|
||||||
if kind == ekBinary {
|
|
||||||
// Assignment operator → use hAssign
|
|
||||||
if expr.intValue == tkAssign {
|
|
||||||
n.kind = hAssign;
|
|
||||||
n.child1 = Lcx_LowerExpr(ctx, expr.child1);
|
|
||||||
n.child2 = Lcx_LowerExpr(ctx, expr.child2);
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
n.kind = hBinary;
|
|
||||||
n.intValue = expr.intValue; // operator
|
|
||||||
n.child1 = Lcx_LowerExpr(ctx, expr.child1);
|
|
||||||
n.child2 = Lcx_LowerExpr(ctx, expr.child2);
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unary
|
|
||||||
if kind == ekUnary {
|
|
||||||
n.kind = hUnary;
|
|
||||||
n.intValue = expr.intValue;
|
|
||||||
n.child1 = Lcx_LowerExpr(ctx, expr.child1);
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call
|
|
||||||
if kind == ekCall {
|
|
||||||
n.kind = hCall;
|
|
||||||
// Method call desugaring: obj.method(args) → Type_method(obj, args)
|
|
||||||
if expr.child1 != null as *Expr && expr.child1.kind == ekField {
|
|
||||||
let methodName: String = expr.child1.strValue;
|
|
||||||
var receiverTypeName: String = "";
|
|
||||||
if expr.child1.child1 != null as *Expr && expr.child1.child1.kind == ekIdent {
|
|
||||||
let sym: Symbol = Scope_Lookup(ctx.scope, expr.child1.child1.strValue);
|
|
||||||
receiverTypeName = sym.typeName;
|
|
||||||
}
|
|
||||||
if String_Eq(receiverTypeName, "") && expr.child1.child1 != null as *Expr && expr.child1.child1.refType != null as *TypeExpr {
|
|
||||||
receiverTypeName = expr.child1.child1.refType.typeName;
|
|
||||||
}
|
|
||||||
if !String_Eq(receiverTypeName, "") {
|
|
||||||
// Strip trailing '*' from pointer type names (e.g. "Box*" → "Box")
|
|
||||||
var baseName: String = receiverTypeName;
|
|
||||||
let len: int = bux_strlen(baseName) as int;
|
|
||||||
if len > 0 {
|
|
||||||
let lastChar: String = bux_str_slice(baseName, (len - 1) as uint, 1);
|
|
||||||
if String_Eq(lastChar, "*") {
|
|
||||||
baseName = bux_str_slice(baseName, 0, (len - 1) as uint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
n.strValue = String_Concat(baseName, "_");
|
|
||||||
n.strValue = String_Concat(n.strValue, methodName);
|
|
||||||
}
|
|
||||||
// Lower receiver as first argument
|
|
||||||
let recv: *HirNode = Lcx_LowerExpr(ctx, expr.child1.child1);
|
|
||||||
n.child1 = recv;
|
|
||||||
// Lower remaining arguments from linked list
|
|
||||||
var arg: *ExprList = expr.callArgs;
|
|
||||||
var argIdx: int = 0;
|
|
||||||
while arg != null as *ExprList {
|
|
||||||
let lowered: *HirNode = Lcx_LowerExpr(ctx, arg.expr);
|
|
||||||
if argIdx == 0 {
|
|
||||||
n.child2 = lowered;
|
|
||||||
} else if argIdx == 1 {
|
|
||||||
// Third argument — start linked list
|
|
||||||
let firstExtra: *HirArgList = bux_alloc(sizeof(HirArgList)) as *HirArgList;
|
|
||||||
firstExtra.node = lowered;
|
|
||||||
firstExtra.next = null as *HirArgList;
|
|
||||||
n.extraData = firstExtra as *void;
|
|
||||||
n.extraCount = 1;
|
|
||||||
} else {
|
|
||||||
// Additional args — append to linked list
|
|
||||||
var cur: *HirArgList = n.extraData as *HirArgList;
|
|
||||||
while cur.next != null as *HirArgList {
|
|
||||||
cur = cur.next;
|
|
||||||
}
|
|
||||||
let newNode: *HirArgList = bux_alloc(sizeof(HirArgList)) as *HirArgList;
|
|
||||||
newNode.node = lowered;
|
|
||||||
newNode.next = null as *HirArgList;
|
|
||||||
cur.next = newNode;
|
|
||||||
n.extraCount = n.extraCount + 1;
|
|
||||||
}
|
|
||||||
arg = arg.next;
|
|
||||||
argIdx = argIdx + 1;
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
// Get callee name
|
|
||||||
if expr.child1 != null as *Expr && expr.child1.kind == ekIdent {
|
|
||||||
n.strValue = expr.child1.strValue;
|
|
||||||
}
|
|
||||||
// Lower arguments from linked list
|
|
||||||
var arg: *ExprList = expr.callArgs;
|
|
||||||
var argIdx: int = 0;
|
|
||||||
while arg != null as *ExprList {
|
|
||||||
let lowered: *HirNode = Lcx_LowerExpr(ctx, arg.expr);
|
|
||||||
if argIdx == 0 {
|
|
||||||
n.child1 = lowered;
|
|
||||||
} else if argIdx == 1 {
|
|
||||||
n.child2 = lowered;
|
|
||||||
} else if argIdx == 2 {
|
|
||||||
// Third argument — start linked list
|
|
||||||
let firstExtra: *HirArgList = bux_alloc(sizeof(HirArgList)) as *HirArgList;
|
|
||||||
firstExtra.node = lowered;
|
|
||||||
firstExtra.next = null as *HirArgList;
|
|
||||||
n.extraData = firstExtra as *void;
|
|
||||||
n.extraCount = 1;
|
|
||||||
} else {
|
|
||||||
// Additional args — append to linked list
|
|
||||||
var cur: *HirArgList = n.extraData as *HirArgList;
|
|
||||||
while cur.next != null as *HirArgList {
|
|
||||||
cur = cur.next;
|
|
||||||
}
|
|
||||||
let newNode: *HirArgList = bux_alloc(sizeof(HirArgList)) as *HirArgList;
|
|
||||||
newNode.node = lowered;
|
|
||||||
newNode.next = null as *HirArgList;
|
|
||||||
cur.next = newNode;
|
|
||||||
n.extraCount = n.extraCount + 1;
|
|
||||||
}
|
|
||||||
arg = arg.next;
|
|
||||||
argIdx = argIdx + 1;
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sizeof
|
|
||||||
if kind == ekSizeOf {
|
|
||||||
n.kind = hSizeOf;
|
|
||||||
if expr.refType != null as *TypeExpr {
|
|
||||||
n.typeName = expr.refType.typeName;
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Field access
|
|
||||||
if kind == ekField {
|
|
||||||
// Check if this is enum variant access: Color::Green
|
|
||||||
if expr.child1 != null as *Expr && expr.child1.kind == ekIdent {
|
|
||||||
let sym: Symbol = Scope_Lookup(ctx.scope, expr.child1.strValue);
|
|
||||||
if sym.decl != null as *Decl && sym.decl.kind == dkEnum {
|
|
||||||
// Emit as variable reference: Color_Green
|
|
||||||
n.kind = hVar;
|
|
||||||
n.strValue = String_Concat(String_Concat(expr.child1.strValue, "_"), expr.strValue);
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
n.kind = hFieldPtr;
|
|
||||||
n.child1 = Lcx_LowerExpr(ctx, expr.child1);
|
|
||||||
n.strValue = expr.strValue;
|
|
||||||
// Get struct type from base expr refType
|
|
||||||
if expr.child1 != null as *Expr && expr.child1.refType != null as *TypeExpr {
|
|
||||||
n.typeName = expr.child1.refType.typeName;
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// spawn Callee(args)
|
|
||||||
if kind == ekSpawn {
|
|
||||||
n.kind = hSpawn;
|
|
||||||
if expr.child1 != null as *Expr && expr.child1.kind == ekIdent {
|
|
||||||
n.strValue = expr.child1.strValue;
|
|
||||||
}
|
|
||||||
if expr.child2 != null as *Expr {
|
|
||||||
n.child1 = Lcx_LowerExpr(ctx, expr.child2);
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// expr.await
|
|
||||||
if kind == ekAwait {
|
|
||||||
n.kind = hAwait;
|
|
||||||
n.child1 = Lcx_LowerExpr(ctx, expr.child1);
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Index: arr[idx]
|
|
||||||
if kind == ekIndex {
|
|
||||||
n.kind = hIndexPtr;
|
|
||||||
n.child1 = Lcx_LowerExpr(ctx, expr.child1);
|
|
||||||
n.child2 = Lcx_LowerExpr(ctx, expr.child2);
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign: target = value
|
|
||||||
if kind == ekAssign {
|
|
||||||
n.kind = hAssign;
|
|
||||||
n.child1 = Lcx_LowerExpr(ctx, expr.child1); // target
|
|
||||||
n.child2 = Lcx_LowerExpr(ctx, expr.child2); // value
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cast
|
|
||||||
if kind == ekCast {
|
|
||||||
n.kind = hCast;
|
|
||||||
n.child1 = Lcx_LowerExpr(ctx, expr.child1);
|
|
||||||
if expr.refType != null as *TypeExpr {
|
|
||||||
let resolvedKind: int = Lcx_ResolveTypeKind(expr.refType);
|
|
||||||
n.typeKind = resolvedKind;
|
|
||||||
// For pointer types, construct "PointeeType*"
|
|
||||||
if expr.refType.kind == tekPointer && expr.refType.pointerPointee != null as *TypeExpr {
|
|
||||||
n.typeName = String_Concat(expr.refType.pointerPointee.typeName, "*");
|
|
||||||
} else if !String_Eq(expr.refType.typeName, "") {
|
|
||||||
n.typeName = expr.refType.typeName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Struct init: TypeName { field: value, ... }
|
|
||||||
if kind == ekStructInit {
|
|
||||||
n.kind = hStructInit;
|
|
||||||
n.strValue = expr.structName;
|
|
||||||
// Lower each field (fields are chained via child3 on synthetic ekField exprs)
|
|
||||||
var field: *Expr = expr.child1;
|
|
||||||
var firstField: *HirNode = null as *HirNode;
|
|
||||||
var lastField: *HirNode = null as *HirNode;
|
|
||||||
while field != null as *Expr {
|
|
||||||
let fNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
|
||||||
fNode.kind = hBlock; // placeholder, field is identified by name+value
|
|
||||||
fNode.line = expr.line;
|
|
||||||
fNode.column = expr.column;
|
|
||||||
fNode.strValue = field.strValue; // field name
|
|
||||||
fNode.child1 = Lcx_LowerExpr(ctx, field.child1); // field value
|
|
||||||
if firstField == null as *HirNode {
|
|
||||||
firstField = fNode;
|
|
||||||
lastField = fNode;
|
|
||||||
} else {
|
|
||||||
lastField.child3 = fNode;
|
|
||||||
lastField = fNode;
|
|
||||||
}
|
|
||||||
field = field.child3;
|
|
||||||
}
|
|
||||||
n.child1 = firstField;
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Statement lowering
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Lcx_LowerStmt(ctx: *LowerCtx, stmt: *Stmt) -> *HirNode {
|
|
||||||
if stmt == null as *Stmt { return null as *HirNode; }
|
|
||||||
|
|
||||||
let line: uint32 = stmt.line;
|
|
||||||
let col: uint32 = stmt.column;
|
|
||||||
let n: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
|
||||||
n.kind = hBlock;
|
|
||||||
n.line = line;
|
|
||||||
n.column = col;
|
|
||||||
|
|
||||||
let kind: int = stmt.kind;
|
|
||||||
|
|
||||||
// Let/var → alloca + store
|
|
||||||
if kind == skLet {
|
|
||||||
let init: *HirNode = Lcx_LowerExpr(ctx, stmt.child1);
|
|
||||||
// alloca for the variable
|
|
||||||
let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
|
||||||
alloca.kind = hAlloca;
|
|
||||||
alloca.line = line;
|
|
||||||
alloca.column = col;
|
|
||||||
alloca.strValue = stmt.strValue;
|
|
||||||
// Set type from the declared type expression
|
|
||||||
alloca.typeName = "";
|
|
||||||
if stmt.refStmtType != null as *TypeExpr {
|
|
||||||
alloca.intValue = stmt.refStmtType.kind;
|
|
||||||
alloca.typeKind = Lcx_ResolveTypeKind(stmt.refStmtType);
|
|
||||||
// For pointer types, construct "PointeeType*"
|
|
||||||
if stmt.refStmtType.kind == tekPointer && stmt.refStmtType.pointerPointee != null as *TypeExpr {
|
|
||||||
alloca.typeName = String_Concat(stmt.refStmtType.pointerPointee.typeName, "*");
|
|
||||||
} else if !String_Eq(stmt.refStmtType.typeName, "") {
|
|
||||||
alloca.typeName = stmt.refStmtType.typeName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Add to scope for field offset lookups
|
|
||||||
var sym: Symbol;
|
|
||||||
sym.kind = skVar;
|
|
||||||
sym.name = stmt.strValue;
|
|
||||||
sym.typeKind = alloca.typeKind;
|
|
||||||
sym.typeName = alloca.typeName;
|
|
||||||
sym.isMutable = false;
|
|
||||||
sym.isPublic = false;
|
|
||||||
sym.decl = null as *Decl;
|
|
||||||
discard Scope_Define(ctx.scope, sym);
|
|
||||||
// store the init value
|
|
||||||
n.kind = hStore;
|
|
||||||
n.child1 = alloca;
|
|
||||||
n.child2 = init;
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return
|
|
||||||
if kind == skReturn {
|
|
||||||
n.kind = hReturn;
|
|
||||||
if stmt.child1 != null as *Expr {
|
|
||||||
n.child1 = Lcx_LowerExpr(ctx, stmt.child1);
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expression statement
|
|
||||||
if kind == skExpr && stmt.child1 != null as *Expr {
|
|
||||||
return Lcx_LowerExpr(ctx, stmt.child1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If
|
|
||||||
if kind == skIf {
|
|
||||||
n.kind = hIf;
|
|
||||||
n.child1 = Lcx_LowerExpr(ctx, stmt.child1); // condition
|
|
||||||
if stmt.refStmtBlock != null as *Block {
|
|
||||||
n.child2 = Lcx_LowerBlock(ctx, stmt.refStmtBlock, -1);
|
|
||||||
}
|
|
||||||
if stmt.refStmtElse != null as *Block {
|
|
||||||
// Store else block in extraData (child3 is reserved for block chaining)
|
|
||||||
n.extraData = Lcx_LowerBlock(ctx, stmt.refStmtElse, -1) as *void;
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// While
|
|
||||||
if kind == skWhile {
|
|
||||||
n.kind = hWhile;
|
|
||||||
n.child1 = Lcx_LowerExpr(ctx, stmt.child1);
|
|
||||||
if stmt.refStmtBlock != null as *Block {
|
|
||||||
n.child2 = Lcx_LowerBlock(ctx, stmt.refStmtBlock, -1);
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop
|
|
||||||
if kind == skLoop {
|
|
||||||
n.kind = hLoop;
|
|
||||||
if stmt.refStmtBlock != null as *Block {
|
|
||||||
n.child1 = Lcx_LowerBlock(ctx, stmt.refStmtBlock, -1);
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Block lowering
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Lcx_LowerBlock(ctx: *LowerCtx, block: *Block, retTypeKind: int) -> *HirNode {
|
|
||||||
if block == null as *Block { return null as *HirNode; }
|
|
||||||
if block.stmtCount == 0 { return null as *HirNode; }
|
|
||||||
|
|
||||||
// Build a linked list of HirNodes via child3:
|
|
||||||
// node1 (stmt1) → child3 → node2 (stmt2) → child3 → node3 (stmt3) → null
|
|
||||||
// child3 is safe for chaining because:
|
|
||||||
// hStore: child1=alloca, child2=value, child3 unused
|
|
||||||
// hReturn: child1=value, child2/child3 unused
|
|
||||||
// hCall: child1=arg1, child2=arg2, child3 unused
|
|
||||||
var firstNode: *HirNode = null as *HirNode;
|
|
||||||
var prevNode: *HirNode = null as *HirNode;
|
|
||||||
var stmt: *Stmt = block.firstStmt;
|
|
||||||
while stmt != null as *Stmt {
|
|
||||||
let lowered: *HirNode = Lcx_LowerStmt(ctx, stmt);
|
|
||||||
if lowered != null as *HirNode {
|
|
||||||
if firstNode == null as *HirNode {
|
|
||||||
firstNode = lowered;
|
|
||||||
prevNode = lowered;
|
|
||||||
} else {
|
|
||||||
prevNode.child3 = lowered;
|
|
||||||
prevNode = lowered;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stmt = stmt.nextStmt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap in an hBlock node with child1 = first statement in chain
|
|
||||||
let n: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
|
||||||
n.kind = hBlock;
|
|
||||||
n.line = block.line;
|
|
||||||
n.column = block.column;
|
|
||||||
n.boolValue = true;
|
|
||||||
n.child1 = firstNode;
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Param → HirParam conversion
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Lcx_LowerParam(out: *HirParam, p: *Param) {
|
|
||||||
out.name = p.name;
|
|
||||||
if p.refParamType != null as *TypeExpr {
|
|
||||||
out.typeKind = Lcx_ResolveTypeKind(p.refParamType);
|
|
||||||
// Use typeName directly if set (parser may have constructed "String*")
|
|
||||||
if !String_Eq(p.refParamType.typeName, "") {
|
|
||||||
out.typeName = p.refParamType.typeName;
|
|
||||||
} else if p.refParamType.kind == tekPointer && p.refParamType.pointerPointee != null as *TypeExpr {
|
|
||||||
out.typeName = String_Concat(p.refParamType.pointerPointee.typeName, "*");
|
|
||||||
} else {
|
|
||||||
out.typeName = "";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
out.typeKind = 0;
|
|
||||||
out.typeName = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Function lowering
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Lcx_LowerFunc(ctx: *LowerCtx, decl: *Decl) -> *HirFunc {
|
|
||||||
let f: *HirFunc = bux_alloc(sizeof(HirFunc)) as *HirFunc;
|
|
||||||
f.name = decl.strValue;
|
|
||||||
f.isPublic = decl.isPublic;
|
|
||||||
f.paramCount = decl.paramCount;
|
|
||||||
Lcx_LowerParam(&f.param0, &decl.param0);
|
|
||||||
Lcx_LowerParam(&f.param1, &decl.param1);
|
|
||||||
Lcx_LowerParam(&f.param2, &decl.param2);
|
|
||||||
Lcx_LowerParam(&f.param3, &decl.param3);
|
|
||||||
Lcx_LowerParam(&f.param4, &decl.param4);
|
|
||||||
Lcx_LowerParam(&f.param5, &decl.param5);
|
|
||||||
Lcx_LowerParam(&f.param6, &decl.param6);
|
|
||||||
Lcx_LowerParam(&f.param7, &decl.param7);
|
|
||||||
Lcx_LowerParam(&f.param8, &decl.param8);
|
|
||||||
|
|
||||||
if decl.retType != null as *TypeExpr {
|
|
||||||
f.retTypeName = decl.retType.typeName;
|
|
||||||
f.retTypeKind = Lcx_ResolveTypeKind(decl.retType);
|
|
||||||
} else {
|
|
||||||
f.retTypeName = "";
|
|
||||||
f.retTypeKind = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add parameters to hir_lower scope for field offset lookups
|
|
||||||
var pi: int = 0;
|
|
||||||
while pi < decl.paramCount {
|
|
||||||
var p: *Param = null as *Param;
|
|
||||||
if pi == 0 { p = &decl.param0; }
|
|
||||||
else if pi == 1 { p = &decl.param1; }
|
|
||||||
else if pi == 2 { p = &decl.param2; }
|
|
||||||
else if pi == 3 { p = &decl.param3; }
|
|
||||||
else if pi == 4 { p = &decl.param4; }
|
|
||||||
else if pi == 5 { p = &decl.param5; }
|
|
||||||
else if pi == 6 { p = &decl.param6; }
|
|
||||||
else if pi == 7 { p = &decl.param7; }
|
|
||||||
else if pi == 8 { p = &decl.param8; }
|
|
||||||
if p != null as *Param && p.refParamType != null as *TypeExpr {
|
|
||||||
var sym: Symbol;
|
|
||||||
sym.kind = skVar;
|
|
||||||
sym.name = p.name;
|
|
||||||
sym.typeKind = Lcx_ResolveTypeKind(p.refParamType);
|
|
||||||
sym.typeName = p.refParamType.typeName;
|
|
||||||
sym.isMutable = false;
|
|
||||||
sym.isPublic = false;
|
|
||||||
sym.decl = null as *Decl;
|
|
||||||
discard Scope_Define(ctx.scope, sym);
|
|
||||||
}
|
|
||||||
pi = pi + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if decl.refBody != null as *Block {
|
|
||||||
f.body = Lcx_LowerBlock(ctx, decl.refBody, -1);
|
|
||||||
} else {
|
|
||||||
f.body = null as *HirNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
return f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Module lowering — main entry point
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func HirLower_LowerModule(mod: *Module, sema: *Sema) -> *HirModule {
|
|
||||||
let ctx: *LowerCtx = bux_alloc(sizeof(LowerCtx)) as *LowerCtx;
|
|
||||||
ctx.module = mod;
|
|
||||||
ctx.scope = sema.scope;
|
|
||||||
ctx.funcs = bux_alloc(256 as uint * sizeof(HirFunc)) as *HirFunc;
|
|
||||||
ctx.funcCount = 0;
|
|
||||||
ctx.externFuncs = bux_alloc(256 as uint * sizeof(HirFunc)) as *HirFunc;
|
|
||||||
ctx.externCount = 0;
|
|
||||||
ctx.varCounter = 0;
|
|
||||||
|
|
||||||
let hm: *HirModule = bux_alloc(sizeof(HirModule)) as *HirModule;
|
|
||||||
hm.funcCount = 0;
|
|
||||||
hm.funcs = ctx.funcs;
|
|
||||||
hm.structCount = 0;
|
|
||||||
hm.structs = bux_alloc(64 as uint * sizeof(HirStruct)) as *HirStruct;
|
|
||||||
hm.enumCount = 0;
|
|
||||||
hm.enums = bux_alloc(64 as uint * sizeof(HirEnum)) as *HirEnum;
|
|
||||||
hm.constCount = 0;
|
|
||||||
hm.consts = bux_alloc(512 as uint * sizeof(HirConst)) as *HirConst;
|
|
||||||
|
|
||||||
// First pass: count structs (to allocate field arrays later)
|
|
||||||
// Second pass: actually collect them
|
|
||||||
// For simplicity, do single pass with pre-allocated field arrays
|
|
||||||
|
|
||||||
// Iterate declarations
|
|
||||||
var decl: *Decl = mod.firstItem;
|
|
||||||
while decl != null as *Decl {
|
|
||||||
if decl.kind == dkFunc {
|
|
||||||
let f: *HirFunc = Lcx_LowerFunc(ctx, decl);
|
|
||||||
ctx.funcs[ctx.funcCount] = *f;
|
|
||||||
ctx.funcCount = ctx.funcCount + 1;
|
|
||||||
}
|
|
||||||
if decl.kind == dkImpl {
|
|
||||||
let implTypeName: String = decl.strValue;
|
|
||||||
var implDecl: *Decl = decl.childDecl1;
|
|
||||||
while implDecl != null as *Decl {
|
|
||||||
if implDecl.kind == dkFunc {
|
|
||||||
let mangled: String = String_Concat(implTypeName, "_");
|
|
||||||
implDecl.strValue = String_Concat(mangled, implDecl.strValue);
|
|
||||||
let f: *HirFunc = Lcx_LowerFunc(ctx, implDecl);
|
|
||||||
ctx.funcs[ctx.funcCount] = *f;
|
|
||||||
ctx.funcCount = ctx.funcCount + 1;
|
|
||||||
}
|
|
||||||
implDecl = implDecl.childDecl2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if decl.kind == dkExternFunc {
|
|
||||||
let f: *HirFunc = Lcx_LowerFunc(ctx, decl);
|
|
||||||
ctx.externFuncs[ctx.externCount] = *f;
|
|
||||||
ctx.externCount = ctx.externCount + 1;
|
|
||||||
}
|
|
||||||
if decl.kind == dkStruct {
|
|
||||||
// Collect struct definition for C codegen
|
|
||||||
let si: int = hm.structCount;
|
|
||||||
hm.structs[si].name = decl.strValue;
|
|
||||||
hm.structs[si].fieldCount = decl.fieldCount;
|
|
||||||
hm.structs[si].fields = bux_alloc(decl.fieldCount as uint * sizeof(HirStructField)) as *HirStructField;
|
|
||||||
var fi: int = 0;
|
|
||||||
while fi < decl.fieldCount {
|
|
||||||
var fname: String = "";
|
|
||||||
var ftype: *TypeExpr = null as *TypeExpr;
|
|
||||||
if fi == 0 { fname = decl.field0.name; ftype = decl.field0.refFieldType; }
|
|
||||||
else if fi == 1 { fname = decl.field1.name; ftype = decl.field1.refFieldType; }
|
|
||||||
else if fi == 2 { fname = decl.field2.name; ftype = decl.field2.refFieldType; }
|
|
||||||
else if fi == 3 { fname = decl.field3.name; ftype = decl.field3.refFieldType; }
|
|
||||||
else if fi == 4 { fname = decl.field4.name; ftype = decl.field4.refFieldType; }
|
|
||||||
else if fi == 5 { fname = decl.field5.name; ftype = decl.field5.refFieldType; }
|
|
||||||
else if fi == 6 { fname = decl.field6.name; ftype = decl.field6.refFieldType; }
|
|
||||||
else if fi == 7 { fname = decl.field7.name; ftype = decl.field7.refFieldType; }
|
|
||||||
// Skip empty field names
|
|
||||||
if String_Eq(fname, "") {
|
|
||||||
fi = fi + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
hm.structs[si].fields[fi].name = fname;
|
|
||||||
if ftype != null as *TypeExpr {
|
|
||||||
if ftype.kind == tekPointer && ftype.pointerPointee != null as *TypeExpr {
|
|
||||||
// Pointer type: emit "TypeName*"
|
|
||||||
if !String_Eq(ftype.pointerPointee.typeName, "") {
|
|
||||||
hm.structs[si].fields[fi].typeName = String_Concat(ftype.pointerPointee.typeName, "*");
|
|
||||||
}
|
|
||||||
} else if !String_Eq(ftype.typeName, "") {
|
|
||||||
hm.structs[si].fields[fi].typeName = ftype.typeName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fi = fi + 1;
|
|
||||||
}
|
|
||||||
hm.structCount = hm.structCount + 1;
|
|
||||||
}
|
|
||||||
if decl.kind == dkConst && hm.constCount < 512 {
|
|
||||||
let ci: int = hm.constCount;
|
|
||||||
hm.consts[ci].name = decl.strValue;
|
|
||||||
var val: int = 0;
|
|
||||||
if decl.constValue != null as *Expr {
|
|
||||||
if decl.constValue.kind == ekLiteral {
|
|
||||||
val = decl.constValue.intValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hm.consts[ci].value = val;
|
|
||||||
hm.constCount = hm.constCount + 1;
|
|
||||||
}
|
|
||||||
if decl.kind == dkEnum {
|
|
||||||
let ei: int = hm.enumCount;
|
|
||||||
hm.enums[ei].name = decl.strValue;
|
|
||||||
hm.enumCount = hm.enumCount + 1;
|
|
||||||
// Emit enum variants as constants: EnumName_VariantName = value
|
|
||||||
var vi: int = 0;
|
|
||||||
while vi < decl.variantCount && hm.constCount < 512 {
|
|
||||||
var vname: String = "";
|
|
||||||
if vi == 0 { vname = decl.variant0.name; }
|
|
||||||
if vi == 1 { vname = decl.variant1.name; }
|
|
||||||
if vi == 2 { vname = decl.variant2.name; }
|
|
||||||
if vi == 3 { vname = decl.variant3.name; }
|
|
||||||
if !String_Eq(vname, "") {
|
|
||||||
let ci: int = hm.constCount;
|
|
||||||
hm.consts[ci].name = String_Concat(String_Concat(decl.strValue, "_"), vname);
|
|
||||||
hm.consts[ci].value = vi;
|
|
||||||
hm.constCount = hm.constCount + 1;
|
|
||||||
}
|
|
||||||
vi = vi + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
decl = decl.childDecl2;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
hm.funcCount = ctx.funcCount;
|
|
||||||
hm.funcs = ctx.funcs;
|
|
||||||
hm.externCount = ctx.externCount;
|
|
||||||
hm.externFuncs = ctx.externFuncs;
|
|
||||||
|
|
||||||
|
|
||||||
return hm;
|
|
||||||
}
|
|
||||||
|
|
||||||
func HirLower_Free(ctx: *LowerCtx) {
|
|
||||||
bux_free(ctx.funcs as *void);
|
|
||||||
bux_free(ctx.externFuncs as *void);
|
|
||||||
bux_free(ctx as *void);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,816 +0,0 @@
|
|||||||
// lexer.bux — Lexer state machine (ported from lexer.nim)
|
|
||||||
// Tokenizes Bux source into a stream of tokens.
|
|
||||||
module Lexer {
|
|
||||||
|
|
||||||
extern func bux_strlen(s: String) -> uint;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Character helpers (wrap C ctype)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Lex_IsDigit(c: uint32) -> bool {
|
|
||||||
return c >= 48 && c <= 57; // '0'..'9'
|
|
||||||
}
|
|
||||||
|
|
||||||
func Lex_IsHexDigit(c: uint32) -> bool {
|
|
||||||
if c >= 48 && c <= 57 { return true; } // 0-9
|
|
||||||
if c >= 65 && c <= 70 { return true; } // A-F
|
|
||||||
if c >= 97 && c <= 102 { return true; } // a-f
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Lex_IsBinDigit(c: uint32) -> bool {
|
|
||||||
return c == 48 || c == 49; // '0' or '1'
|
|
||||||
}
|
|
||||||
|
|
||||||
func Lex_IsOctDigit(c: uint32) -> bool {
|
|
||||||
return c >= 48 && c <= 55; // '0'..'7'
|
|
||||||
}
|
|
||||||
|
|
||||||
func Lex_IsIdentStart(c: uint32) -> bool {
|
|
||||||
if c >= 97 && c <= 122 { return true; } // a-z
|
|
||||||
if c >= 65 && c <= 90 { return true; } // A-Z
|
|
||||||
return c == 95; // '_'
|
|
||||||
}
|
|
||||||
|
|
||||||
func Lex_IsIdentChar(c: uint32) -> bool {
|
|
||||||
if Lex_IsIdentStart(c) { return true; }
|
|
||||||
return Lex_IsDigit(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Lexer state
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const maxTokens: int = 32768;
|
|
||||||
const maxDiags: int = 128;
|
|
||||||
|
|
||||||
struct LexerDiag {
|
|
||||||
line: uint32;
|
|
||||||
column: uint32;
|
|
||||||
message: String;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Lexer {
|
|
||||||
source: String;
|
|
||||||
sourceLen: int;
|
|
||||||
pos: int;
|
|
||||||
line: uint32;
|
|
||||||
column: uint32;
|
|
||||||
startLine: uint32;
|
|
||||||
startColumn: uint32;
|
|
||||||
startPos: int;
|
|
||||||
tokenCount: int;
|
|
||||||
tokens: *LexToken;
|
|
||||||
diagCount: int;
|
|
||||||
diags: *LexerDiag;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct LexToken {
|
|
||||||
kind: int;
|
|
||||||
text: String;
|
|
||||||
line: uint32;
|
|
||||||
column: uint32;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Init
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Lexer_Init(source: String) -> Lexer {
|
|
||||||
let len: uint = bux_strlen(source);
|
|
||||||
let tokBuf: *LexToken = bux_alloc(maxTokens as uint * sizeof(LexToken)) as *LexToken;
|
|
||||||
let diagBuf: *LexerDiag = bux_alloc(maxDiags as uint * sizeof(LexerDiag)) as *LexerDiag;
|
|
||||||
return Lexer {
|
|
||||||
source: source, sourceLen: len as int,
|
|
||||||
pos: 0, line: 1, column: 1,
|
|
||||||
startLine: 0, startColumn: 0, startPos: 0,
|
|
||||||
tokenCount: 0, tokens: tokBuf,
|
|
||||||
diagCount: 0, diags: diagBuf
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Core primitives
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func lexIsAtEnd(lex: *Lexer) -> bool {
|
|
||||||
return lex.pos >= lex.sourceLen;
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexPeek(lex: *Lexer, ahead: int) -> uint32 {
|
|
||||||
let i: int = lex.pos + ahead;
|
|
||||||
if i < lex.sourceLen {
|
|
||||||
return (lex.source[i] as uint32) & 255;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexAdvance(lex: *Lexer) -> uint32 {
|
|
||||||
let c: uint32 = lexPeek(lex, 0);
|
|
||||||
if !lexIsAtEnd(lex) {
|
|
||||||
lex.pos = lex.pos + 1;
|
|
||||||
if c == 10 { // '\n'
|
|
||||||
lex.line = lex.line + 1;
|
|
||||||
lex.column = 1;
|
|
||||||
} else {
|
|
||||||
lex.column = lex.column + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexMatch(lex: *Lexer, expected: uint32) -> bool {
|
|
||||||
if lexIsAtEnd(lex) { return false; }
|
|
||||||
if lexPeek(lex, 0) != expected { return false; }
|
|
||||||
discard lexAdvance(lex);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexMatchStr(lex: *Lexer, s: String) -> bool {
|
|
||||||
let len: uint = bux_strlen(s);
|
|
||||||
var i: int = 0;
|
|
||||||
while i < (len as int) {
|
|
||||||
if lexPeek(lex, i) != (s[i] as uint32) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
i = 0;
|
|
||||||
while i < (len as int) {
|
|
||||||
discard lexAdvance(lex);
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Token emission
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func lexMarkStart(lex: *Lexer) {
|
|
||||||
lex.startLine = lex.line;
|
|
||||||
lex.startColumn = lex.column;
|
|
||||||
lex.startPos = lex.pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexMakeToken(lex: *Lexer, kind: int) -> LexToken {
|
|
||||||
var text: String = "";
|
|
||||||
let endPos: int = lex.pos;
|
|
||||||
let startPos: int = lex.startPos;
|
|
||||||
if endPos > startPos {
|
|
||||||
let len: int = endPos - startPos;
|
|
||||||
let buf: *char8 = bux_alloc((len + 1) as uint) as *char8;
|
|
||||||
var i: int = 0;
|
|
||||||
while i < len {
|
|
||||||
buf[i] = lex.source[startPos + i] as char8;
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
buf[len] = 0 as char8;
|
|
||||||
text = buf;
|
|
||||||
} else {
|
|
||||||
text = "";
|
|
||||||
}
|
|
||||||
return LexToken {
|
|
||||||
kind: kind, text: text,
|
|
||||||
line: lex.startLine, column: lex.startColumn
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexEmitToken(lex: *Lexer, kind: int) {
|
|
||||||
let tok: LexToken = lexMakeToken(lex, kind);
|
|
||||||
if lex.tokenCount < maxTokens {
|
|
||||||
lex.tokens[lex.tokenCount] = tok;
|
|
||||||
lex.tokenCount = lex.tokenCount + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexSetLastTokenText(lex: *Lexer, text: String) {
|
|
||||||
if lex.tokenCount > 0 {
|
|
||||||
lex.tokens[lex.tokenCount - 1].text = text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexEmitDiag(lex: *Lexer, msg: String) {
|
|
||||||
if lex.diagCount < maxDiags {
|
|
||||||
lex.diags[lex.diagCount] = LexerDiag {
|
|
||||||
line: lex.line, column: lex.column, message: msg
|
|
||||||
};
|
|
||||||
lex.diagCount = lex.diagCount + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Whitespace / comments
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func lexSkipLineComment(lex: *Lexer) {
|
|
||||||
while !lexIsAtEnd(lex) && lexPeek(lex, 0) != 10 { // '\n'
|
|
||||||
discard lexAdvance(lex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexSkipBlockComment(lex: *Lexer) {
|
|
||||||
var depth: int = 1;
|
|
||||||
while !lexIsAtEnd(lex) && depth > 0 {
|
|
||||||
if lexPeek(lex, 0) == 47 && lexPeek(lex, 1) == 42 { // /*
|
|
||||||
discard lexAdvance(lex);
|
|
||||||
discard lexAdvance(lex);
|
|
||||||
depth = depth + 1;
|
|
||||||
} else if lexPeek(lex, 0) == 42 && lexPeek(lex, 1) == 47 { // */
|
|
||||||
discard lexAdvance(lex);
|
|
||||||
discard lexAdvance(lex);
|
|
||||||
depth = depth - 1;
|
|
||||||
} else {
|
|
||||||
discard lexAdvance(lex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if depth > 0 {
|
|
||||||
lexEmitDiag(lex, "unterminated block comment");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexSkipWhitespace(lex: *Lexer) {
|
|
||||||
while !lexIsAtEnd(lex) {
|
|
||||||
let c: uint32 = lexPeek(lex, 0);
|
|
||||||
if c == 32 || c == 9 || c == 13 {
|
|
||||||
discard lexAdvance(lex);
|
|
||||||
} else {
|
|
||||||
if c == 47 && lexPeek(lex, 1) == 47 {
|
|
||||||
lexSkipLineComment(lex);
|
|
||||||
} else {
|
|
||||||
if c == 47 && lexPeek(lex, 1) == 42 {
|
|
||||||
discard lexAdvance(lex);
|
|
||||||
discard lexAdvance(lex);
|
|
||||||
lexSkipBlockComment(lex);
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Identifiers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func lexIsIdentKind(tok: int) -> bool {
|
|
||||||
return tok == tkFunc || tok == tkLet || tok == tkVar || tok == tkConst
|
|
||||||
|| tok == tkType || tok == tkStruct || tok == tkEnum || tok == tkUnion
|
|
||||||
|| tok == tkInterface || tok == tkExtend || tok == tkModule || tok == tkImport
|
|
||||||
|| tok == tkPub || tok == tkExtern || tok == tkIf || tok == tkElse
|
|
||||||
|| tok == tkWhile || tok == tkDo || tok == tkLoop || tok == tkFor
|
|
||||||
|| tok == tkIn || tok == tkBreak || tok == tkContinue || tok == tkReturn
|
|
||||||
|| tok == tkMatch || tok == tkAs || tok == tkIs || tok == tkNull
|
|
||||||
|| tok == tkSelf || tok == tkSuper;
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexKeywordKind(text: String) -> int {
|
|
||||||
if String_Eq(text, "true") { return tkBoolLiteral; }
|
|
||||||
if String_Eq(text, "false") { return tkBoolLiteral; }
|
|
||||||
if String_Eq(text, "func") { return tkFunc; }
|
|
||||||
if String_Eq(text, "let") { return tkLet; }
|
|
||||||
if String_Eq(text, "var") { return tkVar; }
|
|
||||||
if String_Eq(text, "const") { return tkConst; }
|
|
||||||
if String_Eq(text, "type") { return tkType; }
|
|
||||||
if String_Eq(text, "struct") { return tkStruct; }
|
|
||||||
if String_Eq(text, "enum") { return tkEnum; }
|
|
||||||
if String_Eq(text, "union") { return tkUnion; }
|
|
||||||
if String_Eq(text, "interface") { return tkInterface; }
|
|
||||||
if String_Eq(text, "extend") { return tkExtend; }
|
|
||||||
if String_Eq(text, "module") { return tkModule; }
|
|
||||||
if String_Eq(text, "import") { return tkImport; }
|
|
||||||
if String_Eq(text, "pub") { return tkPub; }
|
|
||||||
if String_Eq(text, "extern") { return tkExtern; }
|
|
||||||
if String_Eq(text, "if") { return tkIf; }
|
|
||||||
if String_Eq(text, "else") { return tkElse; }
|
|
||||||
if String_Eq(text, "while") { return tkWhile; }
|
|
||||||
if String_Eq(text, "do") { return tkDo; }
|
|
||||||
if String_Eq(text, "loop") { return tkLoop; }
|
|
||||||
if String_Eq(text, "for") { return tkFor; }
|
|
||||||
if String_Eq(text, "in") { return tkIn; }
|
|
||||||
if String_Eq(text, "break") { return tkBreak; }
|
|
||||||
if String_Eq(text, "continue") { return tkContinue; }
|
|
||||||
if String_Eq(text, "return") { return tkReturn; }
|
|
||||||
if String_Eq(text, "match") { return tkMatch; }
|
|
||||||
if String_Eq(text, "as") { return tkAs; }
|
|
||||||
if String_Eq(text, "is") { return tkIs; }
|
|
||||||
if String_Eq(text, "null") { return tkNull; }
|
|
||||||
if String_Eq(text, "self") { return tkSelf; }
|
|
||||||
if String_Eq(text, "super") { return tkSuper; }
|
|
||||||
if String_Eq(text, "sizeof") { return tkSizeOf; }
|
|
||||||
if String_Eq(text, "discard") { return tkDiscard; }
|
|
||||||
if String_Eq(text, "async") { return tkAsync; }
|
|
||||||
if String_Eq(text, "await") { return tkAwait; }
|
|
||||||
if String_Eq(text, "spawn") { return tkSpawn; }
|
|
||||||
return tkIdent;
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexScanIdent(lex: *Lexer) {
|
|
||||||
lexMarkStart(lex);
|
|
||||||
while !lexIsAtEnd(lex) && Lex_IsIdentChar(lexPeek(lex, 0)) {
|
|
||||||
discard lexAdvance(lex);
|
|
||||||
}
|
|
||||||
let tok: LexToken = lexMakeToken(lex, tkIdent);
|
|
||||||
let kind: int = lexKeywordKind(tok.text);
|
|
||||||
tok.kind = kind;
|
|
||||||
if lex.tokenCount < maxTokens {
|
|
||||||
lex.tokens[lex.tokenCount] = tok;
|
|
||||||
lex.tokenCount = lex.tokenCount + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Numbers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func lexScanDigits(lex: *Lexer) {
|
|
||||||
while !lexIsAtEnd(lex) && Lex_IsDigit(lexPeek(lex, 0)) {
|
|
||||||
discard lexAdvance(lex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexScanHexDigits(lex: *Lexer) {
|
|
||||||
while !lexIsAtEnd(lex) && Lex_IsHexDigit(lexPeek(lex, 0)) {
|
|
||||||
discard lexAdvance(lex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexScanNumber(lex: *Lexer) {
|
|
||||||
lexMarkStart(lex);
|
|
||||||
var isFloat: bool = false;
|
|
||||||
|
|
||||||
if lexPeek(lex, 0) == 48 { // '0'
|
|
||||||
let p1: uint32 = lexPeek(lex, 1);
|
|
||||||
if p1 == 120 || p1 == 88 || p1 == 98 || p1 == 66 || p1 == 111 || p1 == 79 { // x X b B o O
|
|
||||||
discard lexAdvance(lex); // 0
|
|
||||||
discard lexAdvance(lex); // prefix
|
|
||||||
if p1 == 120 || p1 == 88 { lexScanHexDigits(lex); }
|
|
||||||
else if p1 == 98 || p1 == 66 {
|
|
||||||
while !lexIsAtEnd(lex) && Lex_IsBinDigit(lexPeek(lex, 0)) {
|
|
||||||
discard lexAdvance(lex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
while !lexIsAtEnd(lex) && Lex_IsOctDigit(lexPeek(lex, 0)) {
|
|
||||||
discard lexAdvance(lex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lexEmitToken(lex, tkIntLiteral);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lexScanDigits(lex);
|
|
||||||
|
|
||||||
if lexPeek(lex, 0) == 46 && Lex_IsDigit(lexPeek(lex, 1)) { // . digit
|
|
||||||
isFloat = true;
|
|
||||||
discard lexAdvance(lex); // .
|
|
||||||
lexScanDigits(lex);
|
|
||||||
}
|
|
||||||
|
|
||||||
let e: uint32 = lexPeek(lex, 0);
|
|
||||||
if e == 101 || e == 69 { // e E
|
|
||||||
isFloat = true;
|
|
||||||
discard lexAdvance(lex);
|
|
||||||
let sign: uint32 = lexPeek(lex, 0);
|
|
||||||
if sign == 43 || sign == 45 { discard lexAdvance(lex); } // + -
|
|
||||||
lexScanDigits(lex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if isFloat {
|
|
||||||
lexEmitToken(lex, tkFloatLiteral);
|
|
||||||
} else {
|
|
||||||
lexEmitToken(lex, tkIntLiteral);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Strings and chars
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func lexScanBacktickString(lex: *Lexer) {
|
|
||||||
lexMarkStart(lex);
|
|
||||||
if lexPeek(lex, 0) == 96 { discard lexAdvance(lex); } // opening backtick
|
|
||||||
while !lexIsAtEnd(lex) && lexPeek(lex, 0) != 96 {
|
|
||||||
discard lexAdvance(lex);
|
|
||||||
}
|
|
||||||
if lexIsAtEnd(lex) {
|
|
||||||
lexEmitDiag(lex, "unterminated backtick string literal");
|
|
||||||
} else {
|
|
||||||
discard lexAdvance(lex); // closing backtick
|
|
||||||
}
|
|
||||||
lexEmitToken(lex, tkStringLiteral);
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexScanString(lex: *Lexer) {
|
|
||||||
lexMarkStart(lex);
|
|
||||||
// Collect the prefix (before opening quote) for the token text
|
|
||||||
var prefix: String = "";
|
|
||||||
var prefixLen: int = 0;
|
|
||||||
if lex.pos > lex.startPos {
|
|
||||||
let plen: int = lex.pos - lex.startPos;
|
|
||||||
let pbuf: *char8 = bux_alloc((plen + 1) as uint) as *char8;
|
|
||||||
var pi: int = 0;
|
|
||||||
while pi < plen {
|
|
||||||
pbuf[pi] = lex.source[lex.startPos + pi] as char8;
|
|
||||||
pi = pi + 1;
|
|
||||||
}
|
|
||||||
pbuf[plen] = 0 as char8;
|
|
||||||
prefix = pbuf;
|
|
||||||
prefixLen = plen;
|
|
||||||
}
|
|
||||||
// Allocate buffer for resolved content (max = remaining source length)
|
|
||||||
let maxLen: int = 4096;
|
|
||||||
let resolved: *char8 = bux_alloc(maxLen as uint) as *char8;
|
|
||||||
var rpos: int = 0;
|
|
||||||
if lexPeek(lex, 0) == 34 { discard lexAdvance(lex); } // opening "
|
|
||||||
while !lexIsAtEnd(lex) && lexPeek(lex, 0) != 34 { // closing "
|
|
||||||
if lexPeek(lex, 0) == 10 { // \n
|
|
||||||
lexEmitDiag(lex, "unterminated string literal");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if lexPeek(lex, 0) == 92 { // backslash
|
|
||||||
discard lexAdvance(lex);
|
|
||||||
if !lexIsAtEnd(lex) {
|
|
||||||
let ec: uint32 = lexAdvance(lex);
|
|
||||||
var rc: char8 = ec as char8;
|
|
||||||
if ec == 110 { rc = 10 as char8; } // \n
|
|
||||||
else if ec == 114 { rc = 13 as char8; } // \r
|
|
||||||
else if ec == 116 { rc = 9 as char8; } // \t
|
|
||||||
else if ec == 48 { rc = 0 as char8; } // \0
|
|
||||||
else if ec == 92 { rc = 92 as char8; } // \\
|
|
||||||
else if ec == 34 { rc = 34 as char8; } // \"
|
|
||||||
else if ec == 39 { rc = 39 as char8; } // \'
|
|
||||||
resolved[rpos] = rc;
|
|
||||||
rpos = rpos + 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let c: uint32 = lexAdvance(lex);
|
|
||||||
resolved[rpos] = c as char8;
|
|
||||||
rpos = rpos + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resolved[rpos] = 0 as char8;
|
|
||||||
if lexIsAtEnd(lex) {
|
|
||||||
lexEmitDiag(lex, "unterminated string literal");
|
|
||||||
} else {
|
|
||||||
discard lexAdvance(lex); // closing "
|
|
||||||
}
|
|
||||||
lexEmitToken(lex, tkStringLiteral);
|
|
||||||
// Replace token text with resolved version: prefix + " + resolved + "
|
|
||||||
let totalLen: int = prefixLen + 1 + rpos + 1;
|
|
||||||
let finalBuf: *char8 = bux_alloc((totalLen + 1) as uint) as *char8;
|
|
||||||
var fi: int = 0;
|
|
||||||
var pi2: int = 0;
|
|
||||||
while pi2 < prefixLen { finalBuf[fi] = prefix[pi2] as char8; fi = fi + 1; pi2 = pi2 + 1; }
|
|
||||||
finalBuf[fi] = 34 as char8; fi = fi + 1; // opening "
|
|
||||||
var ri: int = 0;
|
|
||||||
while ri < rpos { finalBuf[fi] = resolved[ri]; fi = fi + 1; ri = ri + 1; }
|
|
||||||
finalBuf[fi] = 34 as char8; fi = fi + 1; // closing "
|
|
||||||
finalBuf[fi] = 0 as char8;
|
|
||||||
lexSetLastTokenText(lex, finalBuf);
|
|
||||||
}
|
|
||||||
|
|
||||||
func lexScanChar(lex: *Lexer) {
|
|
||||||
lexMarkStart(lex);
|
|
||||||
// Collect the prefix for the token text
|
|
||||||
var prefix: String = "";
|
|
||||||
var prefixLen: int = 0;
|
|
||||||
if lex.pos > lex.startPos {
|
|
||||||
let plen: int = lex.pos - lex.startPos;
|
|
||||||
let pbuf: *char8 = bux_alloc((plen + 1) as uint) as *char8;
|
|
||||||
var pi: int = 0;
|
|
||||||
while pi < plen {
|
|
||||||
pbuf[pi] = lex.source[lex.startPos + pi] as char8;
|
|
||||||
pi = pi + 1;
|
|
||||||
}
|
|
||||||
pbuf[plen] = 0 as char8;
|
|
||||||
prefix = pbuf;
|
|
||||||
prefixLen = plen;
|
|
||||||
}
|
|
||||||
var resolved: char8 = 0 as char8;
|
|
||||||
if lexPeek(lex, 0) == 39 { discard lexAdvance(lex); } // opening '
|
|
||||||
if lexIsAtEnd(lex) {
|
|
||||||
lexEmitDiag(lex, "unterminated char literal");
|
|
||||||
lexEmitToken(lex, tkCharLiteral);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if lexPeek(lex, 0) == 10 { // \n
|
|
||||||
lexEmitDiag(lex, "newline in char literal");
|
|
||||||
} else if lexPeek(lex, 0) == 92 { // backslash
|
|
||||||
discard lexAdvance(lex);
|
|
||||||
if !lexIsAtEnd(lex) {
|
|
||||||
let ec: uint32 = lexAdvance(lex);
|
|
||||||
if ec == 110 { resolved = 10 as char8; } // \n
|
|
||||||
else if ec == 114 { resolved = 13 as char8; } // \r
|
|
||||||
else if ec == 116 { resolved = 9 as char8; } // \t
|
|
||||||
else if ec == 48 { resolved = 0 as char8; } // \0
|
|
||||||
else if ec == 92 { resolved = 92 as char8; } // \\
|
|
||||||
else if ec == 34 { resolved = 34 as char8; } // \"
|
|
||||||
else if ec == 39 { resolved = 39 as char8; } // \'
|
|
||||||
else { resolved = ec as char8; }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resolved = lexAdvance(lex) as char8;
|
|
||||||
}
|
|
||||||
if lexIsAtEnd(lex) || lexPeek(lex, 0) != 39 {
|
|
||||||
lexEmitDiag(lex, "expected closing ' for char literal");
|
|
||||||
} else {
|
|
||||||
discard lexAdvance(lex); // closing '
|
|
||||||
}
|
|
||||||
lexEmitToken(lex, tkCharLiteral);
|
|
||||||
// Replace token text with resolved version
|
|
||||||
let totalLen: int = prefixLen + 1 + 1 + 1;
|
|
||||||
let finalBuf: *char8 = bux_alloc((totalLen + 1) as uint) as *char8;
|
|
||||||
var fi: int = 0;
|
|
||||||
var pi2: int = 0;
|
|
||||||
while pi2 < prefixLen { finalBuf[fi] = prefix[pi2] as char8; fi = fi + 1; pi2 = pi2 + 1; }
|
|
||||||
finalBuf[fi] = 39 as char8; fi = fi + 1; // opening '
|
|
||||||
finalBuf[fi] = resolved; fi = fi + 1; // resolved char
|
|
||||||
finalBuf[fi] = 39 as char8; fi = fi + 1; // closing '
|
|
||||||
finalBuf[fi] = 0 as char8;
|
|
||||||
lexSetLastTokenText(lex, finalBuf);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Symbols / operators
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func lexScanSymbol(lex: *Lexer) {
|
|
||||||
lexMarkStart(lex);
|
|
||||||
let c: uint32 = lexAdvance(lex);
|
|
||||||
|
|
||||||
// Single-char tokens
|
|
||||||
if c == 40 { lexEmitToken(lex, tkLParen); return; }
|
|
||||||
if c == 41 { lexEmitToken(lex, tkRParen); return; }
|
|
||||||
if c == 123 { lexEmitToken(lex, tkLBrace); return; }
|
|
||||||
if c == 125 { lexEmitToken(lex, tkRBrace); return; }
|
|
||||||
if c == 91 { lexEmitToken(lex, tkLBracket); return; }
|
|
||||||
if c == 93 { lexEmitToken(lex, tkRBracket); return; }
|
|
||||||
if c == 44 { lexEmitToken(lex, tkComma); return; }
|
|
||||||
if c == 59 { lexEmitToken(lex, tkSemicolon); return; }
|
|
||||||
if c == 64 { lexEmitToken(lex, tkAt); return; }
|
|
||||||
if c == 63 { lexEmitToken(lex, tkQuestion); return; }
|
|
||||||
if c == 126 { lexEmitToken(lex, tkTilde); return; }
|
|
||||||
|
|
||||||
// : ::
|
|
||||||
if c == 58 {
|
|
||||||
if lexMatch(lex, 58) { lexEmitToken(lex, tkColonColon); }
|
|
||||||
else { lexEmitToken(lex, tkColon); }
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// . .. ... ..=
|
|
||||||
if c == 46 {
|
|
||||||
if lexPeek(lex, 0) == 46 && lexPeek(lex, 1) == 46 {
|
|
||||||
discard lexAdvance(lex); discard lexAdvance(lex);
|
|
||||||
lexEmitToken(lex, tkDotDotDot); return;
|
|
||||||
}
|
|
||||||
if lexPeek(lex, 0) == 46 && lexPeek(lex, 1) == 61 {
|
|
||||||
discard lexAdvance(lex); discard lexAdvance(lex);
|
|
||||||
lexEmitToken(lex, tkDotDotEqual); return;
|
|
||||||
}
|
|
||||||
if lexMatch(lex, 46) { lexEmitToken(lex, tkDotDot); return; }
|
|
||||||
lexEmitToken(lex, tkDot); return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// - -> -- -=
|
|
||||||
if c == 45 {
|
|
||||||
if lexMatch(lex, 62) { lexEmitToken(lex, tkArrow); return; }
|
|
||||||
if lexMatch(lex, 45) { lexEmitToken(lex, tkMinusMinus); return; }
|
|
||||||
if lexMatch(lex, 61) { lexEmitToken(lex, tkMinusAssign); return; }
|
|
||||||
lexEmitToken(lex, tkMinus); return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// + ++ +=
|
|
||||||
if c == 43 {
|
|
||||||
if lexMatch(lex, 43) { lexEmitToken(lex, tkPlusPlus); return; }
|
|
||||||
if lexMatch(lex, 61) { lexEmitToken(lex, tkPlusAssign); return; }
|
|
||||||
lexEmitToken(lex, tkPlus); return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// * ** *=
|
|
||||||
if c == 42 {
|
|
||||||
if lexMatch(lex, 42) { lexEmitToken(lex, tkStarStar); return; }
|
|
||||||
if lexMatch(lex, 61) { lexEmitToken(lex, tkStarAssign); return; }
|
|
||||||
lexEmitToken(lex, tkStar); return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// / /=
|
|
||||||
if c == 47 {
|
|
||||||
if lexMatch(lex, 61) { lexEmitToken(lex, tkSlashAssign); return; }
|
|
||||||
lexEmitToken(lex, tkSlash); return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// % %=
|
|
||||||
if c == 37 {
|
|
||||||
if lexMatch(lex, 61) { lexEmitToken(lex, tkPercentAssign); return; }
|
|
||||||
lexEmitToken(lex, tkPercent); return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// = == =>
|
|
||||||
if c == 61 {
|
|
||||||
if lexMatch(lex, 61) { lexEmitToken(lex, tkEq); return; }
|
|
||||||
if lexMatch(lex, 62) { lexEmitToken(lex, tkFatArrow); return; }
|
|
||||||
lexEmitToken(lex, tkAssign); return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ! !=
|
|
||||||
if c == 33 {
|
|
||||||
if lexMatch(lex, 61) { lexEmitToken(lex, tkNe); return; }
|
|
||||||
lexEmitToken(lex, tkBang); return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// < <= << <<=
|
|
||||||
if c == 60 {
|
|
||||||
if lexMatch(lex, 61) { lexEmitToken(lex, tkLe); return; }
|
|
||||||
if lexMatch(lex, 60) {
|
|
||||||
if lexMatch(lex, 61) { lexEmitToken(lex, tkShlAssign); return; }
|
|
||||||
lexEmitToken(lex, tkShl); return;
|
|
||||||
}
|
|
||||||
lexEmitToken(lex, tkLt); return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// > >= >> >>=
|
|
||||||
if c == 62 {
|
|
||||||
if lexMatch(lex, 61) { lexEmitToken(lex, tkGe); return; }
|
|
||||||
if lexMatch(lex, 62) {
|
|
||||||
if lexMatch(lex, 61) { lexEmitToken(lex, tkShrAssign); return; }
|
|
||||||
lexEmitToken(lex, tkShr); return;
|
|
||||||
}
|
|
||||||
lexEmitToken(lex, tkGt); return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// & && &=
|
|
||||||
if c == 38 {
|
|
||||||
if lexMatch(lex, 38) { lexEmitToken(lex, tkAmpAmp); return; }
|
|
||||||
if lexMatch(lex, 61) { lexEmitToken(lex, tkAmpAssign); return; }
|
|
||||||
lexEmitToken(lex, tkAmp); return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// | || |=
|
|
||||||
if c == 124 {
|
|
||||||
if lexMatch(lex, 124) { lexEmitToken(lex, tkPipePipe); return; }
|
|
||||||
if lexMatch(lex, 61) { lexEmitToken(lex, tkPipeAssign); return; }
|
|
||||||
lexEmitToken(lex, tkPipe); return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ^ ^=
|
|
||||||
if c == 94 {
|
|
||||||
if lexMatch(lex, 61) { lexEmitToken(lex, tkCaretAssign); return; }
|
|
||||||
lexEmitToken(lex, tkCaret); return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// # intrinsics
|
|
||||||
if c == 35 {
|
|
||||||
if lexMatchStr(lex, "line") { lexEmitToken(lex, tkHashLine); return; }
|
|
||||||
if lexMatchStr(lex, "column") { lexEmitToken(lex, tkHashColumn); return; }
|
|
||||||
if lexMatchStr(lex, "file") { lexEmitToken(lex, tkHashFile); return; }
|
|
||||||
if lexMatchStr(lex, "function") { lexEmitToken(lex, tkHashFunction); return; }
|
|
||||||
if lexMatchStr(lex, "date") { lexEmitToken(lex, tkHashDate); return; }
|
|
||||||
if lexMatchStr(lex, "time") { lexEmitToken(lex, tkHashTime); return; }
|
|
||||||
if lexMatchStr(lex, "module") { lexEmitToken(lex, tkHashModule); return; }
|
|
||||||
lexEmitToken(lex, tkHash); return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lexEmitDiag(lex, "unexpected character");
|
|
||||||
lexEmitToken(lex, tkUnknown);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Next token
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func lexNextToken(lex: *Lexer) {
|
|
||||||
lexSkipWhitespace(lex);
|
|
||||||
|
|
||||||
if lexIsAtEnd(lex) {
|
|
||||||
lexMarkStart(lex);
|
|
||||||
lexEmitToken(lex, tkEndOfFile);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let c: uint32 = lexPeek(lex, 0);
|
|
||||||
|
|
||||||
if c == 10 { // \n
|
|
||||||
lexMarkStart(lex);
|
|
||||||
discard lexAdvance(lex);
|
|
||||||
lexEmitToken(lex, tkNewLine);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// String prefixes: c8" c16" c32"
|
|
||||||
if c == 99 { // 'c'
|
|
||||||
let d: uint32 = lexPeek(lex, 1);
|
|
||||||
if d == 56 && lexPeek(lex, 2) == 34 { // c8"
|
|
||||||
discard lexAdvance(lex); discard lexAdvance(lex); // c 8
|
|
||||||
lexScanString(lex); return;
|
|
||||||
}
|
|
||||||
if d == 49 && lexPeek(lex, 2) == 54 && lexPeek(lex, 3) == 34 { // c16"
|
|
||||||
discard lexAdvance(lex); discard lexAdvance(lex); discard lexAdvance(lex);
|
|
||||||
lexScanString(lex); return;
|
|
||||||
}
|
|
||||||
if d == 51 && lexPeek(lex, 2) == 50 && lexPeek(lex, 3) == 34 { // c32"
|
|
||||||
discard lexAdvance(lex); discard lexAdvance(lex); discard lexAdvance(lex);
|
|
||||||
lexScanString(lex); return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if c == 34 { // "
|
|
||||||
lexScanString(lex); return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backtick raw string
|
|
||||||
if c == 96 { // backtick
|
|
||||||
lexScanBacktickString(lex); return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Char prefixes: c8' c16' c32'
|
|
||||||
if c == 99 { // 'c'
|
|
||||||
let d: uint32 = lexPeek(lex, 1);
|
|
||||||
if d == 56 && lexPeek(lex, 2) == 39 { // c8'
|
|
||||||
discard lexAdvance(lex); discard lexAdvance(lex);
|
|
||||||
lexScanChar(lex); return;
|
|
||||||
}
|
|
||||||
if d == 49 && lexPeek(lex, 2) == 54 && lexPeek(lex, 3) == 39 { // c16'
|
|
||||||
discard lexAdvance(lex); discard lexAdvance(lex); discard lexAdvance(lex);
|
|
||||||
lexScanChar(lex); return;
|
|
||||||
}
|
|
||||||
if d == 51 && lexPeek(lex, 2) == 50 && lexPeek(lex, 3) == 39 { // c32'
|
|
||||||
discard lexAdvance(lex); discard lexAdvance(lex); discard lexAdvance(lex);
|
|
||||||
lexScanChar(lex); return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if c == 39 { // '
|
|
||||||
lexScanChar(lex); return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if Lex_IsIdentStart(c) {
|
|
||||||
lexScanIdent(lex); return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if Lex_IsDigit(c) {
|
|
||||||
lexScanNumber(lex); return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lexScanSymbol(lex);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Tokenize — main entry point
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Lexer_Tokenize(source: String) -> *Lexer {
|
|
||||||
let lex: *Lexer = bux_alloc(sizeof(Lexer)) as *Lexer;
|
|
||||||
lex.source = source;
|
|
||||||
lex.sourceLen = bux_strlen(source) as int;
|
|
||||||
lex.pos = 0;
|
|
||||||
lex.line = 1;
|
|
||||||
lex.column = 1;
|
|
||||||
lex.startLine = 0;
|
|
||||||
lex.startColumn = 0;
|
|
||||||
lex.startPos = 0;
|
|
||||||
let tokBuf: *LexToken = bux_alloc(maxTokens as uint * sizeof(LexToken)) as *LexToken;
|
|
||||||
let diagBuf: *LexerDiag = bux_alloc(maxDiags as uint * sizeof(LexerDiag)) as *LexerDiag;
|
|
||||||
lex.tokens = tokBuf;
|
|
||||||
lex.diags = diagBuf;
|
|
||||||
lex.tokenCount = 0;
|
|
||||||
lex.diagCount = 0;
|
|
||||||
|
|
||||||
while true {
|
|
||||||
lexNextToken(lex);
|
|
||||||
if lex.tokenCount >= maxTokens {
|
|
||||||
lexEmitDiag(lex, "too many tokens");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if lex.tokens[lex.tokenCount - 1].kind == tkEndOfFile {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lex;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Lexer_TokenCount(lex: *Lexer) -> int {
|
|
||||||
return lex.tokenCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Lexer_GetToken(lex: *Lexer, index: int) -> LexToken {
|
|
||||||
if index >= 0 && index < lex.tokenCount {
|
|
||||||
return lex.tokens[index];
|
|
||||||
}
|
|
||||||
var empty: LexToken = LexToken { kind: tkEndOfFile, text: "", line: 0, column: 0 };
|
|
||||||
return empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Lexer_DiagCount(lex: *Lexer) -> int {
|
|
||||||
return lex.diagCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Lexer_Free(lex: *Lexer) {
|
|
||||||
bux_free(lex.tokens as *void);
|
|
||||||
bux_free(lex.diags as *void);
|
|
||||||
bux_free(lex as *void);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
// manifest.bux — Manifest parser for bux.toml files
|
|
||||||
// Parses package metadata: name, version, type, build output.
|
|
||||||
module Manifest {
|
|
||||||
|
|
||||||
extern func bux_strlen(s: String) -> uint;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Manifest struct
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
struct Manifest {
|
|
||||||
name: String;
|
|
||||||
version: String;
|
|
||||||
pkgType: String;
|
|
||||||
output: String;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Simple TOML parser (handles [Package] and [Build] sections)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Manifest_Parse(content: String) -> Manifest {
|
|
||||||
var m: Manifest;
|
|
||||||
m.name = "";
|
|
||||||
m.version = "0.1.0";
|
|
||||||
m.pkgType = "bin";
|
|
||||||
m.output = "Bin";
|
|
||||||
|
|
||||||
if String_Eq(content, "") { return m; }
|
|
||||||
|
|
||||||
var currentSection: String = "";
|
|
||||||
let count: uint = String_SplitCount(content, "\n");
|
|
||||||
var i: uint = 0;
|
|
||||||
while i < count {
|
|
||||||
let line: String = String_SplitPart(content, "\n", i);
|
|
||||||
|
|
||||||
// Skip empty lines and comments
|
|
||||||
if String_Eq(line, "") { i = i + 1; continue; }
|
|
||||||
if String_StartsWith(line, "#") { i = i + 1; continue; }
|
|
||||||
|
|
||||||
// Section header: [Section]
|
|
||||||
if String_StartsWith(line, "[") {
|
|
||||||
if String_StartsWith(line, "[Package]") {
|
|
||||||
currentSection = "Package";
|
|
||||||
} else if String_StartsWith(line, "[Build]") {
|
|
||||||
currentSection = "Build";
|
|
||||||
} else {
|
|
||||||
currentSection = "";
|
|
||||||
}
|
|
||||||
i = i + 1; continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Key = Value
|
|
||||||
let eqCount: uint = String_SplitCount(line, "=");
|
|
||||||
if eqCount >= 2 {
|
|
||||||
let key: String = String_Trim(String_SplitPart(line, "=", 0));
|
|
||||||
let rawVal: String = String_Trim(String_SplitPart(line, "=", 1));
|
|
||||||
|
|
||||||
// Strip quotes from value
|
|
||||||
var val: String = rawVal;
|
|
||||||
if String_StartsWith(val, "\"") && String_EndsWith(val, "\"") {
|
|
||||||
let vlen: uint = bux_strlen(val);
|
|
||||||
if vlen >= 2 {
|
|
||||||
val = String_Slice(val, 1, vlen - 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if String_Eq(currentSection, "Package") {
|
|
||||||
if String_Eq(key, "Name") { m.name = val; }
|
|
||||||
if String_Eq(key, "Version") { m.version = val; }
|
|
||||||
if String_Eq(key, "Type") { m.pkgType = val; }
|
|
||||||
} else if String_Eq(currentSection, "Build") {
|
|
||||||
if String_Eq(key, "Output") { m.output = val; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Load manifest from file
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Manifest_Load(path: String) -> Manifest {
|
|
||||||
let content: String = ReadFile(path);
|
|
||||||
return Manifest_Parse(content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,95 +0,0 @@
|
|||||||
// scope.bux — Symbol table with parent-chain lookup
|
|
||||||
module Scope {
|
|
||||||
|
|
||||||
// Symbol kinds
|
|
||||||
const skVar: int = 0;
|
|
||||||
const skFunc: int = 1;
|
|
||||||
const skType: int = 2;
|
|
||||||
const skConst: int = 3;
|
|
||||||
const skModule: int = 4;
|
|
||||||
|
|
||||||
// Maximum symbols per scope
|
|
||||||
const maxSymbols: int = 8192;
|
|
||||||
|
|
||||||
struct Symbol {
|
|
||||||
kind: int;
|
|
||||||
name: String;
|
|
||||||
typeKind: int;
|
|
||||||
typeName: String;
|
|
||||||
isMutable: bool;
|
|
||||||
isPublic: bool;
|
|
||||||
decl: *Decl; // associated declaration (for funcs, structs, enums)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Scope {
|
|
||||||
symbols: *Symbol;
|
|
||||||
count: int;
|
|
||||||
parent: *Scope;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Scope operations
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Scope_New() -> Scope {
|
|
||||||
let sz: uint = maxSymbols as uint * sizeof(Symbol);
|
|
||||||
let data: *Symbol = bux_alloc(sz) as *Symbol;
|
|
||||||
return Scope { symbols: data, count: 0, parent: null as *Scope };
|
|
||||||
}
|
|
||||||
|
|
||||||
func Scope_NewChild(parent: *Scope) -> Scope {
|
|
||||||
let sz: uint = maxSymbols as uint * sizeof(Symbol);
|
|
||||||
let data: *Symbol = bux_alloc(sz) as *Symbol;
|
|
||||||
return Scope { symbols: data, count: 0, parent: parent };
|
|
||||||
}
|
|
||||||
|
|
||||||
func Scope_Define(scope: *Scope, sym: Symbol) -> bool {
|
|
||||||
// Check local scope for duplicates
|
|
||||||
var i: int = 0;
|
|
||||||
while i < scope.count {
|
|
||||||
if String_Eq(scope.symbols[i].name, sym.name) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
if scope.count < maxSymbols {
|
|
||||||
scope.symbols[scope.count] = sym;
|
|
||||||
scope.count = scope.count + 1;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Scope_Lookup(scope: *Scope, name: String) -> Symbol {
|
|
||||||
var cur: *Scope = scope;
|
|
||||||
while cur != null as *Scope {
|
|
||||||
var i: int = 0;
|
|
||||||
while i < cur.count {
|
|
||||||
if String_Eq(cur.symbols[i].name, name) {
|
|
||||||
return cur.symbols[i];
|
|
||||||
}
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
cur = cur.parent;
|
|
||||||
}
|
|
||||||
var empty: Symbol = Symbol { kind: 0, name: "", typeKind: 0, typeName: "", isMutable: false, isPublic: false, decl: null as *Decl };
|
|
||||||
return empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Scope_LookupLocal(scope: *Scope, name: String) -> Symbol {
|
|
||||||
var i: int = 0;
|
|
||||||
while i < scope.count {
|
|
||||||
if String_Eq(scope.symbols[i].name, name) {
|
|
||||||
return scope.symbols[i];
|
|
||||||
}
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
var empty: Symbol = Symbol { kind: 0, name: "", typeKind: 0, typeName: "", isMutable: false, isPublic: false, decl: null as *Decl };
|
|
||||||
return empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Scope_Free(scope: *Scope) {
|
|
||||||
bux_free(scope.symbols as *void);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,753 +0,0 @@
|
|||||||
// sema.bux — Semantic analysis (type checker, ported from sema.nim)
|
|
||||||
// Validates types, resolves identifiers, checks function calls.
|
|
||||||
module Sema {
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Sema context
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
struct Sema {
|
|
||||||
module: *Module;
|
|
||||||
scope: *Scope;
|
|
||||||
typeTable: *void;
|
|
||||||
methodTable: *void;
|
|
||||||
diagCount: int;
|
|
||||||
diags: *SemaDiag;
|
|
||||||
hasError: bool;
|
|
||||||
currentRetType: int; // return type of the function being checked
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SemaDiag {
|
|
||||||
line: uint32;
|
|
||||||
column: uint32;
|
|
||||||
message: String;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Diagnostics
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Sema_EmitError(sema: *Sema, line: uint32, col: uint32, msg: String) {
|
|
||||||
if sema.diagCount < 256 {
|
|
||||||
sema.diags[sema.diagCount] = SemaDiag { line: line, column: col, message: msg };
|
|
||||||
sema.diagCount = sema.diagCount + 1;
|
|
||||||
}
|
|
||||||
sema.hasError = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Symbol zero-init helper (bootstrap C backend does not zero-init structs)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Sema_ZeroInitSymbol(sym: *Symbol) {
|
|
||||||
sym.kind = 0;
|
|
||||||
sym.name = "";
|
|
||||||
sym.typeKind = 0;
|
|
||||||
sym.typeName = "";
|
|
||||||
sym.isMutable = false;
|
|
||||||
sym.isPublic = false;
|
|
||||||
sym.decl = null as *Decl;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Type resolution from TypeExpr → Type constants
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Sema_ResolveType(sema: *Sema, te: *TypeExpr) -> int {
|
|
||||||
if te == null as *TypeExpr { return tyUnknown; }
|
|
||||||
let name: String = te.typeName;
|
|
||||||
|
|
||||||
if te.kind == tekPointer {
|
|
||||||
return tyPointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
if String_Eq(name, "void") { return tyVoid; }
|
|
||||||
if String_Eq(name, "bool") { return tyBool; }
|
|
||||||
if String_Eq(name, "bool8") { return tyBool8; }
|
|
||||||
if String_Eq(name, "bool16") { return tyBool16; }
|
|
||||||
if String_Eq(name, "bool32") { return tyBool32; }
|
|
||||||
if String_Eq(name, "char8") { return tyChar8; }
|
|
||||||
if String_Eq(name, "char16") { return tyChar16; }
|
|
||||||
if String_Eq(name, "char32") { return tyChar32; }
|
|
||||||
if String_Eq(name, "String") { return tyStr; }
|
|
||||||
if String_Eq(name, "str") { return tyStr; }
|
|
||||||
if String_Eq(name, "int8") { return tyInt8; }
|
|
||||||
if String_Eq(name, "int16") { return tyInt16; }
|
|
||||||
if String_Eq(name, "int32") { return tyInt32; }
|
|
||||||
if String_Eq(name, "int64") { return tyInt64; }
|
|
||||||
if String_Eq(name, "int") { return tyInt; }
|
|
||||||
if String_Eq(name, "uint8") { return tyUInt8; }
|
|
||||||
if String_Eq(name, "uint16") { return tyUInt16; }
|
|
||||||
if String_Eq(name, "uint32") { return tyUInt32; }
|
|
||||||
if String_Eq(name, "uint64") { return tyUInt64; }
|
|
||||||
if String_Eq(name, "uint") { return tyUInt; }
|
|
||||||
if String_Eq(name, "float32") { return tyFloat32; }
|
|
||||||
if String_Eq(name, "float64") { return tyFloat64; }
|
|
||||||
if String_Eq(name, "float") { return tyFloat64; }
|
|
||||||
|
|
||||||
// Check type table for user-defined types (StringMap not yet supported)
|
|
||||||
// TODO: re-enable when StringMap generic is available
|
|
||||||
// if sema.typeTable != null as *void { ... }
|
|
||||||
return tyNamed; // assume named type
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Type predicates
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Sema_IsNumeric(kind: int) -> bool {
|
|
||||||
if kind == tyUnknown || kind == tyNamed || kind == tyTypeParam { return true; }
|
|
||||||
if kind == tyInt8 || kind == tyInt16 || kind == tyInt32 || kind == tyInt64 || kind == tyInt { return true; }
|
|
||||||
if kind == tyUInt8 || kind == tyUInt16 || kind == tyUInt32 || kind == tyUInt64 || kind == tyUInt { return true; }
|
|
||||||
if kind == tyFloat32 || kind == tyFloat64 { return true; }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Sema_IsBool(kind: int) -> bool {
|
|
||||||
return kind == tyBool || kind == tyBool8 || kind == tyBool16 || kind == tyBool32;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Sema_TypeName(kind: int) -> String {
|
|
||||||
if kind == tyUnknown { return "?"; }
|
|
||||||
if kind == tyVoid { return "void"; }
|
|
||||||
if kind == tyBool { return "bool"; }
|
|
||||||
if kind == tyInt{ return "int"; }
|
|
||||||
if kind == tyInt64 { return "int64"; }
|
|
||||||
if kind == tyUInt { return "uint"; }
|
|
||||||
if kind == tyFloat64 { return "float64"; }
|
|
||||||
if kind == tyStr { return "String"; }
|
|
||||||
if kind == tyPointer { return "*ptr"; }
|
|
||||||
if kind == tyNamed { return "user-type"; }
|
|
||||||
if kind == tyTypeParam { return "type-param"; }
|
|
||||||
return "?";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Block checking helper
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Sema_CheckBlock(sema: *Sema, block: *Block) {
|
|
||||||
if block == null as *Block { return; }
|
|
||||||
var blockScope: Scope = Scope_NewChild(sema.scope);
|
|
||||||
let prevScope: *Scope = sema.scope;
|
|
||||||
sema.scope = &blockScope;
|
|
||||||
var stmt: *Stmt = block.firstStmt;
|
|
||||||
while stmt != null as *Stmt {
|
|
||||||
Sema_CheckStmt(sema, stmt);
|
|
||||||
stmt = stmt.nextStmt;
|
|
||||||
}
|
|
||||||
sema.scope = prevScope;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Expression type checking
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
|
|
||||||
if expr == null as *Expr { return tyUnknown; }
|
|
||||||
let kind: int = expr.kind;
|
|
||||||
// Debug: print expr kind
|
|
||||||
// Print("Sema_CheckExpr kind=");
|
|
||||||
// PrintInt(kind as int64);
|
|
||||||
// PrintLine("");
|
|
||||||
|
|
||||||
// Literal
|
|
||||||
if kind == ekLiteral {
|
|
||||||
let tk: int = expr.tokKind;
|
|
||||||
if tk == tkIntLiteral { return tyInt; }
|
|
||||||
if tk == tkFloatLiteral { return tyFloat64; }
|
|
||||||
if tk == tkStringLiteral { return tyStr; }
|
|
||||||
if tk == tkBoolLiteral { return tyBool; }
|
|
||||||
if tk == tkCharLiteral { return tyChar32; }
|
|
||||||
if tk == tkNull { return tyPointer; }
|
|
||||||
return tyUnknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Identifier — look up in scope
|
|
||||||
if kind == ekIdent {
|
|
||||||
let sym: Symbol = Scope_Lookup(sema.scope, expr.strValue);
|
|
||||||
if sym.kind == 0 && !String_Eq(sym.name, expr.strValue) {
|
|
||||||
Sema_EmitError(sema, expr.line, expr.column, "undeclared identifier");
|
|
||||||
return tyUnknown;
|
|
||||||
}
|
|
||||||
if sym.typeName != null as String && !String_Eq(sym.typeName, "") {
|
|
||||||
let te: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
|
|
||||||
te.kind = tekNamed;
|
|
||||||
te.typeName = sym.typeName;
|
|
||||||
expr.refType = te;
|
|
||||||
}
|
|
||||||
return sym.typeKind;
|
|
||||||
}
|
|
||||||
|
|
||||||
// self
|
|
||||||
if kind == ekSelf {
|
|
||||||
let sym: Symbol = Scope_Lookup(sema.scope, "self");
|
|
||||||
if sym.kind == 0 && !String_Eq(sym.name, "self") {
|
|
||||||
Sema_EmitError(sema, expr.line, expr.column, "self outside method");
|
|
||||||
return tyUnknown;
|
|
||||||
}
|
|
||||||
return sym.typeKind;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Binary
|
|
||||||
if kind == ekBinary {
|
|
||||||
let left: int = Sema_CheckExpr(sema, expr.child1);
|
|
||||||
let right: int = Sema_CheckExpr(sema, expr.child2);
|
|
||||||
let op: int = expr.intValue;
|
|
||||||
|
|
||||||
// Assignment operators return target type
|
|
||||||
if op == tkAssign || (op >= tkPlusAssign && op <= tkShrAssign) {
|
|
||||||
return left;
|
|
||||||
}
|
|
||||||
// Comparison operators return bool
|
|
||||||
if op >= tkEq && op <= tkGe { return tyBool; }
|
|
||||||
// Logical operators return bool
|
|
||||||
if op == tkAmpAmp || op == tkPipePipe || op == tkBang { return tyBool; }
|
|
||||||
// Arithmetic returns wider type
|
|
||||||
if !Sema_IsNumeric(left) || !Sema_IsNumeric(right) {
|
|
||||||
Sema_EmitError(sema, expr.line, expr.column, "arithmetic requires numeric operands");
|
|
||||||
}
|
|
||||||
if left == tyFloat64 || right == tyFloat64 { return tyFloat64; }
|
|
||||||
return tyInt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unary
|
|
||||||
if kind == ekUnary {
|
|
||||||
let operand: int = Sema_CheckExpr(sema, expr.child1);
|
|
||||||
let op: int = expr.intValue;
|
|
||||||
if op == tkBang { return tyBool; }
|
|
||||||
if op == tkStar { return tyUnknown; } // dereference — resolve pointee
|
|
||||||
if op == tkAmp { return tyPointer; }
|
|
||||||
return operand;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign
|
|
||||||
if kind == ekAssign {
|
|
||||||
let target: int = Sema_CheckExpr(sema, expr.child1);
|
|
||||||
let value: int = Sema_CheckExpr(sema, expr.child2);
|
|
||||||
// Basic type compatibility (permissive for now)
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call
|
|
||||||
if kind == ekCall {
|
|
||||||
discard Sema_CheckExpr(sema, expr.child1);
|
|
||||||
var arg: *ExprList = expr.callArgs;
|
|
||||||
while arg != null as *ExprList {
|
|
||||||
discard Sema_CheckExpr(sema, arg.expr);
|
|
||||||
arg = arg.next;
|
|
||||||
}
|
|
||||||
// Try to resolve return type from function declaration
|
|
||||||
if expr.child1.kind == ekIdent {
|
|
||||||
let sym: Symbol = Scope_Lookup(sema.scope, expr.child1.strValue);
|
|
||||||
if sym.kind == skFunc && sym.decl != null as *Decl && sym.decl.retType != null as *TypeExpr {
|
|
||||||
return Sema_ResolveType(sema, sym.decl.retType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tyUnknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ternary
|
|
||||||
if kind == ekTernary {
|
|
||||||
return Sema_CheckExpr(sema, expr.child2); // then type
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cast — return target type
|
|
||||||
if kind == ekCast {
|
|
||||||
if expr.refType != null as *TypeExpr {
|
|
||||||
return Sema_ResolveType(sema, expr.refType);
|
|
||||||
}
|
|
||||||
return tyUnknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try (?)
|
|
||||||
if kind == ekTry {
|
|
||||||
let inner: int = Sema_CheckExpr(sema, expr.child1);
|
|
||||||
return inner; // simplified
|
|
||||||
}
|
|
||||||
|
|
||||||
// Struct init: TypeName { field: value, ... }
|
|
||||||
if kind == ekStructInit {
|
|
||||||
return tyNamed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Field access
|
|
||||||
if kind == ekField {
|
|
||||||
discard Sema_CheckExpr(sema, expr.child1);
|
|
||||||
return tyUnknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Index
|
|
||||||
if kind == ekIndex {
|
|
||||||
let obj: int = Sema_CheckExpr(sema, expr.child1);
|
|
||||||
let idx: int = Sema_CheckExpr(sema, expr.child2);
|
|
||||||
if !Sema_IsNumeric(idx) && idx != tyUnknown {
|
|
||||||
Sema_EmitError(sema, expr.line, expr.column, "index must be integer");
|
|
||||||
}
|
|
||||||
return tyUnknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block expression
|
|
||||||
if kind == ekBlock {
|
|
||||||
if expr.refBlock != null as *Block {
|
|
||||||
Sema_CheckBlock(sema, expr.refBlock);
|
|
||||||
}
|
|
||||||
return tyVoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
return tyUnknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Statement checking
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Sema_CheckStmt(sema: *Sema, stmt: *Stmt) {
|
|
||||||
if stmt == null as *Stmt { return; }
|
|
||||||
let kind: int = stmt.kind;
|
|
||||||
|
|
||||||
// Let/var
|
|
||||||
if kind == skLet {
|
|
||||||
let initType: int = Sema_CheckExpr(sema, stmt.child1);
|
|
||||||
// Register variable in scope
|
|
||||||
var sym: Symbol;
|
|
||||||
sym.kind = skVar;
|
|
||||||
sym.name = stmt.strValue;
|
|
||||||
sym.typeKind = initType;
|
|
||||||
sym.typeName = "";
|
|
||||||
if stmt.refStmtType != null as *TypeExpr {
|
|
||||||
sym.typeName = stmt.refStmtType.typeName;
|
|
||||||
}
|
|
||||||
sym.isMutable = stmt.boolValue;
|
|
||||||
sym.isPublic = false;
|
|
||||||
sym.decl = null as *Decl;
|
|
||||||
discard Scope_Define(sema.scope, sym);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return
|
|
||||||
if kind == skReturn {
|
|
||||||
if stmt.child1 != null as *Expr {
|
|
||||||
let retType: int = Sema_CheckExpr(sema, stmt.child1);
|
|
||||||
if sema.currentRetType != tyUnknown && retType != tyUnknown {
|
|
||||||
if retType != sema.currentRetType {
|
|
||||||
// Be permissive: allow numeric widening, pointer compatibility
|
|
||||||
if !Sema_IsNumeric(retType) || !Sema_IsNumeric(sema.currentRetType) {
|
|
||||||
// Allow pointer-type compatibility (String <-> *T, *T <-> *U)
|
|
||||||
let retIsPtr: bool = retType == tyPointer || retType == tyStr;
|
|
||||||
let expectedIsPtr: bool = sema.currentRetType == tyPointer || sema.currentRetType == tyStr;
|
|
||||||
if !retIsPtr || !expectedIsPtr {
|
|
||||||
Sema_EmitError(sema, stmt.line, stmt.column, "return type mismatch");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if sema.currentRetType != tyVoid && sema.currentRetType != tyUnknown {
|
|
||||||
Sema_EmitError(sema, stmt.line, stmt.column, "missing return value");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If
|
|
||||||
if kind == skIf {
|
|
||||||
let condType: int = Sema_CheckExpr(sema, stmt.child1);
|
|
||||||
if !Sema_IsBool(condType) && condType != tyUnknown {
|
|
||||||
Sema_EmitError(sema, stmt.line, stmt.column, "if condition must be bool");
|
|
||||||
}
|
|
||||||
Sema_CheckBlock(sema, stmt.refStmtBlock);
|
|
||||||
Sema_CheckBlock(sema, stmt.refStmtElse);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// While
|
|
||||||
if kind == skWhile {
|
|
||||||
let condType: int = Sema_CheckExpr(sema, stmt.child1);
|
|
||||||
if !Sema_IsBool(condType) && condType != tyUnknown {
|
|
||||||
Sema_EmitError(sema, stmt.line, stmt.column, "while condition must be bool");
|
|
||||||
}
|
|
||||||
Sema_CheckBlock(sema, stmt.refStmtBlock);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do-while
|
|
||||||
if kind == skDoWhile {
|
|
||||||
Sema_CheckBlock(sema, stmt.refStmtBlock);
|
|
||||||
let condType: int = Sema_CheckExpr(sema, stmt.child1);
|
|
||||||
if !Sema_IsBool(condType) && condType != tyUnknown {
|
|
||||||
Sema_EmitError(sema, stmt.line, stmt.column, "do-while condition must be bool");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop
|
|
||||||
if kind == skLoop {
|
|
||||||
Sema_CheckBlock(sema, stmt.refStmtBlock);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For
|
|
||||||
if kind == skFor {
|
|
||||||
discard Sema_CheckExpr(sema, stmt.child1);
|
|
||||||
var forScope: Scope = Scope_NewChild(sema.scope);
|
|
||||||
var loopSym: Symbol;
|
|
||||||
loopSym.kind = skVar;
|
|
||||||
loopSym.name = stmt.strValue;
|
|
||||||
loopSym.typeKind = tyUnknown;
|
|
||||||
loopSym.typeName = "";
|
|
||||||
loopSym.isMutable = true;
|
|
||||||
loopSym.isPublic = false;
|
|
||||||
loopSym.decl = null as *Decl;
|
|
||||||
discard Scope_Define(&forScope, loopSym);
|
|
||||||
let prevScope: *Scope = sema.scope;
|
|
||||||
sema.scope = &forScope;
|
|
||||||
Sema_CheckBlock(sema, stmt.refStmtBlock);
|
|
||||||
sema.scope = prevScope;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match
|
|
||||||
if kind == skMatch {
|
|
||||||
discard Sema_CheckExpr(sema, stmt.child1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Break / Continue
|
|
||||||
if kind == skBreak || kind == skContinue {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expression statement
|
|
||||||
if kind == skExpr && stmt.child1 != null as *Expr {
|
|
||||||
discard Sema_CheckExpr(sema, stmt.child1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decl (nested)
|
|
||||||
if kind == skDecl {
|
|
||||||
if stmt.refStmtDecl != null as *Decl && stmt.refStmtDecl.kind == dkFunc {
|
|
||||||
Sema_EmitError(sema, stmt.line, stmt.column, "nested functions not yet supported");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Collect globals (register functions, structs, enums in scope)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Sema_CollectGlobals(sema: *Sema) {
|
|
||||||
var decl: *Decl = sema.module.firstItem;
|
|
||||||
var funcCount: int = 0;
|
|
||||||
while decl != null as *Decl {
|
|
||||||
let dk: int = decl.kind;
|
|
||||||
|
|
||||||
// Function
|
|
||||||
if dk == dkFunc {
|
|
||||||
var sym: Symbol;
|
|
||||||
Sema_ZeroInitSymbol(&sym);
|
|
||||||
sym.kind = skFunc;
|
|
||||||
sym.name = decl.strValue;
|
|
||||||
sym.typeKind = tyFunc;
|
|
||||||
sym.isPublic = decl.isPublic;
|
|
||||||
sym.decl = decl;
|
|
||||||
discard Scope_Define(sema.scope, sym);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Struct
|
|
||||||
if dk == dkStruct {
|
|
||||||
var sym: Symbol;
|
|
||||||
Sema_ZeroInitSymbol(&sym);
|
|
||||||
sym.kind = skType;
|
|
||||||
sym.name = decl.strValue;
|
|
||||||
sym.typeKind = tyNamed;
|
|
||||||
sym.isPublic = decl.isPublic;
|
|
||||||
sym.decl = decl;
|
|
||||||
discard Scope_Define(sema.scope, sym);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enum
|
|
||||||
if dk == dkEnum {
|
|
||||||
var sym: Symbol;
|
|
||||||
Sema_ZeroInitSymbol(&sym);
|
|
||||||
sym.kind = skType;
|
|
||||||
sym.name = decl.strValue;
|
|
||||||
sym.typeKind = tyNamed;
|
|
||||||
sym.isPublic = decl.isPublic;
|
|
||||||
sym.decl = decl;
|
|
||||||
discard Scope_Define(sema.scope, sym);
|
|
||||||
|
|
||||||
// Register enum variants as constants
|
|
||||||
var vi: int = 0;
|
|
||||||
while vi < decl.variantCount && vi < 8 {
|
|
||||||
var v: EnumVariant;
|
|
||||||
if vi == 0 { v = decl.variant0; }
|
|
||||||
else if vi == 1 { v = decl.variant1; }
|
|
||||||
else if vi == 2 { v = decl.variant2; }
|
|
||||||
else if vi == 3 { v = decl.variant3; }
|
|
||||||
else if vi == 4 { v = decl.variant4; }
|
|
||||||
else if vi == 5 { v = decl.variant5; }
|
|
||||||
else if vi == 6 { v = decl.variant6; }
|
|
||||||
else if vi == 7 { v = decl.variant7; }
|
|
||||||
let variantName: String = String_Concat(decl.strValue, "_");
|
|
||||||
let variantName2: String = String_Concat(variantName, v.name);
|
|
||||||
var vSym: Symbol;
|
|
||||||
vSym.kind = skConst;
|
|
||||||
vSym.name = variantName2;
|
|
||||||
vSym.typeKind = tyNamed;
|
|
||||||
vSym.typeName = String_Concat(decl.strValue, "_Tag");
|
|
||||||
vSym.isMutable = false;
|
|
||||||
vSym.isPublic = decl.isPublic;
|
|
||||||
vSym.decl = decl;
|
|
||||||
discard Scope_Define(sema.scope, vSym);
|
|
||||||
vi = vi + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Const
|
|
||||||
if dk == dkConst {
|
|
||||||
var sym: Symbol;
|
|
||||||
Sema_ZeroInitSymbol(&sym);
|
|
||||||
sym.kind = skConst;
|
|
||||||
sym.name = decl.strValue;
|
|
||||||
if decl.constType != null as *TypeExpr {
|
|
||||||
sym.typeKind = Sema_ResolveType(sema, decl.constType);
|
|
||||||
sym.typeName = decl.constType.typeName;
|
|
||||||
} else {
|
|
||||||
sym.typeKind = tyInt;
|
|
||||||
}
|
|
||||||
sym.isMutable = false;
|
|
||||||
sym.isPublic = decl.isPublic;
|
|
||||||
sym.decl = decl;
|
|
||||||
discard Scope_Define(sema.scope, sym);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use (import)
|
|
||||||
if dk == dkUse {
|
|
||||||
if decl.useKind == 2 {
|
|
||||||
// Multi-import: names are comma-separated in useNames
|
|
||||||
// For now, register the whole useNames string as a single func name
|
|
||||||
// (permissive fallback — real fix needs string split)
|
|
||||||
let namesStr: String = decl.useNames;
|
|
||||||
if !String_Eq(namesStr, "") {
|
|
||||||
// Simple split by comma using available string functions
|
|
||||||
var start: uint = 0;
|
|
||||||
var pos: uint = 0;
|
|
||||||
let totalLen: uint = String_Len(namesStr);
|
|
||||||
while pos <= totalLen {
|
|
||||||
let atEnd: bool = pos == totalLen;
|
|
||||||
let isComma: bool = false;
|
|
||||||
if pos < totalLen {
|
|
||||||
// Check if character at pos is comma
|
|
||||||
let chStr: String = bux_str_slice(namesStr, pos, 1);
|
|
||||||
isComma = String_Eq(chStr, ",");
|
|
||||||
}
|
|
||||||
if atEnd || isComma {
|
|
||||||
let nameLen: uint = pos - start;
|
|
||||||
if nameLen > 0 {
|
|
||||||
let name: String = bux_str_slice(namesStr, start, nameLen);
|
|
||||||
var sym: Symbol;
|
|
||||||
sym.kind = skFunc;
|
|
||||||
sym.name = name;
|
|
||||||
sym.typeKind = tyUnknown;
|
|
||||||
sym.typeName = "";
|
|
||||||
sym.isMutable = false;
|
|
||||||
sym.isPublic = true;
|
|
||||||
sym.decl = null as *Decl;
|
|
||||||
discard Scope_Define(sema.scope, sym);
|
|
||||||
}
|
|
||||||
start = pos + 1;
|
|
||||||
}
|
|
||||||
pos = pos + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Single import or glob — add last path segment
|
|
||||||
let path: String = decl.usePath;
|
|
||||||
if !String_Eq(path, "") {
|
|
||||||
// Find last :: segment
|
|
||||||
var lastSeg: String = path;
|
|
||||||
let containsColons: int = bux_str_contains(path, "::");
|
|
||||||
if containsColons != 0 {
|
|
||||||
// Try to get last segment by finding last ::
|
|
||||||
// Use a simple heuristic: slice from various positions
|
|
||||||
let pathLen: uint = String_Len(path);
|
|
||||||
var tryPos: uint = 0;
|
|
||||||
while tryPos < pathLen {
|
|
||||||
let slice: String = bux_str_slice(path, tryPos, pathLen - tryPos);
|
|
||||||
if String_StartsWith(slice, "::") {
|
|
||||||
lastSeg = bux_str_slice(slice, 2, String_Len(slice) - 2);
|
|
||||||
}
|
|
||||||
tryPos = tryPos + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var sym: Symbol;
|
|
||||||
sym.kind = skFunc;
|
|
||||||
sym.name = lastSeg;
|
|
||||||
sym.typeKind = tyUnknown;
|
|
||||||
sym.typeName = "";
|
|
||||||
sym.isMutable = false;
|
|
||||||
sym.isPublic = true;
|
|
||||||
sym.decl = null as *Decl;
|
|
||||||
discard Scope_Define(sema.scope, sym);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Const
|
|
||||||
if dk == dkConst {
|
|
||||||
var sym: Symbol;
|
|
||||||
Sema_ZeroInitSymbol(&sym);
|
|
||||||
sym.kind = skConst;
|
|
||||||
sym.name = decl.strValue;
|
|
||||||
sym.typeKind = tyUnknown;
|
|
||||||
sym.isPublic = decl.isPublic;
|
|
||||||
sym.decl = decl;
|
|
||||||
discard Scope_Define(sema.scope, sym);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extern function
|
|
||||||
if dk == dkExternFunc {
|
|
||||||
var sym: Symbol;
|
|
||||||
Sema_ZeroInitSymbol(&sym);
|
|
||||||
sym.kind = skFunc;
|
|
||||||
sym.name = decl.strValue;
|
|
||||||
sym.typeKind = tyFunc;
|
|
||||||
sym.isPublic = true;
|
|
||||||
sym.decl = decl;
|
|
||||||
discard Scope_Define(sema.scope, sym);
|
|
||||||
}
|
|
||||||
|
|
||||||
decl = decl.childDecl2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Analyze — main entry point
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Sema_Analyze(mod: *Module) -> *Sema {
|
|
||||||
let s: *Sema = bux_alloc(sizeof(Sema)) as *Sema;
|
|
||||||
s.module = mod;
|
|
||||||
s.scope = bux_alloc(sizeof(Scope)) as *Scope;
|
|
||||||
s.scope.symbols = bux_alloc(1024 as uint * sizeof(Symbol)) as *Symbol;
|
|
||||||
s.scope.count = 0;
|
|
||||||
s.scope.parent = null as *Scope;
|
|
||||||
s.hasError = false;
|
|
||||||
s.diagCount = 0;
|
|
||||||
s.diags = bux_alloc(256 as uint * sizeof(SemaDiag)) as *SemaDiag;
|
|
||||||
s.typeTable = null as *void;
|
|
||||||
s.methodTable = null as *void;
|
|
||||||
s.currentRetType = tyVoid;
|
|
||||||
|
|
||||||
// First pass: collect globals
|
|
||||||
Sema_CollectGlobals(s);
|
|
||||||
|
|
||||||
// Second pass: check function bodies
|
|
||||||
var decl: *Decl = mod.firstItem;
|
|
||||||
while decl != null as *Decl {
|
|
||||||
if decl.kind == dkFunc && decl.refBody != null as *Block {
|
|
||||||
// Create function scope (child of global)
|
|
||||||
var funcScope: Scope = Scope_NewChild(s.scope);
|
|
||||||
|
|
||||||
// Add type params to scope
|
|
||||||
if decl.typeParamCount >= 1 {
|
|
||||||
var tpSym: Symbol;
|
|
||||||
Sema_ZeroInitSymbol(&tpSym);
|
|
||||||
tpSym.kind = skType;
|
|
||||||
tpSym.name = decl.typeParam0;
|
|
||||||
tpSym.typeKind = tyTypeParam;
|
|
||||||
tpSym.decl = null as *Decl;
|
|
||||||
discard Scope_Define(&funcScope, tpSym);
|
|
||||||
}
|
|
||||||
if decl.typeParamCount >= 2 {
|
|
||||||
var tpSym2: Symbol;
|
|
||||||
Sema_ZeroInitSymbol(&tpSym2);
|
|
||||||
tpSym2.kind = skType;
|
|
||||||
tpSym2.name = decl.typeParam1;
|
|
||||||
tpSym2.typeKind = tyTypeParam;
|
|
||||||
tpSym2.decl = null as *Decl;
|
|
||||||
discard Scope_Define(&funcScope, tpSym2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add parameters to scope
|
|
||||||
var i: int = 0;
|
|
||||||
while i < decl.paramCount {
|
|
||||||
var p: *Param = null as *Param;
|
|
||||||
if i == 0 { p = &decl.param0; }
|
|
||||||
else if i == 1 { p = &decl.param1; }
|
|
||||||
else if i == 2 { p = &decl.param2; }
|
|
||||||
else if i == 3 { p = &decl.param3; }
|
|
||||||
else if i == 4 { p = &decl.param4; }
|
|
||||||
else if i == 5 { p = &decl.param5; }
|
|
||||||
else if i == 6 { p = &decl.param6; }
|
|
||||||
else if i == 7 { p = &decl.param7; }
|
|
||||||
else if i == 8 { p = &decl.param8; }
|
|
||||||
var pSym: Symbol;
|
|
||||||
Sema_ZeroInitSymbol(&pSym);
|
|
||||||
pSym.kind = skVar;
|
|
||||||
if p != null as *Param && p.refParamType != null as *TypeExpr {
|
|
||||||
pSym.typeKind = Sema_ResolveType(s, p.refParamType);
|
|
||||||
pSym.typeName = p.refParamType.typeName;
|
|
||||||
} else {
|
|
||||||
pSym.typeKind = tyInt;
|
|
||||||
pSym.typeName = "";
|
|
||||||
}
|
|
||||||
pSym.isMutable = false;
|
|
||||||
pSym.isPublic = false;
|
|
||||||
pSym.decl = null as *Decl;
|
|
||||||
if i == 0 { pSym.name = decl.param0.name; }
|
|
||||||
else if i == 1 { pSym.name = decl.param1.name; }
|
|
||||||
else if i == 2 { pSym.name = decl.param2.name; }
|
|
||||||
else if i == 3 { pSym.name = decl.param3.name; }
|
|
||||||
else if i == 4 { pSym.name = decl.param4.name; }
|
|
||||||
else if i == 5 { pSym.name = decl.param5.name; }
|
|
||||||
else if i == 6 { pSym.name = decl.param6.name; }
|
|
||||||
else if i == 7 { pSym.name = decl.param7.name; }
|
|
||||||
else if i == 8 { pSym.name = decl.param8.name; }
|
|
||||||
discard Scope_Define(&funcScope, pSym);
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Switch to function scope and check body statements
|
|
||||||
let prevScope: *Scope = s.scope;
|
|
||||||
s.scope = &funcScope;
|
|
||||||
|
|
||||||
// Set current function return type
|
|
||||||
if decl.retType != null as *TypeExpr {
|
|
||||||
s.currentRetType = Sema_ResolveType(s, decl.retType);
|
|
||||||
} else {
|
|
||||||
s.currentRetType = tyVoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check body statements
|
|
||||||
var stmt: *Stmt = decl.refBody.firstStmt;
|
|
||||||
while stmt != null as *Stmt {
|
|
||||||
Sema_CheckStmt(s, stmt);
|
|
||||||
stmt = stmt.nextStmt;
|
|
||||||
}
|
|
||||||
|
|
||||||
s.scope = prevScope;
|
|
||||||
}
|
|
||||||
decl = decl.childDecl2;
|
|
||||||
}
|
|
||||||
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Sema_HasError(sema: *Sema) -> bool {
|
|
||||||
return sema.hasError;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Sema_DiagCount(sema: *Sema) -> int {
|
|
||||||
return sema.diagCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Sema_Free(sema: *Sema) {
|
|
||||||
bux_free(sema.scope.symbols as *void);
|
|
||||||
bux_free(sema.scope as *void);
|
|
||||||
bux_free(sema.diags as *void);
|
|
||||||
bux_free(sema as *void);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
// source_location.bux — Source position tracking
|
|
||||||
module SourceLocation {
|
|
||||||
|
|
||||||
struct SourceLocation {
|
|
||||||
line: uint32;
|
|
||||||
column: uint32;
|
|
||||||
offset: uint32;
|
|
||||||
}
|
|
||||||
|
|
||||||
func SourceLocation_New(line: uint32, column: uint32, offset: uint32) -> SourceLocation {
|
|
||||||
return SourceLocation { line: line, column: column, offset: offset };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,322 +0,0 @@
|
|||||||
// token.bux — Token kinds and helpers
|
|
||||||
module Token {
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// TokenKind enum
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Literals
|
|
||||||
const tkIntLiteral: int = 0;
|
|
||||||
const tkFloatLiteral: int = 1;
|
|
||||||
const tkStringLiteral: int = 2;
|
|
||||||
const tkCharLiteral: int = 3;
|
|
||||||
const tkBoolLiteral: int = 4;
|
|
||||||
|
|
||||||
// Identifiers
|
|
||||||
const tkIdent: int = 5;
|
|
||||||
const tkUnderscore: int = 6;
|
|
||||||
|
|
||||||
// Control flow keywords
|
|
||||||
const tkIf: int = 7;
|
|
||||||
const tkElse: int = 8;
|
|
||||||
const tkWhile: int = 9;
|
|
||||||
const tkDo: int = 10;
|
|
||||||
const tkLoop: int = 11;
|
|
||||||
const tkFor: int = 12;
|
|
||||||
const tkIn: int = 13;
|
|
||||||
const tkBreak: int = 14;
|
|
||||||
const tkContinue: int = 15;
|
|
||||||
const tkReturn: int = 16;
|
|
||||||
const tkMatch: int = 17;
|
|
||||||
|
|
||||||
// Declaration keywords
|
|
||||||
const tkFunc: int = 18;
|
|
||||||
const tkLet: int = 19;
|
|
||||||
const tkVar: int = 20;
|
|
||||||
const tkConst: int = 21;
|
|
||||||
const tkType: int = 22;
|
|
||||||
const tkStruct: int = 23;
|
|
||||||
const tkEnum: int = 24;
|
|
||||||
const tkUnion: int = 25;
|
|
||||||
const tkInterface: int = 26;
|
|
||||||
const tkExtend: int = 27;
|
|
||||||
const tkModule: int = 28;
|
|
||||||
const tkImport: int = 29;
|
|
||||||
const tkPub: int = 30;
|
|
||||||
const tkExtern: int = 31;
|
|
||||||
|
|
||||||
// Other keywords
|
|
||||||
const tkAs: int = 32;
|
|
||||||
const tkIs: int = 33;
|
|
||||||
const tkNull: int = 34;
|
|
||||||
const tkSelf: int = 35;
|
|
||||||
const tkSuper: int = 36;
|
|
||||||
const tkSizeOf: int = 37;
|
|
||||||
|
|
||||||
// Punctuation
|
|
||||||
const tkLParen: int = 38;
|
|
||||||
const tkRParen: int = 39;
|
|
||||||
const tkLBrace: int = 40;
|
|
||||||
const tkRBrace: int = 41;
|
|
||||||
const tkLBracket: int = 42;
|
|
||||||
const tkRBracket: int = 43;
|
|
||||||
const tkComma: int = 44;
|
|
||||||
const tkSemicolon: int = 45;
|
|
||||||
const tkColon: int = 46;
|
|
||||||
const tkColonColon: int = 47;
|
|
||||||
const tkDot: int = 48;
|
|
||||||
const tkDotDot: int = 49;
|
|
||||||
const tkDotDotDot: int = 50;
|
|
||||||
const tkDotDotEqual: int = 51;
|
|
||||||
const tkArrow: int = 52;
|
|
||||||
const tkFatArrow: int = 53;
|
|
||||||
const tkAt: int = 54;
|
|
||||||
const tkHash: int = 55;
|
|
||||||
const tkQuestion: int = 56;
|
|
||||||
|
|
||||||
// Arithmetic operators
|
|
||||||
const tkPlus: int = 57;
|
|
||||||
const tkMinus: int = 58;
|
|
||||||
const tkStar: int = 59;
|
|
||||||
const tkSlash: int = 60;
|
|
||||||
const tkPercent: int = 61;
|
|
||||||
const tkStarStar: int = 62;
|
|
||||||
const tkPlusPlus: int = 63;
|
|
||||||
const tkMinusMinus: int = 64;
|
|
||||||
|
|
||||||
// Bitwise operators
|
|
||||||
const tkAmp: int = 65;
|
|
||||||
const tkPipe: int = 66;
|
|
||||||
const tkCaret: int = 67;
|
|
||||||
const tkTilde: int = 68;
|
|
||||||
const tkShl: int = 69;
|
|
||||||
const tkShr: int = 70;
|
|
||||||
|
|
||||||
// Logical operators
|
|
||||||
const tkAmpAmp: int = 71;
|
|
||||||
const tkPipePipe: int = 72;
|
|
||||||
const tkBang: int = 73;
|
|
||||||
|
|
||||||
// Comparison operators
|
|
||||||
const tkEq: int = 74;
|
|
||||||
const tkNe: int = 75;
|
|
||||||
const tkLt: int = 76;
|
|
||||||
const tkLe: int = 77;
|
|
||||||
const tkGt: int = 78;
|
|
||||||
const tkGe: int = 79;
|
|
||||||
|
|
||||||
// Assignment operators
|
|
||||||
const tkAssign: int = 80;
|
|
||||||
const tkPlusAssign: int = 81;
|
|
||||||
const tkMinusAssign: int = 82;
|
|
||||||
const tkStarAssign: int = 83;
|
|
||||||
const tkSlashAssign: int = 84;
|
|
||||||
const tkPercentAssign: int = 85;
|
|
||||||
const tkAmpAssign: int = 86;
|
|
||||||
const tkPipeAssign: int = 87;
|
|
||||||
const tkCaretAssign: int = 88;
|
|
||||||
const tkShlAssign: int = 89;
|
|
||||||
const tkShrAssign: int = 90;
|
|
||||||
|
|
||||||
// Compile-time intrinsics
|
|
||||||
const tkHashLine: int = 91;
|
|
||||||
const tkHashColumn: int = 92;
|
|
||||||
const tkHashFile: int = 93;
|
|
||||||
const tkHashFunction: int = 94;
|
|
||||||
const tkHashDate: int = 95;
|
|
||||||
const tkHashTime: int = 96;
|
|
||||||
const tkHashModule: int = 97;
|
|
||||||
|
|
||||||
// Special
|
|
||||||
const tkOwn: int = 98;
|
|
||||||
const tkNewLine: int = 99;
|
|
||||||
const tkEndOfFile: int = 100;
|
|
||||||
const tkUnknown: int = 101;
|
|
||||||
|
|
||||||
// Async / concurrency
|
|
||||||
const tkAsync: int = 102;
|
|
||||||
const tkAwait: int = 103;
|
|
||||||
const tkSpawn: int = 104;
|
|
||||||
const tkDiscard: int = 105;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Token struct
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
struct Token {
|
|
||||||
kind: int;
|
|
||||||
text: String;
|
|
||||||
line: uint32;
|
|
||||||
column: uint32;
|
|
||||||
offset: uint32;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Helpers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Token_IsKeyword(kind: int) -> bool {
|
|
||||||
if kind >= tkIf && kind <= tkIn { return true; }
|
|
||||||
if kind >= tkBreak && kind <= tkMatch { return true; }
|
|
||||||
if kind >= tkFunc && kind <= tkExtern { return true; }
|
|
||||||
if kind >= tkAs && kind <= tkSuper { return true; }
|
|
||||||
if kind == tkSizeOf { return true; }
|
|
||||||
if kind >= tkAsync && kind <= tkSpawn { return true; }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Token_IsLiteral(kind: int) -> bool {
|
|
||||||
if kind >= tkIntLiteral && kind <= tkBoolLiteral { return true; }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Token_IsOperator(kind: int) -> bool {
|
|
||||||
if kind >= tkPlus && kind <= tkShrAssign { return true; }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Token_IsEof(kind: int) -> bool {
|
|
||||||
return kind == tkEndOfFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Token_KeywordKind(text: String) -> int {
|
|
||||||
if String_Eq(text, "func") { return tkFunc; }
|
|
||||||
if String_Eq(text, "let") { return tkLet; }
|
|
||||||
if String_Eq(text, "var") { return tkVar; }
|
|
||||||
if String_Eq(text, "const") { return tkConst; }
|
|
||||||
if String_Eq(text, "type") { return tkType; }
|
|
||||||
if String_Eq(text, "struct") { return tkStruct; }
|
|
||||||
if String_Eq(text, "enum") { return tkEnum; }
|
|
||||||
if String_Eq(text, "union") { return tkUnion; }
|
|
||||||
if String_Eq(text, "interface") { return tkInterface; }
|
|
||||||
if String_Eq(text, "extend") { return tkExtend; }
|
|
||||||
if String_Eq(text, "module") { return tkModule; }
|
|
||||||
if String_Eq(text, "import") { return tkImport; }
|
|
||||||
if String_Eq(text, "pub") { return tkPub; }
|
|
||||||
if String_Eq(text, "extern") { return tkExtern; }
|
|
||||||
if String_Eq(text, "if") { return tkIf; }
|
|
||||||
if String_Eq(text, "else") { return tkElse; }
|
|
||||||
if String_Eq(text, "while") { return tkWhile; }
|
|
||||||
if String_Eq(text, "do") { return tkDo; }
|
|
||||||
if String_Eq(text, "loop") { return tkLoop; }
|
|
||||||
if String_Eq(text, "for") { return tkFor; }
|
|
||||||
if String_Eq(text, "in") { return tkIn; }
|
|
||||||
if String_Eq(text, "break") { return tkBreak; }
|
|
||||||
if String_Eq(text, "continue") { return tkContinue; }
|
|
||||||
if String_Eq(text, "return") { return tkReturn; }
|
|
||||||
if String_Eq(text, "match") { return tkMatch; }
|
|
||||||
if String_Eq(text, "as") { return tkAs; }
|
|
||||||
if String_Eq(text, "is") { return tkIs; }
|
|
||||||
if String_Eq(text, "null") { return tkNull; }
|
|
||||||
if String_Eq(text, "self") { return tkSelf; }
|
|
||||||
if String_Eq(text, "super") { return tkSuper; }
|
|
||||||
if String_Eq(text, "sizeof") { return tkSizeOf; }
|
|
||||||
if String_Eq(text, "true") { return tkBoolLiteral; }
|
|
||||||
if String_Eq(text, "false") { return tkBoolLiteral; }
|
|
||||||
return tkIdent;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Token_KindName(kind: int) -> String {
|
|
||||||
if kind == tkIntLiteral { return "integer literal"; }
|
|
||||||
if kind == tkFloatLiteral { return "float literal"; }
|
|
||||||
if kind == tkStringLiteral { return "string literal"; }
|
|
||||||
if kind == tkCharLiteral { return "char literal"; }
|
|
||||||
if kind == tkBoolLiteral { return "boolean literal"; }
|
|
||||||
if kind == tkIdent { return "identifier"; }
|
|
||||||
if kind == tkUnderscore { return "_"; }
|
|
||||||
if kind == tkSizeOf { return "sizeof"; }
|
|
||||||
if kind == tkIf { return "if"; }
|
|
||||||
if kind == tkElse { return "else"; }
|
|
||||||
if kind == tkWhile { return "while"; }
|
|
||||||
if kind == tkDo { return "do"; }
|
|
||||||
if kind == tkLoop { return "loop"; }
|
|
||||||
if kind == tkFor { return "for"; }
|
|
||||||
if kind == tkIn { return "in"; }
|
|
||||||
if kind == tkBreak { return "break"; }
|
|
||||||
if kind == tkContinue { return "continue"; }
|
|
||||||
if kind == tkReturn { return "return"; }
|
|
||||||
if kind == tkMatch { return "match"; }
|
|
||||||
if kind == tkFunc { return "func"; }
|
|
||||||
if kind == tkLet { return "let"; }
|
|
||||||
if kind == tkVar { return "var"; }
|
|
||||||
if kind == tkConst { return "const"; }
|
|
||||||
if kind == tkType { return "type"; }
|
|
||||||
if kind == tkStruct { return "struct"; }
|
|
||||||
if kind == tkEnum { return "enum"; }
|
|
||||||
if kind == tkUnion { return "union"; }
|
|
||||||
if kind == tkInterface { return "interface"; }
|
|
||||||
if kind == tkExtend { return "extend"; }
|
|
||||||
if kind == tkModule { return "module"; }
|
|
||||||
if kind == tkImport { return "import"; }
|
|
||||||
if kind == tkPub { return "pub"; }
|
|
||||||
if kind == tkExtern { return "extern"; }
|
|
||||||
if kind == tkAs { return "as"; }
|
|
||||||
if kind == tkIs { return "is"; }
|
|
||||||
if kind == tkNull { return "null"; }
|
|
||||||
if kind == tkSelf { return "self"; }
|
|
||||||
if kind == tkSuper { return "super"; }
|
|
||||||
if kind == tkLParen { return "("; }
|
|
||||||
if kind == tkRParen { return ")"; }
|
|
||||||
if kind == tkLBrace { return "{"; }
|
|
||||||
if kind == tkRBrace { return "}"; }
|
|
||||||
if kind == tkLBracket { return "["; }
|
|
||||||
if kind == tkRBracket { return "]"; }
|
|
||||||
if kind == tkComma { return ","; }
|
|
||||||
if kind == tkSemicolon { return ";"; }
|
|
||||||
if kind == tkColon { return ":"; }
|
|
||||||
if kind == tkColonColon { return "::"; }
|
|
||||||
if kind == tkDot { return "."; }
|
|
||||||
if kind == tkDotDot { return ".."; }
|
|
||||||
if kind == tkDotDotDot { return "..."; }
|
|
||||||
if kind == tkDotDotEqual { return "..="; }
|
|
||||||
if kind == tkArrow { return "->"; }
|
|
||||||
if kind == tkFatArrow { return "=>"; }
|
|
||||||
if kind == tkAt { return "@"; }
|
|
||||||
if kind == tkHash { return "#"; }
|
|
||||||
if kind == tkQuestion { return "?"; }
|
|
||||||
if kind == tkPlus { return "+"; }
|
|
||||||
if kind == tkMinus { return "-"; }
|
|
||||||
if kind == tkStar { return "*"; }
|
|
||||||
if kind == tkSlash { return "/"; }
|
|
||||||
if kind == tkPercent { return "%"; }
|
|
||||||
if kind == tkStarStar { return "**"; }
|
|
||||||
if kind == tkPlusPlus { return "++"; }
|
|
||||||
if kind == tkMinusMinus { return "--"; }
|
|
||||||
if kind == tkAmp { return "&"; }
|
|
||||||
if kind == tkPipe { return "|"; }
|
|
||||||
if kind == tkCaret { return "^"; }
|
|
||||||
if kind == tkTilde { return "~"; }
|
|
||||||
if kind == tkShl { return "<<"; }
|
|
||||||
if kind == tkShr { return ">>"; }
|
|
||||||
if kind == tkAmpAmp { return "&&"; }
|
|
||||||
if kind == tkPipePipe { return "||"; }
|
|
||||||
if kind == tkBang { return "!"; }
|
|
||||||
if kind == tkEq { return "=="; }
|
|
||||||
if kind == tkNe { return "!="; }
|
|
||||||
if kind == tkLt { return "<"; }
|
|
||||||
if kind == tkLe { return "<="; }
|
|
||||||
if kind == tkGt { return ">"; }
|
|
||||||
if kind == tkGe { return ">="; }
|
|
||||||
if kind == tkAssign { return "="; }
|
|
||||||
if kind == tkPlusAssign { return "+="; }
|
|
||||||
if kind == tkMinusAssign { return "-="; }
|
|
||||||
if kind == tkStarAssign { return "*="; }
|
|
||||||
if kind == tkSlashAssign { return "/="; }
|
|
||||||
if kind == tkPercentAssign { return "%="; }
|
|
||||||
if kind == tkAmpAssign { return "&="; }
|
|
||||||
if kind == tkPipeAssign { return "|="; }
|
|
||||||
if kind == tkCaretAssign { return "^="; }
|
|
||||||
if kind == tkShlAssign { return "<<="; }
|
|
||||||
if kind == tkShrAssign { return ">>="; }
|
|
||||||
if kind == tkHashLine { return "#line"; }
|
|
||||||
if kind == tkHashColumn { return "#column"; }
|
|
||||||
if kind == tkHashFile { return "#file"; }
|
|
||||||
if kind == tkHashFunction { return "#function"; }
|
|
||||||
if kind == tkHashDate { return "#date"; }
|
|
||||||
if kind == tkHashTime { return "#time"; }
|
|
||||||
if kind == tkHashModule { return "#module"; }
|
|
||||||
if kind == tkNewLine { return "newline"; }
|
|
||||||
if kind == tkEndOfFile { return "end of file"; }
|
|
||||||
return "unknown token";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
// types.bux — Type system definitions and factories
|
|
||||||
module Types {
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// TypeKind constants
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const tyUnknown: int = 0;
|
|
||||||
const tyVoid: int = 1;
|
|
||||||
const tyBool: int = 2;
|
|
||||||
const tyBool8: int = 3;
|
|
||||||
const tyBool16: int = 4;
|
|
||||||
const tyBool32: int = 5;
|
|
||||||
const tyChar8: int = 6;
|
|
||||||
const tyChar16: int = 7;
|
|
||||||
const tyChar32: int = 8;
|
|
||||||
const tyStr: int = 9;
|
|
||||||
const tyInt8: int = 10;
|
|
||||||
const tyInt16: int = 11;
|
|
||||||
const tyInt32: int = 12;
|
|
||||||
const tyInt64: int = 13;
|
|
||||||
const tyInt: int = 14;
|
|
||||||
const tyUInt8: int = 15;
|
|
||||||
const tyUInt16: int = 16;
|
|
||||||
const tyUInt32: int = 17;
|
|
||||||
const tyUInt64: int = 18;
|
|
||||||
const tyUInt: int = 19;
|
|
||||||
const tyFloat32: int = 20;
|
|
||||||
const tyFloat64: int = 21;
|
|
||||||
const tyPointer: int = 22;
|
|
||||||
const tySlice: int = 23;
|
|
||||||
const tyRange: int = 24;
|
|
||||||
const tyTuple: int = 25;
|
|
||||||
const tyNamed: int = 26;
|
|
||||||
const tyTypeParam: int = 27;
|
|
||||||
const tyFunc: int = 28;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Type struct
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
struct Type {
|
|
||||||
kind: int;
|
|
||||||
name: String;
|
|
||||||
// inner types stored as array of pointers (simplified)
|
|
||||||
innerKind1: int;
|
|
||||||
innerName1: String;
|
|
||||||
innerKind2: int;
|
|
||||||
innerName2: String;
|
|
||||||
innerKind3: int;
|
|
||||||
innerName3: String;
|
|
||||||
innerCount: int;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Factories
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Type_MakeUnknown() -> Type {
|
|
||||||
return Type { kind: tyUnknown, name: "", innerCount: 0,
|
|
||||||
innerKind1: 0, innerName1: "", innerKind2: 0, innerName2: "",
|
|
||||||
innerKind3: 0, innerName3: "" };
|
|
||||||
}
|
|
||||||
|
|
||||||
func Type_MakeVoid() -> Type {
|
|
||||||
return Type { kind: tyVoid, name: "void", innerCount: 0,
|
|
||||||
innerKind1: 0, innerName1: "", innerKind2: 0, innerName2: "",
|
|
||||||
innerKind3: 0, innerName3: "" };
|
|
||||||
}
|
|
||||||
|
|
||||||
func Type_MakeBool() -> Type {
|
|
||||||
return Type { kind: tyBool, name: "bool", innerCount: 0,
|
|
||||||
innerKind1: 0, innerName1: "", innerKind2: 0, innerName2: "",
|
|
||||||
innerKind3: 0, innerName3: "" };
|
|
||||||
}
|
|
||||||
|
|
||||||
func Type_MakeInt() -> Type {
|
|
||||||
return Type { kind: tyInt, name: "int", innerCount: 0,
|
|
||||||
innerKind1: 0, innerName1: "", innerKind2: 0, innerName2: "",
|
|
||||||
innerKind3: 0, innerName3: "" };
|
|
||||||
}
|
|
||||||
|
|
||||||
func Type_MakeInt64() -> Type {
|
|
||||||
return Type { kind: tyInt64, name: "int64", innerCount: 0,
|
|
||||||
innerKind1: 0, innerName1: "", innerKind2: 0, innerName2: "",
|
|
||||||
innerKind3: 0, innerName3: "" };
|
|
||||||
}
|
|
||||||
|
|
||||||
func Type_MakeUInt() -> Type {
|
|
||||||
return Type { kind: tyUInt, name: "uint", innerCount: 0,
|
|
||||||
innerKind1: 0, innerName1: "", innerKind2: 0, innerName2: "",
|
|
||||||
innerKind3: 0, innerName3: "" };
|
|
||||||
}
|
|
||||||
|
|
||||||
func Type_MakeFloat64() -> Type {
|
|
||||||
return Type { kind: tyFloat64, name: "float64", innerCount: 0,
|
|
||||||
innerKind1: 0, innerName1: "", innerKind2: 0, innerName2: "",
|
|
||||||
innerKind3: 0, innerName3: "" };
|
|
||||||
}
|
|
||||||
|
|
||||||
func Type_MakeStr() -> Type {
|
|
||||||
return Type { kind: tyStr, name: "String", innerCount: 0,
|
|
||||||
innerKind1: 0, innerName1: "", innerKind2: 0, innerName2: "",
|
|
||||||
innerKind3: 0, innerName3: "" };
|
|
||||||
}
|
|
||||||
|
|
||||||
func Type_MakePointer(pointee: Type) -> Type {
|
|
||||||
return Type { kind: tyPointer, name: "", innerCount: 1,
|
|
||||||
innerKind1: pointee.kind, innerName1: pointee.name,
|
|
||||||
innerKind2: 0, innerName2: "", innerKind3: 0, innerName3: "" };
|
|
||||||
}
|
|
||||||
|
|
||||||
func Type_MakeNamed(name: String) -> Type {
|
|
||||||
return Type { kind: tyNamed, name: name, innerCount: 0,
|
|
||||||
innerKind1: 0, innerName1: "", innerKind2: 0, innerName2: "",
|
|
||||||
innerKind3: 0, innerName3: "" };
|
|
||||||
}
|
|
||||||
|
|
||||||
func Type_MakeTypeParam(name: String) -> Type {
|
|
||||||
return Type { kind: tyTypeParam, name: name, innerCount: 0,
|
|
||||||
innerKind1: 0, innerName1: "", innerKind2: 0, innerName2: "",
|
|
||||||
innerKind3: 0, innerName3: "" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Predicates
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Type_IsNumeric(t: Type) -> bool {
|
|
||||||
let k: int = t.kind;
|
|
||||||
if k == tyInt8 || k == tyInt16 || k == tyInt32 || k == tyInt64 || k == tyInt { return true; }
|
|
||||||
if k == tyUInt8 || k == tyUInt16 || k == tyUInt32 || k == tyUInt64 || k == tyUInt { return true; }
|
|
||||||
if k == tyFloat32 || k == tyFloat64 { return true; }
|
|
||||||
if k == tyUnknown || k == tyNamed || k == tyTypeParam { return true; }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Type_IsInteger(t: Type) -> bool {
|
|
||||||
let k: int = t.kind;
|
|
||||||
if k == tyInt8 || k == tyInt16 || k == tyInt32 || k == tyInt64 || k == tyInt { return true; }
|
|
||||||
if k == tyUInt8 || k == tyUInt16 || k == tyUInt32 || k == tyUInt64 || k == tyUInt { return true; }
|
|
||||||
if k == tyUnknown || k == tyNamed || k == tyTypeParam { return true; }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Type_IsBool(t: Type) -> bool {
|
|
||||||
let k: int = t.kind;
|
|
||||||
return k == tyBool || k == tyBool8 || k == tyBool16 || k == tyBool32;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Type_IsPointer(t: Type) -> bool {
|
|
||||||
return t.kind == tyPointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
func Type_IsSlice(t: Type) -> bool {
|
|
||||||
return t.kind == tySlice;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Comparison (structural, limited to kind + name for simplicity)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Type_Eq(a: Type, b: Type) -> bool {
|
|
||||||
if a.kind != b.kind { return false; }
|
|
||||||
if a.kind == tyNamed || a.kind == tyTypeParam {
|
|
||||||
return String_Eq(a.name, b.name);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// toString
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
func Type_ToString(t: Type) -> String {
|
|
||||||
if t.kind == tyVoid { return "void"; }
|
|
||||||
if t.kind == tyBool { return "bool"; }
|
|
||||||
if t.kind == tyStr { return "String"; }
|
|
||||||
if t.kind == tyInt { return "int"; }
|
|
||||||
if t.kind == tyInt64 { return "int64"; }
|
|
||||||
if t.kind == tyUInt { return "uint"; }
|
|
||||||
if t.kind == tyFloat64 { return "float64"; }
|
|
||||||
if t.kind == tyNamed { return t.name; }
|
|
||||||
if t.kind == tyTypeParam { return t.name; }
|
|
||||||
if t.kind == tyPointer { return String_Concat("*", t.innerName1); }
|
|
||||||
return "?";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
name = "test_array"
|
|
||||||
version = "0.1.0"
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
module Main {
|
|
||||||
import Std::Array::Array;
|
|
||||||
import Std::Iter::Iter;
|
|
||||||
import Std::Test::Test;
|
|
||||||
import Std::Io::Io;
|
|
||||||
|
|
||||||
func Main() -> int {
|
|
||||||
var arr: Array<int> = Array_New<int>(4);
|
|
||||||
Array_Push<int>(&arr, 10);
|
|
||||||
Array_Push<int>(&arr, 20);
|
|
||||||
Array_Push<int>(&arr, 30);
|
|
||||||
|
|
||||||
Test_AssertEqInt(Array_Len<int>(&arr) as int, 3);
|
|
||||||
Test_AssertEqInt(Array_Get<int>(&arr, 0), 10);
|
|
||||||
Test_AssertEqInt(Array_Get<int>(&arr, 1), 20);
|
|
||||||
Test_AssertEqInt(Array_Get<int>(&arr, 2), 30);
|
|
||||||
|
|
||||||
var it: Iter<int> = Array_Iter<int>(&arr);
|
|
||||||
Test_AssertTrue(Iter_HasNext<int>(&it));
|
|
||||||
Test_AssertEqInt(Iter_Next<int>(&it), 10);
|
|
||||||
Test_AssertEqInt(Iter_Next<int>(&it), 20);
|
|
||||||
Test_AssertEqInt(Iter_Next<int>(&it), 30);
|
|
||||||
Test_AssertFalse(Iter_HasNext<int>(&it));
|
|
||||||
|
|
||||||
var it2: Iter<int> = Array_Iter<int>(&arr);
|
|
||||||
let count: uint = Iter_Count<int>(&it2);
|
|
||||||
Test_AssertEqInt(count as int, 3);
|
|
||||||
|
|
||||||
Print("All array+iter tests passed!");
|
|
||||||
Array_Free<int>(&arr);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,54 @@
|
|||||||
|
/* Bux Standard Library - I/O functions */
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/* PrintLine - print string with newline */
|
||||||
|
void PrintLine(const char* s) {
|
||||||
|
if (s != NULL) {
|
||||||
|
puts(s);
|
||||||
|
} else {
|
||||||
|
puts("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print - print string without newline */
|
||||||
|
void Print(const char* s) {
|
||||||
|
if (s != NULL) {
|
||||||
|
printf("%s", s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PrintInt - print integer */
|
||||||
|
void PrintInt(int n) {
|
||||||
|
printf("%d", n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PrintInt64 - print 64-bit integer */
|
||||||
|
void PrintInt64(int64_t n) {
|
||||||
|
printf("%lld", (long long)n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PrintFloat - print float */
|
||||||
|
void PrintFloat(double f) {
|
||||||
|
printf("%g", f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PrintBool - print boolean */
|
||||||
|
void PrintBool(int b) {
|
||||||
|
printf("%s", b ? "true" : "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ReadLine - read line from stdin (simplified) */
|
||||||
|
const char* ReadLine(void) {
|
||||||
|
static char buffer[1024];
|
||||||
|
if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
|
||||||
|
size_t len = strlen(buffer);
|
||||||
|
if (len > 0 && buffer[len-1] == '\n') {
|
||||||
|
buffer[len-1] = '\0';
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
|||||||
|
[Package]
|
||||||
|
Name = "closure_test"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
module Main {
|
||||||
|
|
||||||
|
extern func PrintInt(x: int);
|
||||||
|
extern func PrintLine(msg: String);
|
||||||
|
|
||||||
|
func Apply(x: int, op: func(int) -> int) -> int {
|
||||||
|
return op(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Double(x: int) -> int {
|
||||||
|
return x * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() -> int {
|
||||||
|
// Closure as variable
|
||||||
|
let add: func(int, int) -> int = |a: int, b: int| -> int { return a + b; };
|
||||||
|
let sum: int = add(3, 4);
|
||||||
|
PrintInt(sum);
|
||||||
|
PrintLine("");
|
||||||
|
|
||||||
|
// Closure passed to function
|
||||||
|
let result: int = Apply(5, |x: int| -> int { return x * 3; });
|
||||||
|
PrintInt(result);
|
||||||
|
PrintLine("");
|
||||||
|
|
||||||
|
// Address of named function as function pointer
|
||||||
|
let d: func(int) -> int = &Double;
|
||||||
|
PrintInt(d(7));
|
||||||
|
PrintLine("");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
@[Drop]
|
||||||
|
struct Buffer {
|
||||||
|
ptr: *int
|
||||||
|
}
|
||||||
|
|
||||||
|
func Buffer_Drop(self: *Buffer) {
|
||||||
|
bux_free(self.ptr as *void);
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() -> int {
|
||||||
|
let buf: Buffer = Buffer { ptr: bux_alloc(10 as uint * sizeof(int)) as *int };
|
||||||
|
|
||||||
|
// Buffer should be auto-dropped here via Buffer_Drop(&buf)
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Executable
BIN
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
|||||||
|
[package]
|
||||||
|
name = "error_snippet_test"
|
||||||
|
version = "0.1.0"
|
||||||
|
pkgType = "bin"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
func Main() -> int {
|
||||||
|
let x: int = undefined_variable;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
name = "test_funcptr"
|
||||||
|
version = "0.1.0"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
module Main {
|
||||||
|
|
||||||
|
func Apply(x: int, op: func(int) -> int) -> int {
|
||||||
|
return op(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Double(n: int) -> int {
|
||||||
|
return n * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() -> int {
|
||||||
|
let result: int = Apply(5, &Double);
|
||||||
|
if result != 10 { return 1; }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[package]
|
||||||
|
name = "green_threads_test"
|
||||||
|
version = "0.1.0"
|
||||||
|
pkgType = "bin"
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import Std::Task;
|
||||||
|
import Std::String;
|
||||||
|
|
||||||
|
func Worker(id: int) {
|
||||||
|
PrintLine(String_Concat(String_Concat("Worker ", String_FromInt(id)), " starting"));
|
||||||
|
Task_Sleep(50);
|
||||||
|
PrintLine(String_Concat(String_Concat("Worker ", String_FromInt(id)), " done"));
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
PrintLine("=== Green Thread Test ===");
|
||||||
|
|
||||||
|
Task_Init(4);
|
||||||
|
|
||||||
|
let h1: TaskHandle = Task_Spawn(Worker as *void, 1 as *void);
|
||||||
|
let h2: TaskHandle = Task_Spawn(Worker as *void, 2 as *void);
|
||||||
|
let h3: TaskHandle = Task_Spawn(Worker as *void, 3 as *void);
|
||||||
|
|
||||||
|
PrintLine("Waiting for tasks...");
|
||||||
|
|
||||||
|
Task_Wait(h1);
|
||||||
|
Task_Wait(h2);
|
||||||
|
Task_Wait(h3);
|
||||||
|
|
||||||
|
Task_Shutdown();
|
||||||
|
|
||||||
|
PrintLine("=== All tasks complete ===");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import Std::Io;
|
||||||
|
import Std::Array;
|
||||||
|
import Std::Slice;
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
// Test Array operator_index_get/set
|
||||||
|
let arr: Array<int> = Array_New<int>(10);
|
||||||
|
Array_Push<int>(&arr, 10);
|
||||||
|
Array_Push<int>(&arr, 20);
|
||||||
|
Array_Push<int>(&arr, 30);
|
||||||
|
|
||||||
|
// Use [] syntax (operator_index_get)
|
||||||
|
if arr[0] != 10 { return 1; }
|
||||||
|
if arr[1] != 20 { return 2; }
|
||||||
|
if arr[2] != 30 { return 3; }
|
||||||
|
|
||||||
|
// Use [] syntax (operator_index_set)
|
||||||
|
arr[1] = 99;
|
||||||
|
if arr[1] != 99 { return 4; }
|
||||||
|
|
||||||
|
// Test Slice operator_index_get/set
|
||||||
|
let s: Slice<int> = Slice_FromArray<int>(&arr);
|
||||||
|
|
||||||
|
if s[0] != 10 { return 5; }
|
||||||
|
if s[1] != 99 { return 6; }
|
||||||
|
if s[2] != 30 { return 7; }
|
||||||
|
|
||||||
|
s[2] = 42;
|
||||||
|
if s[2] != 42 { return 8; }
|
||||||
|
|
||||||
|
PrintInt(s[0]);
|
||||||
|
PrintLine("");
|
||||||
|
PrintInt(Slice_Len<int>(&s));
|
||||||
|
PrintLine("");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
# Boko Framework
|
||||||
|
|
||||||
|
**Async web framework for Bux — inspired by FastAPI.**
|
||||||
|
|
||||||
|
Boko is a lightweight, multi-threaded web framework that brings FastAPI-style routing to the Bux programming language. It handles HTTP parsing, path pattern matching, query parameter extraction, and response building so you can focus on your application logic.
|
||||||
|
|
||||||
|
This version (0.2.0) is rewritten with modern Bux: `struct` methods, generic `StringMap<String>` for headers/query/path params, `for` loops, algebraic enums, `StringBuilder`, and string interpolation.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/boko-framework
|
||||||
|
../../buxc build
|
||||||
|
./build/boko-framework
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `http://localhost:8080` — you'll see the demo landing page.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
Boko follows a **single-dispatch-function** pattern. You define one function — `Boko_Router` — and the framework calls it for every incoming request:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
import Boko::{Request, Response, Response_Json, Response_Html, Response_NotFound,
|
||||||
|
Path_Match,
|
||||||
|
App, App_New, App_Run};
|
||||||
|
|
||||||
|
func Boko_Router(req: Request) -> Response {
|
||||||
|
// Route: GET /
|
||||||
|
if String_Eq(req.path, "/") {
|
||||||
|
return Response_Html("<h1>Hello Boko!</h1>");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route: GET /api/health
|
||||||
|
if String_Eq(req.path, "/api/health") {
|
||||||
|
return Response_Json("{\"status\":\"ok\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route: GET /users/{id} — path parameter
|
||||||
|
if Path_Match("/users/{id}", req.path, &req) {
|
||||||
|
let id: String = req.GetPathParam("id");
|
||||||
|
// Build JSON response with id
|
||||||
|
return Response_Json(...);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route: GET /search?q=... — query parameter
|
||||||
|
if String_Eq(req.path, "/search") {
|
||||||
|
let q: String = req.GetQuery("q");
|
||||||
|
return Response_Json(...);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response_NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
let app: App = App_New(8080, 4); // port, threads
|
||||||
|
App_Run(&app);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `method` | `HttpVerb` | GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS |
|
||||||
|
| `path` | `String` | Request path (without query string) |
|
||||||
|
| `body` | `String` | Request body (for POST/PUT) |
|
||||||
|
| `headers` | `StringMap<String>` | Parsed request headers |
|
||||||
|
| `query` | `StringMap<String>` | Parsed query parameters |
|
||||||
|
| `pathParams` | `StringMap<String>` | Extracted path parameters |
|
||||||
|
|
||||||
|
| Method | Returns | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `req.GetHeader(name)` | `String` | Get header value by name |
|
||||||
|
| `req.GetQuery(name)` | `String` | Get query parameter by name |
|
||||||
|
| `req.HasQuery(name)` | `bool` | Check if query parameter exists |
|
||||||
|
| `req.GetPathParam(name)` | `String` | Get extracted path parameter |
|
||||||
|
|
||||||
|
### Response
|
||||||
|
|
||||||
|
| Constructor | Content-Type | Status |
|
||||||
|
|-------------|-------------|--------|
|
||||||
|
| `Response_Html(html)` | `text/html` | 200 |
|
||||||
|
| `Response_Json(json)` | `application/json` | 200 |
|
||||||
|
| `Response_Text(text)` | `text/plain` | 200 |
|
||||||
|
| `Response_Redirect(url)` | — | 302 |
|
||||||
|
| `Response_NotFound()` | `application/json` | 404 |
|
||||||
|
| `Response_Error(code, msg)` | `application/json` | custom |
|
||||||
|
| `Response_NoContent()` | — | 204 |
|
||||||
|
|
||||||
|
### Path Matching
|
||||||
|
|
||||||
|
`Path_Match(pattern, actualPath, req)` matches a pattern with `{param}` placeholders:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
// Pattern: /users/{id}/posts/{postId}
|
||||||
|
// Actual: /users/42/posts/7
|
||||||
|
// Extracts: id=42, postId=7
|
||||||
|
|
||||||
|
if Path_Match("/users/{id}/posts/{postId}", req.path, &req) {
|
||||||
|
let userId: String = req.GetPathParam("id"); // "42"
|
||||||
|
let postId: String = req.GetPathParam("postId"); // "7"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The function returns `true` if the pattern matches and populates `req.pathParams`.
|
||||||
|
|
||||||
|
### App
|
||||||
|
|
||||||
|
```bux
|
||||||
|
let app: App = App_New(8080, 4); // port 8080, 4 worker threads
|
||||||
|
App_Run(&app); // blocks, handles requests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Incoming connection
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Net_Accept() ─── worker thread (1 of N)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Net_Recv() → raw HTTP bytes
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Request_Parse() → method, path, query, headers, body
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Boko_Router(req) ←── YOUR CODE
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Response_Build() → HTTP/1.1 response string
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Net_Send() → bytes to client
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Net_Close()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Endpoints in Demo App
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| GET | `/` | Landing page (HTML) |
|
||||||
|
| GET | `/api/health` | Health check (JSON) |
|
||||||
|
| GET | `/api/info` | Framework info (JSON) |
|
||||||
|
| GET | `/hello?name=X` | Query param demo |
|
||||||
|
| GET | `/users/{id}` | Path param demo |
|
||||||
|
| GET | `/posts/{id}/comments/{cid}` | Multi path param demo |
|
||||||
|
| GET | `/redirect` | 302 redirect to `/` |
|
||||||
|
| POST | `/api/echo` | Echo request body |
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/boko-framework/
|
||||||
|
├── bux.toml # Package manifest
|
||||||
|
├── README.md # This file
|
||||||
|
└── src/
|
||||||
|
├── Boko.bux # Framework core (~500 lines)
|
||||||
|
└── Main.bux # Example app (~150 lines)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design Philosophy
|
||||||
|
|
||||||
|
Boko is intentionally simple. Instead of complex DSLs or code generation, it gives you:
|
||||||
|
|
||||||
|
- **One function to write** — `Boko_Router(req) -> Response`
|
||||||
|
- **Direct control** — you write plain if/else or match for routing
|
||||||
|
- **No magic** — path params, query params, headers are explicit function calls
|
||||||
|
- **Fast** — multi-threaded accept loop, zero allocations where possible
|
||||||
|
|
||||||
|
This is the same philosophy as Go's `net/http` — simple, explicit, composable.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Part of the Bux project. See root [LICENSE](../../LICENSE).
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
EXIT:137
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[Package]
|
||||||
|
Name = "boko-framework"
|
||||||
|
Version = "0.2.0"
|
||||||
|
Type = "bin"
|
||||||
|
Description = "Boko — async web framework for Bux, inspired by FastAPI"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
EXIT:137
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
EXIT:143
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
EXIT:137
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
bash: ред 1: ../../buxc_lir: Няма такъв файл или директория
|
||||||
|
EXIT:127
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
EXIT:143
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
EXIT:143
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
bux 0.1.0 (bootstrap)
|
||||||
|
EXIT:0
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
nim c -o:buxc -d:release --opt:size bootstrap/main.nim
|
||||||
|
Hint: used config file '/etc/nim/nim.cfg' [Conf]
|
||||||
|
Hint: used config file '/etc/nim/config.nims' [Conf]
|
||||||
|
...........................................................................................................................................
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lexer.nim(340, 12) Hint: 'check3' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lexer.nim(350, 12) Hint: 'checkEq' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lexer.nim(64, 6) Hint: 'match' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lexer.nim(84, 6) Hint: 'emitWarning' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
..
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/parser.nim(663, 7) Hint: 'loc' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/parser.nim(1001, 9) Hint: 'isVar' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/parser.nim(59, 6) Hint: 'match' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
.......
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/sema.nim(1150, 9) Hint: 'operandType' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/sema.nim(1155, 9) Hint: 'operandType' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/sema.nim(1165, 9) Hint: 'subjectType' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/sema.nim(1200, 9) Hint: 'operand' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/sema.nim(94, 6) Hint: 'emitWarning' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
.
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/manifest.nim(93, 11) Hint: 'val' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
..
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/hir_lower.nim(1057, 9) Hint: 'loweredIter' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
.
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lir.nim(5, 8) Warning: imported and not used: 'types' [UnusedImport]
|
||||||
|
.
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lir_lower.nim(419, 9) Hint: 't' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lir_lower.nim(595, 9) Hint: 'bodyLbl' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lir_lower.nim(734, 11) Hint: 'exprVal' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lir_lower.nim(746, 9) Hint: 'exprVal' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lir_lower.nim(6, 8) Warning: imported and not used: 'ast' [UnusedImport]
|
||||||
|
.
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lir_c_backend.nim(20, 6) Hint: 'emit' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lir_c_backend.nim(46, 6) Hint: 'typeFromValue' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/lir_c_backend.nim(58, 6) Hint: 'setTempType' is declared but not used [XDeclaredButNotUsed]
|
||||||
|
/home/ziko/z-git/bux/bux/bootstrap/cli.nim(2, 55) Warning: imported and not used: 'lir' [UnusedImport]
|
||||||
|
Hint: [Link]
|
||||||
|
Hint: mm: orc; threads: on; opt: size; options: -d:release
|
||||||
|
76982 lines; 1.552s; 174.824MiB peakmem; proj: /home/ziko/z-git/bux/bux/bootstrap/main.nim; out: /home/ziko/z-git/bux/bux/buxc [SuccessX]
|
||||||
|
# strip buxc
|
||||||
|
EXIT:0
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd /home/ziko/z-git/bux/bux/apps/boko-framework
|
||||||
|
|
||||||
|
echo "=== CHECK ==="
|
||||||
|
../../buxc check 2>&1
|
||||||
|
echo "CHECK_EXIT=$?"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== BUILD ==="
|
||||||
|
../../buxc build 2>&1
|
||||||
|
echo "BUILD_EXIT=$?"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== FILES ==="
|
||||||
|
find . -type f -newer bux.toml 2>/dev/null | head -30
|
||||||
|
echo ""
|
||||||
|
echo "=== BINARY SEARCH ==="
|
||||||
|
file boko-framework 2>/dev/null || echo "no boko-framework"
|
||||||
|
ls -la build/ 2>/dev/null || echo "no build dir"
|
||||||
@@ -0,0 +1,467 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// Boko — Async Web Framework for Bux (inspired by FastAPI)
|
||||||
|
// Rewritten with modern Bux: methods on structs, generic StringMap, for-in
|
||||||
|
// loops, algebraic enums, StringBuilder, and string interpolation.
|
||||||
|
// =============================================================================
|
||||||
|
module Boko {
|
||||||
|
|
||||||
|
import Std::Io::{PrintLine, Print, PrintInt};
|
||||||
|
import Std::Net::{Net_Create, Net_SetReuse, Net_Bind, Net_Listen, Net_Accept, Net_Send, Net_Recv, Net_Close, Net_LastError};
|
||||||
|
import Std::String::{
|
||||||
|
String_Len, String_Eq, String_IsNull,
|
||||||
|
String_StartsWith, String_Contains,
|
||||||
|
String_Find, String_Offset, String_Slice,
|
||||||
|
String_SplitCount, String_SplitPart,
|
||||||
|
StringBuilder, StringBuilder_New, StringBuilder_Append,
|
||||||
|
StringBuilder_AppendInt, StringBuilder_Build, StringBuilder_Free
|
||||||
|
};
|
||||||
|
import Std::Map::{StringMap, StringMap_New, StringMap_Set, StringMap_Get, StringMap_Has};
|
||||||
|
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// HTTP Methods
|
||||||
|
// =============================================================================
|
||||||
|
enum HttpVerb {
|
||||||
|
GET,
|
||||||
|
POST,
|
||||||
|
PUT,
|
||||||
|
DELETE,
|
||||||
|
PATCH,
|
||||||
|
HEAD,
|
||||||
|
OPTIONS,
|
||||||
|
}
|
||||||
|
|
||||||
|
func HttpVerb_MethodName(verb: HttpVerb) -> String {
|
||||||
|
if verb.tag == HttpVerb_GET { return "GET"; }
|
||||||
|
if verb.tag == HttpVerb_POST { return "POST"; }
|
||||||
|
if verb.tag == HttpVerb_PUT { return "PUT"; }
|
||||||
|
if verb.tag == HttpVerb_DELETE { return "DELETE"; }
|
||||||
|
if verb.tag == HttpVerb_PATCH { return "PATCH"; }
|
||||||
|
if verb.tag == HttpVerb_HEAD { return "HEAD"; }
|
||||||
|
if verb.tag == HttpVerb_OPTIONS { return "OPTIONS"; }
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
func HttpVerb_Parse(methodStr: String) -> HttpVerb {
|
||||||
|
if String_Eq(methodStr, "GET") { return HttpVerb { tag: HttpVerb_GET }; }
|
||||||
|
if String_Eq(methodStr, "POST") { return HttpVerb { tag: HttpVerb_POST }; }
|
||||||
|
if String_Eq(methodStr, "PUT") { return HttpVerb { tag: HttpVerb_PUT }; }
|
||||||
|
if String_Eq(methodStr, "DELETE") { return HttpVerb { tag: HttpVerb_DELETE }; }
|
||||||
|
if String_Eq(methodStr, "PATCH") { return HttpVerb { tag: HttpVerb_PATCH }; }
|
||||||
|
if String_Eq(methodStr, "HEAD") { return HttpVerb { tag: HttpVerb_HEAD }; }
|
||||||
|
if String_Eq(methodStr, "OPTIONS") { return HttpVerb { tag: HttpVerb_OPTIONS }; }
|
||||||
|
return HttpVerb { tag: HttpVerb_GET };
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Request — parsed incoming HTTP request
|
||||||
|
// =============================================================================
|
||||||
|
struct Request {
|
||||||
|
method: HttpVerb,
|
||||||
|
path: String,
|
||||||
|
body: String,
|
||||||
|
headers: StringMap<String>,
|
||||||
|
query: StringMap<String>,
|
||||||
|
pathParams: StringMap<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
extend Request {
|
||||||
|
func GetHeader(self: Request, name: String) -> String {
|
||||||
|
if StringMap_Has<String>(&self.headers, name) {
|
||||||
|
return StringMap_Get<String>(&self.headers, name);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetQuery(self: Request, name: String) -> String {
|
||||||
|
if StringMap_Has<String>(&self.query, name) {
|
||||||
|
return StringMap_Get<String>(&self.query, name);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
func HasQuery(self: Request, name: String) -> bool {
|
||||||
|
return StringMap_Has<String>(&self.query, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPathParam(self: Request, name: String) -> String {
|
||||||
|
if StringMap_Has<String>(&self.pathParams, name) {
|
||||||
|
return StringMap_Get<String>(&self.pathParams, name);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Response — outgoing HTTP response
|
||||||
|
// =============================================================================
|
||||||
|
struct Response {
|
||||||
|
statusCode: int,
|
||||||
|
contentType: String,
|
||||||
|
body: String,
|
||||||
|
extraHeaders: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Constructors ---
|
||||||
|
func Response_New(status: int, contentType: String, body: String) -> Response {
|
||||||
|
return Response { statusCode: status, contentType: contentType, body: body, extraHeaders: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
func Response_Ok(body: String) -> Response {
|
||||||
|
return Response_New(200, "text/html; charset=utf-8", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Response_Html(html: String) -> Response {
|
||||||
|
return Response_New(200, "text/html; charset=utf-8", html);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Response_Json(json: String) -> Response {
|
||||||
|
return Response_New(200, "application/json; charset=utf-8", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Response_Text(text: String) -> Response {
|
||||||
|
return Response_New(200, "text/plain; charset=utf-8", text);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Response_Redirect(url: String) -> Response {
|
||||||
|
let sb: StringBuilder = StringBuilder_New();
|
||||||
|
StringBuilder_Append(&sb, "Location: ");
|
||||||
|
StringBuilder_Append(&sb, url);
|
||||||
|
StringBuilder_Append(&sb, "\r\n");
|
||||||
|
let headers: String = StringBuilder_Build(&sb);
|
||||||
|
StringBuilder_Free(&sb);
|
||||||
|
return Response { statusCode: 302, contentType: "", body: "", extraHeaders: headers };
|
||||||
|
}
|
||||||
|
|
||||||
|
func Response_NotFound() -> Response {
|
||||||
|
return Response_New(404, "application/json; charset=utf-8", "{\"error\":\"not_found\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
func Response_Error(status: int, message: String) -> Response {
|
||||||
|
let sb: StringBuilder = StringBuilder_New();
|
||||||
|
StringBuilder_Append(&sb, "{\"error\":\"");
|
||||||
|
StringBuilder_Append(&sb, message);
|
||||||
|
StringBuilder_Append(&sb, "\"}");
|
||||||
|
let body: String = StringBuilder_Build(&sb);
|
||||||
|
StringBuilder_Free(&sb);
|
||||||
|
return Response_New(status, "application/json; charset=utf-8", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Response_NoContent() -> Response {
|
||||||
|
return Response_New(204, "", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Instance helpers ---
|
||||||
|
extend Response {
|
||||||
|
func WithHeader(self: Response, key: String, value: String) -> Response {
|
||||||
|
let sb: StringBuilder = StringBuilder_New();
|
||||||
|
StringBuilder_Append(&sb, self.extraHeaders);
|
||||||
|
StringBuilder_Append(&sb, key);
|
||||||
|
StringBuilder_Append(&sb, ": ");
|
||||||
|
StringBuilder_Append(&sb, value);
|
||||||
|
StringBuilder_Append(&sb, "\r\n");
|
||||||
|
self.extraHeaders = StringBuilder_Build(&sb);
|
||||||
|
StringBuilder_Free(&sb);
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Status phrase lookup
|
||||||
|
// =============================================================================
|
||||||
|
func StatusPhrase(code: int) -> String {
|
||||||
|
if code == 200 { return "OK"; }
|
||||||
|
if code == 201 { return "Created"; }
|
||||||
|
if code == 204 { return "No Content"; }
|
||||||
|
if code == 301 { return "Moved Permanently"; }
|
||||||
|
if code == 302 { return "Found"; }
|
||||||
|
if code == 400 { return "Bad Request"; }
|
||||||
|
if code == 401 { return "Unauthorized"; }
|
||||||
|
if code == 403 { return "Forbidden"; }
|
||||||
|
if code == 404 { return "Not Found"; }
|
||||||
|
if code == 500 { return "Internal Server Error"; }
|
||||||
|
if code == 502 { return "Bad Gateway"; }
|
||||||
|
if code == 503 { return "Service Unavailable"; }
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Build HTTP response string from Response struct
|
||||||
|
// =============================================================================
|
||||||
|
func Response_Build(resp: Response) -> String {
|
||||||
|
let sb: StringBuilder = StringBuilder_New();
|
||||||
|
|
||||||
|
// Status line
|
||||||
|
StringBuilder_Append(&sb, "HTTP/1.1 ");
|
||||||
|
StringBuilder_AppendInt(&sb, resp.statusCode as int64);
|
||||||
|
StringBuilder_Append(&sb, " ");
|
||||||
|
StringBuilder_Append(&sb, StatusPhrase(resp.statusCode));
|
||||||
|
StringBuilder_Append(&sb, "\r\n");
|
||||||
|
|
||||||
|
// Server
|
||||||
|
StringBuilder_Append(&sb, "Server: Boko/0.2.0 (Bux)\r\n");
|
||||||
|
|
||||||
|
// Extra headers
|
||||||
|
if String_Len(resp.extraHeaders) > 0 {
|
||||||
|
StringBuilder_Append(&sb, resp.extraHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content-Type
|
||||||
|
if String_Len(resp.contentType) > 0 {
|
||||||
|
StringBuilder_Append(&sb, "Content-Type: ");
|
||||||
|
StringBuilder_Append(&sb, resp.contentType);
|
||||||
|
StringBuilder_Append(&sb, "\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content-Length
|
||||||
|
let bodyLen: uint = String_Len(resp.body);
|
||||||
|
StringBuilder_Append(&sb, "Content-Length: ");
|
||||||
|
StringBuilder_AppendInt(&sb, bodyLen as int64);
|
||||||
|
StringBuilder_Append(&sb, "\r\n");
|
||||||
|
|
||||||
|
// Connection close
|
||||||
|
StringBuilder_Append(&sb, "Connection: close\r\n");
|
||||||
|
StringBuilder_Append(&sb, "\r\n");
|
||||||
|
|
||||||
|
if bodyLen > 0 {
|
||||||
|
StringBuilder_Append(&sb, resp.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: String = StringBuilder_Build(&sb);
|
||||||
|
StringBuilder_Free(&sb);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Parse query string: ?key=val&key2=val2 → generic StringMap
|
||||||
|
// =============================================================================
|
||||||
|
func Query_Parse(queryString: String) -> StringMap<String> {
|
||||||
|
let map: StringMap<String> = StringMap_New<String>(8);
|
||||||
|
if String_Len(queryString) == 0 {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pairCount: uint = String_SplitCount(queryString, "&");
|
||||||
|
for i in 0..pairCount {
|
||||||
|
let pair: String = String_SplitPart(queryString, "&", i);
|
||||||
|
let eqPos: String = String_Find(pair, "=");
|
||||||
|
var key: String = pair;
|
||||||
|
var value: String = "";
|
||||||
|
if !String_IsNull(eqPos) {
|
||||||
|
let keyLen: uint = String_Offset(eqPos, pair);
|
||||||
|
key = String_Slice(pair, 0, keyLen);
|
||||||
|
let valStart: uint = keyLen + 1;
|
||||||
|
let pairLen: uint = String_Len(pair);
|
||||||
|
if valStart < pairLen {
|
||||||
|
value = String_Slice(pair, valStart, pairLen - valStart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StringMap_Set<String>(&map, key, value);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Parse incoming HTTP request from raw bytes
|
||||||
|
// =============================================================================
|
||||||
|
func Request_Parse(raw: String) -> Request {
|
||||||
|
var req: Request;
|
||||||
|
req.method = HttpVerb { tag: HttpVerb_GET };
|
||||||
|
req.path = "/";
|
||||||
|
req.body = "";
|
||||||
|
req.headers = StringMap_New<String>(8);
|
||||||
|
req.query = StringMap_New<String>(4);
|
||||||
|
req.pathParams = StringMap_New<String>(4);
|
||||||
|
|
||||||
|
if String_Len(raw) == 0 { return req; }
|
||||||
|
|
||||||
|
// Find header/body boundary
|
||||||
|
let boundary: String = String_Find(raw, "\r\n\r\n");
|
||||||
|
let rawLen: uint = String_Len(raw);
|
||||||
|
var headerLen: uint = rawLen;
|
||||||
|
if !String_IsNull(boundary) {
|
||||||
|
headerLen = String_Offset(boundary, raw);
|
||||||
|
let bodyStart: uint = headerLen + 4;
|
||||||
|
if bodyStart < rawLen {
|
||||||
|
req.body = String_Slice(raw, bodyStart, rawLen - bodyStart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if headerLen == 0 { return req; }
|
||||||
|
let headerBlock: String = String_Slice(raw, 0, headerLen);
|
||||||
|
let lineCount: uint = String_SplitCount(headerBlock, "\r\n");
|
||||||
|
if lineCount == 0 { return req; }
|
||||||
|
|
||||||
|
// Parse request line
|
||||||
|
let requestLine: String = String_SplitPart(headerBlock, "\r\n", 0);
|
||||||
|
if String_Len(requestLine) > 0 {
|
||||||
|
let methodStr: String = String_SplitPart(requestLine, " ", 0);
|
||||||
|
req.method = HttpVerb_Parse(methodStr);
|
||||||
|
|
||||||
|
let fullPath: String = String_SplitPart(requestLine, " ", 1);
|
||||||
|
let qmark: String = String_Find(fullPath, "?");
|
||||||
|
if !String_IsNull(qmark) {
|
||||||
|
let pathLen: uint = String_Offset(qmark, fullPath);
|
||||||
|
req.path = String_Slice(fullPath, 0, pathLen);
|
||||||
|
let qsStart: uint = pathLen + 1;
|
||||||
|
let fullLen: uint = String_Len(fullPath);
|
||||||
|
if qsStart < fullLen {
|
||||||
|
let qs: String = String_Slice(fullPath, qsStart, fullLen - qsStart);
|
||||||
|
req.query = Query_Parse(qs);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
req.path = fullPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse headers
|
||||||
|
for i in 1..lineCount {
|
||||||
|
let line: String = String_SplitPart(headerBlock, "\r\n", i);
|
||||||
|
let colonPos: String = String_Find(line, ": ");
|
||||||
|
if !String_IsNull(colonPos) {
|
||||||
|
let keyLen: uint = String_Offset(colonPos, line);
|
||||||
|
let key: String = String_Slice(line, 0, keyLen);
|
||||||
|
let valStart: uint = keyLen + 2;
|
||||||
|
let lineLen: uint = String_Len(line);
|
||||||
|
var value: String = "";
|
||||||
|
if valStart < lineLen {
|
||||||
|
value = String_Slice(line, valStart, lineLen - valStart);
|
||||||
|
}
|
||||||
|
StringMap_Set<String>(&req.headers, key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Path pattern matching: /users/{id} against /users/42 → extracts id=42
|
||||||
|
// =============================================================================
|
||||||
|
func Path_Match(pattern: String, path: String, req: *Request) -> bool {
|
||||||
|
let patParts: uint = String_SplitCount(pattern, "/");
|
||||||
|
let pathParts: uint = String_SplitCount(path, "/");
|
||||||
|
|
||||||
|
if patParts != pathParts { return false; }
|
||||||
|
|
||||||
|
req.pathParams = StringMap_New<String>(8);
|
||||||
|
|
||||||
|
for i in 0..patParts {
|
||||||
|
let patPart: String = String_SplitPart(pattern, "/", i);
|
||||||
|
let pathPart: String = String_SplitPart(path, "/", i);
|
||||||
|
|
||||||
|
if String_StartsWith(patPart, "{") && String_Contains(patPart, "}") {
|
||||||
|
let nameLen: uint = String_Len(patPart) - 2;
|
||||||
|
let name: String = String_Slice(patPart, 1, nameLen);
|
||||||
|
StringMap_Set<String>(&req.pathParams, name, pathPart);
|
||||||
|
} else {
|
||||||
|
if !String_Eq(patPart, pathPart) { return false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// App — server instance
|
||||||
|
// =============================================================================
|
||||||
|
struct App {
|
||||||
|
port: int,
|
||||||
|
threadCount: int,
|
||||||
|
serverName: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
func App_New(port: int, threadCount: int) -> App {
|
||||||
|
return App { port: port, threadCount: threadCount, serverName: "Boko/0.2.0 (Bux)" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Forward declaration — user must implement this in their module
|
||||||
|
// =============================================================================
|
||||||
|
func Boko_Router(req: Request) -> Response;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Handle a single connection: parse → dispatch → respond
|
||||||
|
// =============================================================================
|
||||||
|
func App_HandleConnection(clientFd: int) {
|
||||||
|
let raw: String = Net_Recv(clientFd, 8192);
|
||||||
|
if String_Len(raw) == 0 { return; }
|
||||||
|
|
||||||
|
let req: Request = Request_Parse(raw);
|
||||||
|
let resp: Response = Boko_Router(req);
|
||||||
|
|
||||||
|
// Log
|
||||||
|
Print(HttpVerb_MethodName(req.method));
|
||||||
|
Print(" ");
|
||||||
|
Print(req.path);
|
||||||
|
Print(" → ");
|
||||||
|
PrintInt(resp.statusCode);
|
||||||
|
PrintLine("");
|
||||||
|
|
||||||
|
// Send
|
||||||
|
let respStr: String = Response_Build(resp);
|
||||||
|
Net_Send(clientFd, respStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Worker thread: accept loop
|
||||||
|
// =============================================================================
|
||||||
|
func App_Worker(serverFd: int) {
|
||||||
|
while true {
|
||||||
|
let clientFd: int = Net_Accept(serverFd);
|
||||||
|
if clientFd < 0 { continue; }
|
||||||
|
App_HandleConnection(clientFd);
|
||||||
|
Net_Close(clientFd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// App_Run: start the server (blocking)
|
||||||
|
// =============================================================================
|
||||||
|
func App_Run(app: *App) {
|
||||||
|
PrintLine("╔══════════════════════════════════════╗");
|
||||||
|
PrintLine("║ Boko Framework v0.2.0 ║");
|
||||||
|
PrintLine("║ Async web framework for Bux ║");
|
||||||
|
PrintLine("╚══════════════════════════════════════════════════════╝");
|
||||||
|
PrintLine("");
|
||||||
|
|
||||||
|
let fd: int = Net_Create();
|
||||||
|
if fd < 0 {
|
||||||
|
PrintLine("FATAL: socket() failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Net_SetReuse(fd);
|
||||||
|
|
||||||
|
if !Net_Bind(fd, "0.0.0.0", app.port) {
|
||||||
|
Print("FATAL: bind(:");
|
||||||
|
PrintInt(app.port);
|
||||||
|
Print(") failed: ");
|
||||||
|
PrintLine(Net_LastError());
|
||||||
|
Net_Close(fd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Net_Listen(fd, 128) {
|
||||||
|
PrintLine("FATAL: listen() failed");
|
||||||
|
Net_Close(fd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Print("✓ Boko running on http://0.0.0.0:");
|
||||||
|
PrintInt(app.port);
|
||||||
|
PrintLine("");
|
||||||
|
Print("✓ Workers: ");
|
||||||
|
PrintInt(app.threadCount);
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("");
|
||||||
|
|
||||||
|
// Spawn workers
|
||||||
|
for i in 0..(app.threadCount - 1) {
|
||||||
|
spawn App_Worker(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main thread is the last worker
|
||||||
|
App_Worker(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // module Boko
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// Boko Example App — demonstrates the modern framework API
|
||||||
|
// =============================================================================
|
||||||
|
module Main {
|
||||||
|
|
||||||
|
import Std::Io::{PrintLine, Print};
|
||||||
|
import Std::String::{
|
||||||
|
String_Eq, String_Len,
|
||||||
|
StringBuilder, StringBuilder_New, StringBuilder_Append,
|
||||||
|
StringBuilder_Build, StringBuilder_Free
|
||||||
|
};
|
||||||
|
import Boko::{
|
||||||
|
App, App_New, App_Run,
|
||||||
|
Request, Response,
|
||||||
|
Response_Html, Response_Json, Response_NotFound, Response_Redirect,
|
||||||
|
Path_Match,
|
||||||
|
HttpVerb
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Boko_Router — user-defined dispatch (called by the framework)
|
||||||
|
// =============================================================================
|
||||||
|
func Boko_Router(req: Request) -> Response {
|
||||||
|
// --- GET / ---
|
||||||
|
if String_Eq(req.path, "/") && req.method.tag == HttpVerb_GET {
|
||||||
|
return Response_Html(PageHome());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GET /api/health ---
|
||||||
|
if String_Eq(req.path, "/api/health") {
|
||||||
|
return Response_Json("{\"status\":\"ok\",\"framework\":\"Boko\",\"version\":\"0.2.0\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GET /api/info ---
|
||||||
|
if String_Eq(req.path, "/api/info") {
|
||||||
|
return Response_Json("{\"name\":\"Boko\",\"language\":\"Bux\",\"inspiration\":\"FastAPI\",\"features\":[\"routing\",\"path-params\",\"query-params\",\"json\"]}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GET /hello?name=World ---
|
||||||
|
if String_Eq(req.path, "/hello") {
|
||||||
|
var name: String = req.GetQuery("name");
|
||||||
|
if String_Len(name) == 0 { name = "World"; }
|
||||||
|
let html: String = f"<h1>Hello, {name}!</h1>";
|
||||||
|
return Response_Html(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GET /users/{id} ---
|
||||||
|
if Path_Match("/users/{id}", req.path, &req) {
|
||||||
|
let id: String = req.GetPathParam("id");
|
||||||
|
let sb: StringBuilder = StringBuilder_New();
|
||||||
|
StringBuilder_Append(&sb, "{\"id\":");
|
||||||
|
StringBuilder_Append(&sb, id);
|
||||||
|
StringBuilder_Append(&sb, ",\"name\":\"User ");
|
||||||
|
StringBuilder_Append(&sb, id);
|
||||||
|
StringBuilder_Append(&sb, "\"}");
|
||||||
|
let json: String = StringBuilder_Build(&sb);
|
||||||
|
StringBuilder_Free(&sb);
|
||||||
|
return Response_Json(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GET /posts/{postId}/comments/{commentId} ---
|
||||||
|
if Path_Match("/posts/{postId}/comments/{commentId}", req.path, &req) {
|
||||||
|
let pid: String = req.GetPathParam("postId");
|
||||||
|
let cid: String = req.GetPathParam("commentId");
|
||||||
|
let sb: StringBuilder = StringBuilder_New();
|
||||||
|
StringBuilder_Append(&sb, "{\"postId\":");
|
||||||
|
StringBuilder_Append(&sb, pid);
|
||||||
|
StringBuilder_Append(&sb, ",\"commentId\":");
|
||||||
|
StringBuilder_Append(&sb, cid);
|
||||||
|
StringBuilder_Append(&sb, ",\"text\":\"Great post!\"}");
|
||||||
|
let json: String = StringBuilder_Build(&sb);
|
||||||
|
StringBuilder_Free(&sb);
|
||||||
|
return Response_Json(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GET /redirect → 302 to / ---
|
||||||
|
if String_Eq(req.path, "/redirect") {
|
||||||
|
return Response_Redirect("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- POST /api/echo ---
|
||||||
|
if String_Eq(req.path, "/api/echo") && req.method.tag == HttpVerb_POST {
|
||||||
|
let sb: StringBuilder = StringBuilder_New();
|
||||||
|
StringBuilder_Append(&sb, "{\"echo\":");
|
||||||
|
StringBuilder_Append(&sb, req.body);
|
||||||
|
StringBuilder_Append(&sb, "}");
|
||||||
|
let json: String = StringBuilder_Build(&sb);
|
||||||
|
StringBuilder_Free(&sb);
|
||||||
|
return Response_Json(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 404 ---
|
||||||
|
return Response_NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// HTML landing page (raw multi-line string via backticks)
|
||||||
|
// =============================================================================
|
||||||
|
func PageHome() -> String {
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Boko Framework</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:system-ui,sans-serif;background:#0d1117;color:#c9d1d9;min-height:100vh;display:flex;align-items:center;justify-content:center}
|
||||||
|
.card{background:#161b22;border:1px solid #30363d;border-radius:12px;padding:3rem;max-width:560px;width:100%}
|
||||||
|
h1{color:#58a6ff;font-size:2rem;margin-bottom:.5rem}
|
||||||
|
.tag{color:#8b949e;font-size:.9rem;margin-bottom:2rem}
|
||||||
|
h3{color:#d2a8ff;margin:1.5rem 0 .75rem}
|
||||||
|
.ep{display:flex;gap:1rem;padding:.35rem 0;font-family:monospace;font-size:.9rem}
|
||||||
|
.ep .m{color:#3fb950;font-weight:bold;min-width:52px}
|
||||||
|
.ep .p{color:#c9d1d9}
|
||||||
|
.ep .d{color:#484f58;margin-left:auto}
|
||||||
|
a{color:#58a6ff}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<h1>⚡ Boko</h1>
|
||||||
|
<p class="tag">Async web framework for Bux — inspired by FastAPI</p>
|
||||||
|
<h3>Try it</h3>
|
||||||
|
<div class="ep"><span class="m">GET</span><span class="p"><a href="/hello?name=Bux">/hello?name=Bux</a></span><span class="d">query param</span></div>
|
||||||
|
<div class="ep"><span class="m">GET</span><span class="p"><a href="/users/42">/users/42</a></span><span class="d">path param</span></div>
|
||||||
|
<div class="ep"><span class="m">GET</span><span class="p"><a href="/posts/7/comments/3">/posts/7/comments/3</a></span><span class="d">multi params</span></div>
|
||||||
|
<div class="ep"><span class="m">GET</span><span class="p"><a href="/redirect">/redirect</a></span><span class="d">302 → /</span></div>
|
||||||
|
<div class="ep"><span class="m">GET</span><span class="p"><a href="/api/health">/api/health</a></span><span class="d">JSON</span></div>
|
||||||
|
<div class="ep"><span class="m">GET</span><span class="p"><a href="/api/info">/api/info</a></span><span class="d">JSON</span></div>
|
||||||
|
<h3>Features</h3>
|
||||||
|
<div class="ep"><span class="m">✓</span><span class="p">Path routing with {params}</span></div>
|
||||||
|
<div class="ep"><span class="m">✓</span><span class="p">Query parameter extraction</span></div>
|
||||||
|
<div class="ep"><span class="m">✓</span><span class="p">JSON / HTML / Text responses</span></div>
|
||||||
|
<div class="ep"><span class="m">✓</span><span class="p">Multi-threaded (configurable)</span></div>
|
||||||
|
<div class="ep"><span class="m">✓</span><span class="p">Redirects (302)</span></div>
|
||||||
|
<div class="ep"><span class="m">✓</span><span class="p">POST body access</span></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Main — start the server
|
||||||
|
// =============================================================================
|
||||||
|
func Main() -> int {
|
||||||
|
let app: App = App_New(8080, 4);
|
||||||
|
App_Run(&app);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // module Main
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
# jwt-pitbul
|
||||||
|
|
||||||
|
**JWT CLI tool for the Bux ecosystem — sign, verify, decode, and key generation.**
|
||||||
|
|
||||||
|
A standalone command-line utility for working with JSON Web Tokens (RFC 7519). Built on `Std::Crypto`, backed by OpenSSL.
|
||||||
|
|
||||||
|
This version (0.2.0) is rewritten with modern Bux: algebraic enums, generic `Array<String>`, methods on `JwtAlg`, string interpolation, `for` loops, and structured `Result`/`Option`-style error handling.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/jwt-pitbul
|
||||||
|
../../buxc build
|
||||||
|
./build/jwt-pitbul
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### `sign` — Create a JWT
|
||||||
|
|
||||||
|
```
|
||||||
|
jwt-pitbul sign <alg> <key> <claims_json>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Argument | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `<alg>` | Algorithm: `HS256`, `HS384`, `HS512`, `RS256`, `RS384`, `RS512`, `ES256`, `ES384`, `EdDSA` |
|
||||||
|
| `<key>` | HMAC secret string, or path to PEM file (RSA/ECDSA), or base64 raw key (Ed25519) |
|
||||||
|
| `<claims_json>` | JSON payload string (e.g. `{"sub":"123","exp":1735689600}`) |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# HMAC
|
||||||
|
jwt-pitbul sign HS256 'my-secret-key' '{"sub":"123","role":"admin"}'
|
||||||
|
|
||||||
|
# RSA (private key from file)
|
||||||
|
jwt-pitbul sign RS256 private.pem '{"sub":"456"}'
|
||||||
|
|
||||||
|
# ECDSA
|
||||||
|
jwt-pitbul sign ES256 ec_private.pem '{"sub":"789"}'
|
||||||
|
|
||||||
|
# Ed25519 (base64-encoded 32-byte private key)
|
||||||
|
jwt-pitbul sign EdDSA 'MC4CAQAwBQYDK2VwBCIEIJ...' '{"sub":"000"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### `verify` — Verify and decode
|
||||||
|
|
||||||
|
```
|
||||||
|
jwt-pitbul verify <token> <alg> <key>
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jwt-pitbul verify eyJhbGciOiJ... HS256 'my-secret-key'
|
||||||
|
# ✓ Signature valid
|
||||||
|
#
|
||||||
|
# Header:
|
||||||
|
# {"alg":"HS256","typ":"JWT"}
|
||||||
|
#
|
||||||
|
# Payload:
|
||||||
|
# {"sub":"123","role":"admin"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `decode` — Decode without verification
|
||||||
|
|
||||||
|
```
|
||||||
|
jwt-pitbul decode <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jwt-pitbul decode eyJhbGciOiJ...
|
||||||
|
# Decoded (no verification):
|
||||||
|
#
|
||||||
|
# Header:
|
||||||
|
# {"alg":"HS256","typ":"JWT"}
|
||||||
|
#
|
||||||
|
# Payload:
|
||||||
|
# {"sub":"123"}
|
||||||
|
#
|
||||||
|
# Signature (base64url): xxxxxxxxx...
|
||||||
|
```
|
||||||
|
|
||||||
|
### `keygen` — Generate keys
|
||||||
|
|
||||||
|
```
|
||||||
|
jwt-pitbul keygen <type>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Type | Output |
|
||||||
|
|------|--------|
|
||||||
|
| `ed25519` | Generates an Ed25519 keypair (prints base64 public/private keys) |
|
||||||
|
| `rsa` | Prints OpenSSL CLI commands to generate RSA 2048-bit keys |
|
||||||
|
| `ecdsa` | Prints OpenSSL CLI commands to generate ECDSA P-256 keys |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
jwt-pitbul keygen ed25519
|
||||||
|
# Ed25519 keypair (base64):
|
||||||
|
# Public: MCowBQYDK2VwAyEA...
|
||||||
|
# Private: MC4CAQAwBQYDK2VwBCIEIJ...
|
||||||
|
|
||||||
|
jwt-pitbul keygen rsa
|
||||||
|
# RSA key generation requires OpenSSL CLI:
|
||||||
|
# openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
|
||||||
|
# openssl rsa -in private.pem -pubout -out public.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Algorithms
|
||||||
|
|
||||||
|
| Algorithm | Type | Key Format |
|
||||||
|
|-----------|------|------------|
|
||||||
|
| HS256 | HMAC-SHA256 | Raw secret string |
|
||||||
|
| HS384 | HMAC-SHA384 | Raw secret string |
|
||||||
|
| HS512 | HMAC-SHA512 | Raw secret string |
|
||||||
|
| RS256 | RSA PKCS#1 v1.5 SHA-256 | PEM file path |
|
||||||
|
| RS384 | RSA PKCS#1 v1.5 SHA-384 | PEM file path |
|
||||||
|
| RS512 | RSA PKCS#1 v1.5 SHA-512 | PEM file path |
|
||||||
|
| ES256 | ECDSA P-256 | PEM file path |
|
||||||
|
| ES384 | ECDSA P-384 | PEM file path |
|
||||||
|
| EdDSA | Ed25519 | Base64 32-byte raw key |
|
||||||
|
|
||||||
|
## Generating Keys with OpenSSL
|
||||||
|
|
||||||
|
### RSA 2048-bit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
|
||||||
|
openssl rsa -in private.pem -pubout -out public.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
### ECDSA P-256
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem
|
||||||
|
openssl ec -in ec_private.pem -pubout -out ec_public.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
### ECDSA P-384
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl ecparam -genkey -name secp384r1 -noout -out ec_private.pem
|
||||||
|
openssl ec -in ec_private.pem -pubout -out ec_public.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ed25519
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In-app generation:
|
||||||
|
jwt-pitbul keygen ed25519
|
||||||
|
|
||||||
|
# Or via OpenSSL:
|
||||||
|
openssl genpkey -algorithm Ed25519 -out ed25519_private.pem
|
||||||
|
openssl pkey -in ed25519_private.pem -pubout -out ed25519_public.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Codes
|
||||||
|
|
||||||
|
| Exit | Meaning |
|
||||||
|
|------|---------|
|
||||||
|
| 0 | Success |
|
||||||
|
| 1 | Error (invalid arguments, bad signature, missing file, etc.) |
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/jwt-pitbul
|
||||||
|
../../buxc build # Bootstrap compiler (Nim)
|
||||||
|
# or
|
||||||
|
../../buxc_lir build # Self-hosted compiler
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Bux standard library (`Std::Crypto`, `Std::Io`, `Std::String`)
|
||||||
|
- OpenSSL 1.1.1+ (linked via runtime)
|
||||||
|
- Bux compiler (`buxc` or `buxc_lir`)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Part of the Bux project. See root [LICENSE](../../LICENSE).
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[Package]
|
||||||
|
Name = "jwt-pitbul"
|
||||||
|
Version = "0.2.0"
|
||||||
|
Type = "bin"
|
||||||
|
Description = "JWT CLI tool — encode, decode, verify, and key generation"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
@@ -0,0 +1,468 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// jwt-pitbul — JWT CLI Tool (encode, decode, verify, keygen)
|
||||||
|
// Rewritten with modern Bux: algebraic enums, methods, string interpolation,
|
||||||
|
// for-in loops, generic arrays, and structured error handling.
|
||||||
|
// =============================================================================
|
||||||
|
module Main {
|
||||||
|
|
||||||
|
import Std::Io::{PrintLine, Print, PrintInt};
|
||||||
|
import Std::String::{
|
||||||
|
String_Len, String_Eq,
|
||||||
|
String_SplitCount, String_SplitPart,
|
||||||
|
StringBuilder, StringBuilder_New, StringBuilder_Append,
|
||||||
|
StringBuilder_AppendInt, StringBuilder_Build, StringBuilder_Free
|
||||||
|
};
|
||||||
|
import Std::Crypto::Jwt::{
|
||||||
|
JwtAlg,
|
||||||
|
Jwt_MakeHeader,
|
||||||
|
Jwt_Encode,
|
||||||
|
Jwt_Decode,
|
||||||
|
Jwt_EncodeHS256, Jwt_EncodeHS384, Jwt_EncodeHS512,
|
||||||
|
Jwt_EncodeRS256, Jwt_EncodeES256, Jwt_EncodeEdDSA
|
||||||
|
};
|
||||||
|
import Std::Crypto::Base64::{Base64URL_Decode, Base64_Encode};
|
||||||
|
import Std::Crypto::Ed25519::{Ed25519_Keypair};
|
||||||
|
|
||||||
|
extern func bux_argc() -> int;
|
||||||
|
extern func bux_argv(index: int) -> String;
|
||||||
|
extern func bux_alloc(size: uint) -> *void;
|
||||||
|
extern func bux_read_file(path: String) -> String;
|
||||||
|
extern func bux_file_exists(path: String) -> int;
|
||||||
|
extern func bux_base64_encode(data: String, len: int) -> String;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Constants
|
||||||
|
// =============================================================================
|
||||||
|
const AppName: String = "jwt-pitbul";
|
||||||
|
const Version: String = "0.2.0";
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Algebraic enums for optionals and results
|
||||||
|
// =============================================================================
|
||||||
|
enum AlgOption {
|
||||||
|
Some(JwtAlg),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
func AlgOption_MakeSome(value: JwtAlg) -> AlgOption {
|
||||||
|
let o: AlgOption = AlgOption { tag: AlgOption_Some };
|
||||||
|
o.data.Some_0 = value;
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
func AlgOption_MakeNone() -> AlgOption {
|
||||||
|
return AlgOption { tag: AlgOption_None };
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CmdOption {
|
||||||
|
Some(Cmd),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
func CmdOption_MakeSome(value: Cmd) -> CmdOption {
|
||||||
|
let o: CmdOption = CmdOption { tag: CmdOption_Some };
|
||||||
|
o.data.Some_0 = value;
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
func CmdOption_MakeNone() -> CmdOption {
|
||||||
|
return CmdOption { tag: CmdOption_None };
|
||||||
|
}
|
||||||
|
|
||||||
|
enum KeyTypeOption {
|
||||||
|
Some(KeyType),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
func KeyTypeOption_MakeSome(value: KeyType) -> KeyTypeOption {
|
||||||
|
let o: KeyTypeOption = KeyTypeOption { tag: KeyTypeOption_Some };
|
||||||
|
o.data.Some_0 = value;
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
func KeyTypeOption_MakeNone() -> KeyTypeOption {
|
||||||
|
return KeyTypeOption { tag: KeyTypeOption_None };
|
||||||
|
}
|
||||||
|
|
||||||
|
enum KeyResult {
|
||||||
|
Ok(String),
|
||||||
|
Err(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
func KeyResult_MakeOk(value: String) -> KeyResult {
|
||||||
|
let r: KeyResult = KeyResult { tag: KeyResult_Ok };
|
||||||
|
r.data.Ok_0 = value;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
func KeyResult_MakeErr(msg: String) -> KeyResult {
|
||||||
|
let r: KeyResult = KeyResult { tag: KeyResult_Err };
|
||||||
|
r.data.Err_0 = msg;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ExitResult {
|
||||||
|
Ok(int),
|
||||||
|
Err(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExitResult_MakeOk(value: int) -> ExitResult {
|
||||||
|
let r: ExitResult = ExitResult { tag: ExitResult_Ok };
|
||||||
|
r.data.Ok_0 = value;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExitResult_MakeErr(msg: String) -> ExitResult {
|
||||||
|
let r: ExitResult = ExitResult { tag: ExitResult_Err };
|
||||||
|
r.data.Err_0 = msg;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Commands
|
||||||
|
// =============================================================================
|
||||||
|
enum Cmd {
|
||||||
|
Sign,
|
||||||
|
Verify,
|
||||||
|
Decode,
|
||||||
|
Keygen,
|
||||||
|
Help,
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseCmd(name: String) -> CmdOption {
|
||||||
|
if String_Eq(name, "sign") { return CmdOption_MakeSome(Cmd { tag: Cmd_Sign }); }
|
||||||
|
if String_Eq(name, "verify") { return CmdOption_MakeSome(Cmd { tag: Cmd_Verify }); }
|
||||||
|
if String_Eq(name, "decode") { return CmdOption_MakeSome(Cmd { tag: Cmd_Decode }); }
|
||||||
|
if String_Eq(name, "keygen") { return CmdOption_MakeSome(Cmd { tag: Cmd_Keygen }); }
|
||||||
|
if String_Eq(name, "help") || String_Eq(name, "--help") || String_Eq(name, "-h") {
|
||||||
|
return CmdOption_MakeSome(Cmd { tag: Cmd_Help });
|
||||||
|
}
|
||||||
|
return CmdOption_MakeNone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Key type generation
|
||||||
|
// =============================================================================
|
||||||
|
enum KeyType {
|
||||||
|
Rsa,
|
||||||
|
Ecdsa,
|
||||||
|
Ed25519,
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseKeyType(name: String) -> KeyTypeOption {
|
||||||
|
if String_Eq(name, "rsa") { return KeyTypeOption_MakeSome(KeyType { tag: KeyType_Rsa }); }
|
||||||
|
if String_Eq(name, "ecdsa") { return KeyTypeOption_MakeSome(KeyType { tag: KeyType_Ecdsa }); }
|
||||||
|
if String_Eq(name, "ed25519") { return KeyTypeOption_MakeSome(KeyType { tag: KeyType_Ed25519 }); }
|
||||||
|
return KeyTypeOption_MakeNone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Extend JwtAlg with parsing / introspection methods
|
||||||
|
// =============================================================================
|
||||||
|
extend JwtAlg {
|
||||||
|
func Name(self: JwtAlg) -> String {
|
||||||
|
match self {
|
||||||
|
JwtAlg::HS256 => "HS256",
|
||||||
|
JwtAlg::HS384 => "HS384",
|
||||||
|
JwtAlg::HS512 => "HS512",
|
||||||
|
JwtAlg::RS256 => "RS256",
|
||||||
|
JwtAlg::RS384 => "RS384",
|
||||||
|
JwtAlg::RS512 => "RS512",
|
||||||
|
JwtAlg::ES256 => "ES256",
|
||||||
|
JwtAlg::ES384 => "ES384",
|
||||||
|
JwtAlg::EdDSA => "EdDSA",
|
||||||
|
_ => "HS256",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsHmac(self: JwtAlg) -> bool {
|
||||||
|
return self.tag == JwtAlg_HS256 || self.tag == JwtAlg_HS384 || self.tag == JwtAlg_HS512;
|
||||||
|
}
|
||||||
|
|
||||||
|
func NeedsPemFile(self: JwtAlg) -> bool {
|
||||||
|
return self.tag == JwtAlg_RS256 || self.tag == JwtAlg_RS384 || self.tag == JwtAlg_RS512 ||
|
||||||
|
self.tag == JwtAlg_ES256 || self.tag == JwtAlg_ES384;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseAlg(name: String) -> AlgOption {
|
||||||
|
if String_Eq(name, "HS256") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_HS256 }); }
|
||||||
|
if String_Eq(name, "HS384") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_HS384 }); }
|
||||||
|
if String_Eq(name, "HS512") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_HS512 }); }
|
||||||
|
if String_Eq(name, "RS256") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_RS256 }); }
|
||||||
|
if String_Eq(name, "RS384") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_RS384 }); }
|
||||||
|
if String_Eq(name, "RS512") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_RS512 }); }
|
||||||
|
if String_Eq(name, "ES256") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_ES256 }); }
|
||||||
|
if String_Eq(name, "ES384") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_ES384 }); }
|
||||||
|
if String_Eq(name, "EdDSA") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_EdDSA }); }
|
||||||
|
return AlgOption_MakeNone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Help / Usage
|
||||||
|
// =============================================================================
|
||||||
|
func PrintUsage() {
|
||||||
|
PrintLine("╔══════════════════════════════════════════════════════╗");
|
||||||
|
PrintLine(f"║ {AppName} — JWT CLI Tool v{Version} ║");
|
||||||
|
PrintLine("║ Sign, verify, decode JSON Web Tokens ║");
|
||||||
|
PrintLine("╚══════════════════════════════════════════════════════╝");
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("Usage:");
|
||||||
|
PrintLine(f" {AppName} sign <alg> <key> <claims>");
|
||||||
|
PrintLine(f" {AppName} verify <token> <alg> <key>");
|
||||||
|
PrintLine(f" {AppName} decode <token>");
|
||||||
|
PrintLine(f" {AppName} keygen <type>");
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("Commands:");
|
||||||
|
PrintLine(" sign Create a signed JWT from claims JSON");
|
||||||
|
PrintLine(" verify Verify a JWT signature and print payload");
|
||||||
|
PrintLine(" decode Decode a JWT without verification");
|
||||||
|
PrintLine(" keygen Generate cryptographic keys");
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("Algorithms:");
|
||||||
|
PrintLine(" HS256, HS384, HS512 — HMAC (symmetric)");
|
||||||
|
PrintLine(" RS256, RS384, RS512 — RSA PKCS#1 v1.5");
|
||||||
|
PrintLine(" ES256, ES384 — ECDSA P-256 / P-384");
|
||||||
|
PrintLine(" EdDSA — Ed25519");
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("Key formats:");
|
||||||
|
PrintLine(" HMAC: raw secret string");
|
||||||
|
PrintLine(" RSA: path to PEM private/public key file");
|
||||||
|
PrintLine(" ECDSA: path to PEM private/public key file");
|
||||||
|
PrintLine(" EdDSA: base64-encoded 32-byte raw key");
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("Key generation:");
|
||||||
|
PrintLine(f" {AppName} keygen rsa # RSA 2048-bit (PEM)");
|
||||||
|
PrintLine(f" {AppName} keygen ecdsa # ECDSA P-256 (PEM)");
|
||||||
|
PrintLine(f" {AppName} keygen ed25519 # Ed25519 (raw base64)");
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("Examples:");
|
||||||
|
Print(f" {AppName} sign HS256 'my-secret' ");
|
||||||
|
PrintLine("'{\"sub\":\"123\"}'");
|
||||||
|
PrintLine(f" {AppName} verify eyJh... HS256 'my-secret'");
|
||||||
|
PrintLine(f" {AppName} decode eyJh...");
|
||||||
|
PrintLine(f" {AppName} keygen ed25519");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Key resolution — for RSA/ECDSA, read PEM file; for HMAC/EdDSA, pass through
|
||||||
|
// =============================================================================
|
||||||
|
func ResolveKey(alg: JwtAlg, keyArg: String) -> KeyResult {
|
||||||
|
if !alg.NeedsPemFile() {
|
||||||
|
return KeyResult_MakeOk(keyArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if bux_file_exists(keyArg) == 0 {
|
||||||
|
return KeyResult_MakeErr(f"ERROR: PEM file not found: {keyArg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
let pem: String = bux_read_file(keyArg);
|
||||||
|
if String_Len(pem) == 0 {
|
||||||
|
return KeyResult_MakeErr(f"ERROR: could not read PEM file: {keyArg}");
|
||||||
|
}
|
||||||
|
return KeyResult_MakeOk(pem);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Command: sign
|
||||||
|
// =============================================================================
|
||||||
|
func CmdSign(algName: String, keyArg: String, claimsJson: String) -> ExitResult {
|
||||||
|
let algOpt: AlgOption = ParseAlg(algName);
|
||||||
|
if algOpt.tag != AlgOption_Some {
|
||||||
|
return ExitResult_MakeErr(f"ERROR: unknown algorithm '{algName}'");
|
||||||
|
}
|
||||||
|
let alg: JwtAlg = algOpt.data.Some_0;
|
||||||
|
|
||||||
|
let keyRes: KeyResult = ResolveKey(alg, keyArg);
|
||||||
|
if keyRes.tag != KeyResult_Ok {
|
||||||
|
return ExitResult_MakeErr(keyRes.data.Err_0);
|
||||||
|
}
|
||||||
|
let key: String = keyRes.data.Ok_0;
|
||||||
|
|
||||||
|
let header: String = Jwt_MakeHeader(alg);
|
||||||
|
let token: String = Jwt_Encode(header, claimsJson, alg, key);
|
||||||
|
|
||||||
|
if String_Len(token) == 0 {
|
||||||
|
return ExitResult_MakeErr("ERROR: signing failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintLine(token);
|
||||||
|
return ExitResult_MakeOk(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Command: verify
|
||||||
|
// =============================================================================
|
||||||
|
func CmdVerify(token: String, algName: String, keyArg: String) -> ExitResult {
|
||||||
|
let algOpt: AlgOption = ParseAlg(algName);
|
||||||
|
if algOpt.tag != AlgOption_Some {
|
||||||
|
return ExitResult_MakeErr(f"ERROR: unknown algorithm '{algName}'");
|
||||||
|
}
|
||||||
|
let alg: JwtAlg = algOpt.data.Some_0;
|
||||||
|
|
||||||
|
let keyRes: KeyResult = ResolveKey(alg, keyArg);
|
||||||
|
if keyRes.tag != KeyResult_Ok {
|
||||||
|
return ExitResult_MakeErr(keyRes.data.Err_0);
|
||||||
|
}
|
||||||
|
let key: String = keyRes.data.Ok_0;
|
||||||
|
|
||||||
|
var header: String = "";
|
||||||
|
var payload: String = "";
|
||||||
|
|
||||||
|
if !Jwt_Decode(token, alg, key, &header, &payload) {
|
||||||
|
return ExitResult_MakeErr("✗ Signature INVALID (or malformed token)");
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintLine("✓ Signature valid");
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("Header:");
|
||||||
|
PrintLine(header);
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("Payload:");
|
||||||
|
PrintLine(payload);
|
||||||
|
return ExitResult_MakeOk(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Command: decode (no verification)
|
||||||
|
// =============================================================================
|
||||||
|
func CmdDecode(token: String) -> ExitResult {
|
||||||
|
let partCount: uint = String_SplitCount(token, ".");
|
||||||
|
if partCount != 3 {
|
||||||
|
return ExitResult_MakeErr("ERROR: not a valid JWT (expected 3 parts)");
|
||||||
|
}
|
||||||
|
|
||||||
|
let headerB64: String = String_SplitPart(token, ".", 0);
|
||||||
|
let payloadB64: String = String_SplitPart(token, ".", 1);
|
||||||
|
let sigB64: String = String_SplitPart(token, ".", 2);
|
||||||
|
|
||||||
|
let headerJson: String = Base64URL_Decode(headerB64);
|
||||||
|
let payloadJson: String = Base64URL_Decode(payloadB64);
|
||||||
|
|
||||||
|
PrintLine("Decoded (no verification):");
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("Header:");
|
||||||
|
PrintLine(headerJson);
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("Payload:");
|
||||||
|
PrintLine(payloadJson);
|
||||||
|
PrintLine("");
|
||||||
|
Print("Signature (base64url): ");
|
||||||
|
PrintLine(sigB64);
|
||||||
|
|
||||||
|
return ExitResult_MakeOk(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Command: keygen
|
||||||
|
// =============================================================================
|
||||||
|
func CmdKeygen(keyType: String) -> ExitResult {
|
||||||
|
let ktOpt: KeyTypeOption = ParseKeyType(keyType);
|
||||||
|
if ktOpt.tag != KeyTypeOption_Some {
|
||||||
|
return ExitResult_MakeErr(f"ERROR: unknown key type '{keyType}'. Use: rsa, ecdsa, ed25519");
|
||||||
|
}
|
||||||
|
let kt: KeyType = ktOpt.data.Some_0;
|
||||||
|
|
||||||
|
if kt.tag == KeyType_Ed25519 {
|
||||||
|
let pubBuf: *void = bux_alloc(32);
|
||||||
|
let priv: *void = bux_alloc(32);
|
||||||
|
if !Ed25519_Keypair(pubBuf, priv) {
|
||||||
|
return ExitResult_MakeErr("ERROR: Ed25519 key generation failed (OpenSSL 1.1.1+ required)");
|
||||||
|
}
|
||||||
|
let pubB64: String = bux_base64_encode(pubBuf as String, 32);
|
||||||
|
let privB64: String = bux_base64_encode(priv as String, 32);
|
||||||
|
PrintLine("Ed25519 keypair (base64):");
|
||||||
|
PrintLine(f" Public: {pubB64}");
|
||||||
|
PrintLine(f" Private: {privB64}");
|
||||||
|
return ExitResult_MakeOk(0);
|
||||||
|
}
|
||||||
|
if kt.tag == KeyType_Rsa {
|
||||||
|
PrintLine("RSA key generation requires OpenSSL CLI:");
|
||||||
|
PrintLine(" openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048");
|
||||||
|
PrintLine(" openssl rsa -in private.pem -pubout -out public.pem");
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("(Key generation from within Bux requires PEM write support — coming soon)");
|
||||||
|
return ExitResult_MakeOk(0);
|
||||||
|
}
|
||||||
|
if kt.tag == KeyType_Ecdsa {
|
||||||
|
PrintLine("ECDSA key generation requires OpenSSL CLI:");
|
||||||
|
PrintLine(" openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem");
|
||||||
|
PrintLine(" openssl ec -in ec_private.pem -pubout -out ec_public.pem");
|
||||||
|
PrintLine("");
|
||||||
|
PrintLine("(Key generation from within Bux requires PEM write support — coming soon)");
|
||||||
|
return ExitResult_MakeOk(0);
|
||||||
|
}
|
||||||
|
return ExitResult_MakeErr(f"ERROR: unknown key type '{keyType}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Dispatch a parsed command to its handler
|
||||||
|
// =============================================================================
|
||||||
|
func RunCommand(cmd: Cmd, args: Array<String>) -> ExitResult {
|
||||||
|
if cmd.tag == Cmd_Help {
|
||||||
|
PrintUsage();
|
||||||
|
return ExitResult_MakeOk(0);
|
||||||
|
}
|
||||||
|
if cmd.tag == Cmd_Sign {
|
||||||
|
if args.len < 5 {
|
||||||
|
return ExitResult_MakeErr("ERROR: 'sign' requires: <alg> <key> <claims_json>");
|
||||||
|
}
|
||||||
|
return CmdSign(args[2], args[3], args[4]);
|
||||||
|
}
|
||||||
|
if cmd.tag == Cmd_Verify {
|
||||||
|
if args.len < 5 {
|
||||||
|
return ExitResult_MakeErr("ERROR: 'verify' requires: <token> <alg> <key>");
|
||||||
|
}
|
||||||
|
return CmdVerify(args[2], args[3], args[4]);
|
||||||
|
}
|
||||||
|
if cmd.tag == Cmd_Decode {
|
||||||
|
if args.len < 3 {
|
||||||
|
return ExitResult_MakeErr("ERROR: 'decode' requires: <token>");
|
||||||
|
}
|
||||||
|
return CmdDecode(args[2]);
|
||||||
|
}
|
||||||
|
if cmd.tag == Cmd_Keygen {
|
||||||
|
if args.len < 3 {
|
||||||
|
return ExitResult_MakeErr("ERROR: 'keygen' requires: <type>");
|
||||||
|
}
|
||||||
|
return CmdKeygen(args[2]);
|
||||||
|
}
|
||||||
|
return ExitResult_MakeErr("ERROR: unhandled command");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Main Entry Point
|
||||||
|
// =============================================================================
|
||||||
|
func Main() -> int {
|
||||||
|
let argc: int = bux_argc();
|
||||||
|
|
||||||
|
// Collect CLI arguments into a generic Array<String>
|
||||||
|
let args: Array<String> = Array_New<String>(argc as uint);
|
||||||
|
for i in 0..argc {
|
||||||
|
Array_Push<String>(&args, bux_argv(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.len < 2 {
|
||||||
|
PrintUsage();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let command: String = args[1];
|
||||||
|
let cmdOpt: CmdOption = ParseCmd(command);
|
||||||
|
if cmdOpt.tag != CmdOption_Some {
|
||||||
|
PrintLine(f"ERROR: unknown command '{command}'");
|
||||||
|
PrintLine(f"Run '{AppName} help' for usage.");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
let cmd: Cmd = cmdOpt.data.Some_0;
|
||||||
|
|
||||||
|
let result: ExitResult = RunCommand(cmd, args);
|
||||||
|
if result.tag == ExitResult_Err {
|
||||||
|
PrintLine(result.data.Err_0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return result.data.Ok_0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // module Main
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
# Nexus
|
||||||
|
|
||||||
|
**High-performance, multi-threaded HTTP/1.1, HTTP/2 & WebSocket server — built with the [Bux](https://github.com/bux-lang/bux) programming language.**
|
||||||
|
|
||||||
|
Nexus is a from-scratch web server that demonstrates Bux's systems-programming capabilities: raw TCP sockets, pthread-based concurrency, manual memory management, and zero-dependency C ABI interop — all from a clean, modern syntax.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
| Area | What's Implemented |
|
||||||
|
|------|-------------------|
|
||||||
|
| **HTTP/1.1** | Full request parsing, **keep-alive** (reuse TCP), status codes, content negotiation |
|
||||||
|
| **Multi-threaded** | Worker pool + channel task queue; configurable via `NEXUS_WORKERS` |
|
||||||
|
| **HTTP/2** | Connection preface detection (`PRI * HTTP/2.0`), upgrade-aware routing |
|
||||||
|
| **WebSocket** | RFC 6455 upgrade handshake detection, `Sec-WebSocket-Key` extraction |
|
||||||
|
| **Static files** | Serves from `public/` with MIME-type detection for 20+ file types, directory-traversal protection |
|
||||||
|
| **JSON API** | Built-in `/api/health` and `/api/info` endpoints |
|
||||||
|
| **Logging** | Access log: `METHOD path status duration_ms` (`NEXUS_ACCESS_LOG=0` to disable) |
|
||||||
|
| **Limits** | `NEXUS_MAX_BODY` (default 1 MiB) → HTTP 413 when exceeded |
|
||||||
|
| **Graceful stop** | SIGINT/SIGTERM: close listen fd, poison workers, exit 0 |
|
||||||
|
| **TLS / HTTPS** | OpenSSL server mode via `NEXUS_TLS=1` + PEM cert/key |
|
||||||
|
| **mTLS** | `NEXUS_TLS_CLIENT_CA` PEM → require client certificates |
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From the project root
|
||||||
|
cd apps/nexus
|
||||||
|
|
||||||
|
# Build with the bootstrap compiler (Nim)
|
||||||
|
../../buxc build
|
||||||
|
|
||||||
|
# Or with the self-hosted compiler
|
||||||
|
../../buxc_lir build
|
||||||
|
|
||||||
|
# Run
|
||||||
|
./nexus
|
||||||
|
|
||||||
|
# Optional env (also used by `make bench-nexus`)
|
||||||
|
# NEXUS_PORT=18080 NEXUS_BIND=127.0.0.1 NEXUS_WORKERS=4 \
|
||||||
|
# NEXUS_MAX_BODY=1048576 NEXUS_ACCESS_LOG=1 ./build/nexus
|
||||||
|
|
||||||
|
# HTTPS (self-signed example)
|
||||||
|
# openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 365 -subj /CN=localhost
|
||||||
|
# NEXUS_TLS=1 NEXUS_TLS_CERT=cert.pem NEXUS_TLS_KEY=key.pem NEXUS_PORT=8443 ./build/nexus
|
||||||
|
# curl -k https://127.0.0.1:8443/api/health
|
||||||
|
|
||||||
|
# mTLS (require client cert signed by CA)
|
||||||
|
# NEXUS_TLS_CLIENT_CA=ca.pem NEXUS_TLS=1 NEXUS_TLS_CERT=server.pem NEXUS_TLS_KEY=server.key …
|
||||||
|
# curl --cert client.pem --key client.key --cacert ca.pem https://…
|
||||||
|
```
|
||||||
|
|
||||||
|
Server starts on `http://0.0.0.0:8080` (or `https://` when TLS is enabled).
|
||||||
|
Stop with **Ctrl+C** or `kill -TERM` (graceful: workers drained via poison pills).
|
||||||
|
|
||||||
|
Smoke: `make test-nexus-tls` (self-signed cert + curl -k).
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Full Nexus (needs libssl3)
|
||||||
|
../../buxc --release build # from apps/nexus
|
||||||
|
docker build -f ../../examples/docker/Dockerfile.nexus -t bux-nexus ../..
|
||||||
|
docker run --rm -p 8080:8080 bux-nexus
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
╔══════════════════════════════════════════════╗
|
||||||
|
║ Nexus HTTP Server v0.1.0 ║
|
||||||
|
║ High-performance multi-threaded HTTP/1.1 ║
|
||||||
|
║ HTTP/2 & WebSocket detection included ║
|
||||||
|
║ Built with Bux ║
|
||||||
|
╚══════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
✓ Server listening on http://0.0.0.0:8080
|
||||||
|
✓ Worker threads: 4
|
||||||
|
✓ Static files: ./public/
|
||||||
|
|
||||||
|
Endpoints:
|
||||||
|
GET / — Static files (public/)
|
||||||
|
GET /api/health — Health check (JSON)
|
||||||
|
GET /api/info — Server info (JSON)
|
||||||
|
GET /ws — WebSocket upgrade
|
||||||
|
ANY /* — Static file serving
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing It
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Home page (HTML)
|
||||||
|
curl http://localhost:8080/
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
curl http://localhost:8080/api/health
|
||||||
|
# → {"status":"ok","server":"Nexus","version":"0.1.0"}
|
||||||
|
|
||||||
|
# Server info
|
||||||
|
curl http://localhost:8080/api/info
|
||||||
|
# → {"name":"Nexus","language":"Bux","features":[...]}
|
||||||
|
|
||||||
|
# Static file (404 page)
|
||||||
|
curl http://localhost:8080/404.html
|
||||||
|
|
||||||
|
# Nonexistent file
|
||||||
|
curl http://localhost:8080/nope
|
||||||
|
# → 404 Not Found
|
||||||
|
|
||||||
|
# WebSocket upgrade attempt
|
||||||
|
curl -H "Upgrade: websocket" \
|
||||||
|
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
|
||||||
|
http://localhost:8080/ws
|
||||||
|
# → 101 Switching Protocols
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Thread Model — Multi-Accept
|
||||||
|
|
||||||
|
Nexus uses the **multi-accept** pattern rather than a traditional thread pool with a work queue:
|
||||||
|
|
||||||
|
```
|
||||||
|
Main Thread Worker 1 Worker 2 Worker 3
|
||||||
|
│ │ │ │
|
||||||
|
├─ socket()/bind()/listen() │ │ │
|
||||||
|
├─ spawn Worker() ──────────► │ │
|
||||||
|
├─ spawn Worker() ─────────────────────────► │
|
||||||
|
├─ spawn Worker() ────────────────────────────────────────►
|
||||||
|
│ │ │ │
|
||||||
|
▼ Worker() ▼ accept() ▼ accept() ▼ accept()
|
||||||
|
accept() loop │ │ │
|
||||||
|
```
|
||||||
|
|
||||||
|
Each worker thread calls `accept()` on the **same** listening socket. The Linux kernel distributes incoming connections across the blocked accept calls — the same mechanism used by nginx and Apache prefork. No mutex, no queue, no context switching between a dispatcher and workers.
|
||||||
|
|
||||||
|
### Request Lifecycle
|
||||||
|
|
||||||
|
```
|
||||||
|
Client connects
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Net_Accept() → client fd
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Net_Recv(fd, 8192) → raw bytes
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
ParseRequest()
|
||||||
|
├─ Split headers on \r\n\r\n
|
||||||
|
├─ Parse request line → method, path, version
|
||||||
|
└─ Parse header lines → key-value array
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Router_Dispatch()
|
||||||
|
├─ /api/* → JSON handlers
|
||||||
|
├─ /ws → WebSocket upgrade
|
||||||
|
├─ Upgrade header → HTTP/2 or WS detection
|
||||||
|
└─ /* → Static file serving
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
BuildResponse() → HTTP/1.1 status line + headers + body
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Net_Send(fd, response) → bytes to client
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Net_Close(fd)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Design Decisions
|
||||||
|
|
||||||
|
- **No keep-alive by default.** Each connection is closed after one response. This avoids blocking worker threads on idle clients (Bux doesn't yet expose `SO_RCVTIMEO`).
|
||||||
|
- **Linear header array instead of hash map.** HTTP requests typically carry 5–15 headers. A linear scan over a key-value array is faster than hashing for this N, and avoids the complexity of iterating over Bux's generic `StringMap`.
|
||||||
|
- **Raw extern calls for the string builder.** The stdlib `StringBuilder` wrapper adds a struct indirection. Calling `bux_sb_new` / `bux_sb_append` / `bux_sb_build` directly is simpler and equally safe.
|
||||||
|
- **WebSocket accept key is a placeholder.** A full implementation needs SHA-1 hashing and Base64 encoding. These aren't in Bux's stdlib yet; they can be added as extern C functions when needed.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
apps/nexus/
|
||||||
|
├── bux.toml # Package manifest
|
||||||
|
├── README.md # This file
|
||||||
|
├── src/
|
||||||
|
│ └── Main.bux # The entire server (~640 lines)
|
||||||
|
└── public/
|
||||||
|
├── index.html # Landing page
|
||||||
|
└── 404.html # Error page
|
||||||
|
```
|
||||||
|
|
||||||
|
Everything lives in one file (`src/Main.bux`) by design — it keeps the module graph flat and the build fast. As the server grows, the HTTP parser, router, and handlers can be split into separate modules.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Edit the constants at the top of `src/Main.bux`:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
const SERVER_PORT: int = 8080; // Listen port
|
||||||
|
const THREAD_COUNT: int = 4; // Number of worker threads
|
||||||
|
const RECV_BUF_SIZE: int = 8192; // Receive buffer per request
|
||||||
|
const SERVER_NAME: String = "Nexus/0.1.0 (Bux)";
|
||||||
|
const PUBLIC_DIR: String = "public"; // Static files directory
|
||||||
|
```
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
- [ ] Keep-alive with configurable socket timeout
|
||||||
|
- [ ] Full WebSocket frame read/write (requires SHA-1 + Base64)
|
||||||
|
- [ ] HTTP/2 binary framing layer (HPACK, stream multiplexing)
|
||||||
|
- [ ] SSL/TLS via OpenSSL extern bindings
|
||||||
|
- [ ] Middleware / filter chain
|
||||||
|
- [ ] Request body parsing (JSON, form-encoded, multipart)
|
||||||
|
- [ ] Virtual hosts
|
||||||
|
- [ ] Access logging to file
|
||||||
|
- [ ] Rate limiting
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Nexus is part of the Bux project. See the root [LICENSE](../../LICENSE) for terms.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
EXIT:137
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
EXIT:124
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
EXIT:124
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
EXIT:124
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
EXIT:124
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
timeout: командата „„../../buxc““ не може да бъде изпълнена: Няма такъв файл или директория
|
||||||
|
EXIT:127
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
EXIT:137
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[Package]
|
||||||
|
Name = "nexus"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
Description = "High-performance multi-threaded HTTP/1.1, HTTP/2 and WebSocket server"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>404 — Not Found | Nexus</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #0d1117;
|
||||||
|
color: #c9d1d9;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
h1 { font-size: 4rem; color: #f85149; margin-bottom: 0.5rem; }
|
||||||
|
p { color: #8b949e; font-size: 1.2rem; }
|
||||||
|
.back { margin-top: 2rem; }
|
||||||
|
.back a { color: #58a6ff; text-decoration: none; }
|
||||||
|
.back a:hover { text-decoration: underline; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>404</h1>
|
||||||
|
<p>The requested resource was not found on this server.</p>
|
||||||
|
<p class="back"><a href="/">← Back to Home</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<html><body>Hello Nexus</body></html>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
module Config {
|
||||||
|
|
||||||
|
pub struct ServerConfig {
|
||||||
|
bindAddr: String;
|
||||||
|
port: int;
|
||||||
|
workerCount: int;
|
||||||
|
publicDir: String;
|
||||||
|
backlog: int;
|
||||||
|
/// Max raw request bytes (recv buffer / body safety). Default 1 MiB.
|
||||||
|
maxBodyBytes: int;
|
||||||
|
/// Access log to stdout (method path status ms). Default true.
|
||||||
|
accessLog: bool;
|
||||||
|
/// Enable HTTPS (OpenSSL). Requires tlsCertPath + tlsKeyPath.
|
||||||
|
tlsEnabled: bool;
|
||||||
|
tlsCertPath: String;
|
||||||
|
tlsKeyPath: String;
|
||||||
|
/// Optional client CA PEM → mTLS (require client certificate).
|
||||||
|
tlsClientCaPath: String;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const func DefaultConfig() -> ServerConfig {
|
||||||
|
return ServerConfig {
|
||||||
|
bindAddr: "0.0.0.0",
|
||||||
|
port: 8080,
|
||||||
|
workerCount: 4,
|
||||||
|
publicDir: "public",
|
||||||
|
backlog: 128,
|
||||||
|
maxBodyBytes: 1048576,
|
||||||
|
accessLog: true,
|
||||||
|
tlsEnabled: false,
|
||||||
|
tlsCertPath: "",
|
||||||
|
tlsKeyPath: "",
|
||||||
|
tlsClientCaPath: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
module Errors {
|
||||||
|
|
||||||
|
import Http::{HttpRequest};
|
||||||
|
|
||||||
|
pub enum HttpError {
|
||||||
|
BadRequest,
|
||||||
|
NotFound,
|
||||||
|
MethodNotAllowed,
|
||||||
|
InternalError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ParseResult {
|
||||||
|
Ok(HttpRequest),
|
||||||
|
Err(HttpError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum FileResult {
|
||||||
|
Ok(String),
|
||||||
|
Err(HttpError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func ParseResult_NewOk(req: HttpRequest) -> ParseResult {
|
||||||
|
let r: ParseResult = ParseResult { tag: ParseResult_Ok };
|
||||||
|
r.data.Ok_0 = req;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func ParseResult_NewErr(err: HttpError) -> ParseResult {
|
||||||
|
let r: ParseResult = ParseResult { tag: ParseResult_Err };
|
||||||
|
r.data.Err_0 = err;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func FileResult_NewOk(content: String) -> FileResult {
|
||||||
|
let r: FileResult = FileResult { tag: FileResult_Ok };
|
||||||
|
r.data.Ok_0 = content;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func FileResult_NewErr(err: HttpError) -> FileResult {
|
||||||
|
let r: FileResult = FileResult { tag: FileResult_Err };
|
||||||
|
r.data.Err_0 = err;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func FileResult_IsOk(r: FileResult) -> bool {
|
||||||
|
return r.tag == FileResult_Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func FileResult_Unwrap(r: FileResult) -> String {
|
||||||
|
return r.data.Ok_0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func FileResult_UnwrapErr(r: FileResult) -> HttpError {
|
||||||
|
return r.data.Err_0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func HttpError_ToString(err: HttpError) -> String {
|
||||||
|
if err.tag == HttpError_BadRequest { return "Bad Request"; }
|
||||||
|
if err.tag == HttpError_NotFound { return "Not Found"; }
|
||||||
|
if err.tag == HttpError_MethodNotAllowed { return "Method Not Allowed"; }
|
||||||
|
return err.data.InternalError_0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
module Handlers {
|
||||||
|
|
||||||
|
import Std::String::{String_Eq, String_Contains};
|
||||||
|
import Http::{HttpMethod, HttpRequest, HttpResponse, Http_NewResponse, Http_MimeType, RequestHeader_Get};
|
||||||
|
import Errors::{HttpError, FileResult, FileResult_NewOk, FileResult_NewErr, FileResult_IsOk, FileResult_Unwrap, FileResult_UnwrapErr, HttpError_ToString};
|
||||||
|
|
||||||
|
extern func bux_strlen(s: String) -> uint;
|
||||||
|
extern func bux_file_exists(path: String) -> int;
|
||||||
|
extern func bux_read_file(path: String) -> String;
|
||||||
|
extern func bux_path_join(a: String, b: String) -> String;
|
||||||
|
extern func bux_sb_new(initial_cap: uint) -> *void;
|
||||||
|
extern func bux_sb_append(sb: *void, s: String);
|
||||||
|
extern func bux_sb_append_int(sb: *void, n: int64);
|
||||||
|
extern func bux_sb_build(sb: *void) -> String;
|
||||||
|
extern func bux_sb_free(sb: *void);
|
||||||
|
|
||||||
|
pub func NotFoundResponse() -> HttpResponse {
|
||||||
|
return Http_NewResponse(404, "application/json; charset=utf-8", "{\"error\":\"not_found\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func MethodNotAllowedResponse() -> HttpResponse {
|
||||||
|
return Http_NewResponse(405, "text/plain; charset=utf-8", "Method Not Allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func ReadStaticFile(requestPath: String) -> FileResult {
|
||||||
|
if String_Contains(requestPath, "..") {
|
||||||
|
return FileResult_NewErr(HttpError { tag: HttpError_NotFound });
|
||||||
|
}
|
||||||
|
|
||||||
|
var filePath: String = requestPath;
|
||||||
|
if String_Eq(filePath, "/") {
|
||||||
|
filePath = "/index.html";
|
||||||
|
}
|
||||||
|
|
||||||
|
let fullPath: String = bux_path_join("public", filePath);
|
||||||
|
if bux_file_exists(fullPath) == 0 {
|
||||||
|
return FileResult_NewErr(HttpError { tag: HttpError_NotFound });
|
||||||
|
}
|
||||||
|
|
||||||
|
let content: String = bux_read_file(fullPath);
|
||||||
|
return FileResult_NewOk(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileErrorResponse(err: HttpError) -> HttpResponse {
|
||||||
|
match err {
|
||||||
|
HttpError::NotFound => NotFoundResponse(),
|
||||||
|
_ => Http_NewResponse(500, "text/plain; charset=utf-8", HttpError_ToString(err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func ServeStaticFile(req: HttpRequest) -> HttpResponse {
|
||||||
|
if req.method != HttpMethod_GET && req.method != HttpMethod_HEAD {
|
||||||
|
return MethodNotAllowedResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: FileResult = ReadStaticFile(req.path);
|
||||||
|
if FileResult_IsOk(result) {
|
||||||
|
let content: String = FileResult_Unwrap(result);
|
||||||
|
return Http_NewResponse(200, Http_MimeType(req.path), content);
|
||||||
|
}
|
||||||
|
return FileErrorResponse(FileResult_UnwrapErr(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func HandleApiHealth() -> HttpResponse {
|
||||||
|
return Http_NewResponse(200, "application/json; charset=utf-8",
|
||||||
|
"{\"status\":\"ok\",\"server\":\"Nexus\",\"version\":\"0.6.0\"}");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func HandleApiInfo() -> HttpResponse {
|
||||||
|
return Http_NewResponse(200, "application/json; charset=utf-8",
|
||||||
|
"{\"name\":\"Nexus\",\"language\":\"Bux\",\"features\":[\"HTTP/1.1\",\"TLS\",\"mTLS\",\"keep-alive\",\"thread-pool\",\"graceful-stop\",\"access-log\",\"max-body\"]}");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func HandleWebSocketUpgrade(req: HttpRequest) -> HttpResponse {
|
||||||
|
var reqMut: HttpRequest = req;
|
||||||
|
let wsKey: String = RequestHeader_Get(&reqMut, "Sec-WebSocket-Key");
|
||||||
|
|
||||||
|
var resp: HttpResponse;
|
||||||
|
resp.statusCode = 101;
|
||||||
|
resp.contentType = "";
|
||||||
|
resp.body = "";
|
||||||
|
|
||||||
|
let sb: *void = bux_sb_new(256);
|
||||||
|
bux_sb_append(sb, "Upgrade: websocket\r\n");
|
||||||
|
bux_sb_append(sb, "Connection: Upgrade\r\n");
|
||||||
|
bux_sb_append(sb, "Sec-WebSocket-Accept: ");
|
||||||
|
bux_sb_append(sb, wsKey);
|
||||||
|
bux_sb_append(sb, "\r\n");
|
||||||
|
resp.extraHeaders = bux_sb_build(sb);
|
||||||
|
bux_sb_free(sb);
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
module Http {
|
||||||
|
|
||||||
|
import Std::Array::{Array, Array_Len, Array_Get};
|
||||||
|
import Std::String::{String_Eq, String_EndsWith, String_Contains, String_Len, String_StartsWith};
|
||||||
|
|
||||||
|
pub enum HttpMethod {
|
||||||
|
GET,
|
||||||
|
POST,
|
||||||
|
PUT,
|
||||||
|
DELETE,
|
||||||
|
PATCH,
|
||||||
|
HEAD,
|
||||||
|
OPTIONS,
|
||||||
|
UNKNOWN,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HeaderEntry {
|
||||||
|
key: String;
|
||||||
|
value: String;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HttpRequest {
|
||||||
|
method: HttpMethod;
|
||||||
|
path: String;
|
||||||
|
version: String;
|
||||||
|
body: String;
|
||||||
|
headers: Array<HeaderEntry>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HttpResponse {
|
||||||
|
statusCode: int;
|
||||||
|
contentType: String;
|
||||||
|
body: String;
|
||||||
|
extraHeaders: String;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func Http_StatusText(code: int) -> String {
|
||||||
|
if code == 200 { return "OK"; }
|
||||||
|
if code == 201 { return "Created"; }
|
||||||
|
if code == 204 { return "No Content"; }
|
||||||
|
if code == 301 { return "Moved Permanently"; }
|
||||||
|
if code == 302 { return "Found"; }
|
||||||
|
if code == 304 { return "Not Modified"; }
|
||||||
|
if code == 400 { return "Bad Request"; }
|
||||||
|
if code == 401 { return "Unauthorized"; }
|
||||||
|
if code == 403 { return "Forbidden"; }
|
||||||
|
if code == 404 { return "Not Found"; }
|
||||||
|
if code == 405 { return "Method Not Allowed"; }
|
||||||
|
if code == 413 { return "Payload Too Large"; }
|
||||||
|
if code == 414 { return "URI Too Long"; }
|
||||||
|
if code == 500 { return "Internal Server Error"; }
|
||||||
|
if code == 501 { return "Not Implemented"; }
|
||||||
|
if code == 503 { return "Service Unavailable"; }
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func Http_MimeType(path: String) -> String {
|
||||||
|
if String_EndsWith(path, ".html") || String_EndsWith(path, ".htm") { return "text/html; charset=utf-8"; }
|
||||||
|
if String_EndsWith(path, ".css") { return "text/css; charset=utf-8"; }
|
||||||
|
if String_EndsWith(path, ".js") { return "application/javascript; charset=utf-8"; }
|
||||||
|
if String_EndsWith(path, ".json") { return "application/json; charset=utf-8"; }
|
||||||
|
if String_EndsWith(path, ".xml") { return "application/xml; charset=utf-8"; }
|
||||||
|
if String_EndsWith(path, ".txt") { return "text/plain; charset=utf-8"; }
|
||||||
|
if String_EndsWith(path, ".png") { return "image/png"; }
|
||||||
|
if String_EndsWith(path, ".jpg") || String_EndsWith(path, ".jpeg") { return "image/jpeg"; }
|
||||||
|
if String_EndsWith(path, ".gif") { return "image/gif"; }
|
||||||
|
if String_EndsWith(path, ".svg") { return "image/svg+xml"; }
|
||||||
|
if String_EndsWith(path, ".ico") { return "image/x-icon"; }
|
||||||
|
if String_EndsWith(path, ".webp") { return "image/webp"; }
|
||||||
|
if String_EndsWith(path, ".woff2") { return "font/woff2"; }
|
||||||
|
if String_EndsWith(path, ".woff") { return "font/woff"; }
|
||||||
|
if String_EndsWith(path, ".wasm") { return "application/wasm"; }
|
||||||
|
return "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func Http_MethodName(m: HttpMethod) -> String {
|
||||||
|
match m {
|
||||||
|
HttpMethod::GET => "GET",
|
||||||
|
HttpMethod::POST => "POST",
|
||||||
|
HttpMethod::PUT => "PUT",
|
||||||
|
HttpMethod::DELETE => "DELETE",
|
||||||
|
HttpMethod::PATCH => "PATCH",
|
||||||
|
HttpMethod::HEAD => "HEAD",
|
||||||
|
HttpMethod::OPTIONS => "OPTIONS",
|
||||||
|
HttpMethod::UNKNOWN => "UNKNOWN",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func Http_NewResponse(code: int, contentType: String, body: String) -> HttpResponse {
|
||||||
|
var resp: HttpResponse;
|
||||||
|
resp.statusCode = code;
|
||||||
|
resp.contentType = contentType;
|
||||||
|
resp.body = body;
|
||||||
|
resp.extraHeaders = "";
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
func CharLower(c: int) -> int {
|
||||||
|
if c >= 65 && c <= 90 {
|
||||||
|
return c + 32;
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Case-insensitive string equality (HTTP header names/values).
|
||||||
|
pub func String_EqIgnoreCase(a: String, b: String) -> bool {
|
||||||
|
let la: uint = String_Len(a);
|
||||||
|
let lb: uint = String_Len(b);
|
||||||
|
if la != lb {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var i: uint = 0;
|
||||||
|
while i < la {
|
||||||
|
if CharLower(a[i] as int) != CharLower(b[i] as int) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func RequestHeader_Get(req: *HttpRequest, key: String) -> String {
|
||||||
|
// Index-based walk — for-in over Array<struct{String}> can corrupt string fields.
|
||||||
|
let n: uint = Array_Len<HeaderEntry>(&req.headers);
|
||||||
|
var i: uint = 0;
|
||||||
|
while i < n {
|
||||||
|
let entry: HeaderEntry = Array_Get<HeaderEntry>(&req.headers, i);
|
||||||
|
if String_EqIgnoreCase(entry.key, key) {
|
||||||
|
return entry.value;
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// HTTP/1.1 defaults to keep-alive; Connection: close forces close;
|
||||||
|
/// HTTP/1.0 needs explicit keep-alive. Uses parsed headers (safe after
|
||||||
|
/// Parser field-move ownership handoff).
|
||||||
|
pub func Request_WantsKeepAlive(req: *HttpRequest) -> bool {
|
||||||
|
let conn: String = RequestHeader_Get(req, "Connection");
|
||||||
|
if String_EqIgnoreCase(conn, "close") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if String_EqIgnoreCase(conn, "keep-alive") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if String_StartsWith(req.version, "HTTP/1.0") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fallback when only raw bytes are available.
|
||||||
|
pub func RawRequest_WantsKeepAlive(raw: String) -> bool {
|
||||||
|
if String_Contains(raw, "Connection: close") || String_Contains(raw, "connection: close") ||
|
||||||
|
String_Contains(raw, "CONNECTION: CLOSE") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if String_Contains(raw, "HTTP/1.0") {
|
||||||
|
if String_Contains(raw, "Connection: keep-alive") ||
|
||||||
|
String_Contains(raw, "Connection: Keep-Alive") ||
|
||||||
|
String_Contains(raw, "connection: keep-alive") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
module Main {
|
||||||
|
|
||||||
|
import Config::{ServerConfig, DefaultConfig};
|
||||||
|
import Http::{HttpMethod};
|
||||||
|
import Router::{Handler, Route, Router};
|
||||||
|
import Server::{RunServer};
|
||||||
|
import Std::Array::{Array, Array_New, Array_Push};
|
||||||
|
import Std::Os::{Os_GetEnv};
|
||||||
|
import Std::String::{String_Len, String_ToInt, String_Eq};
|
||||||
|
|
||||||
|
func BuildRouter() -> Router {
|
||||||
|
var routes: Array<Route> = Array_New<Route>(8);
|
||||||
|
|
||||||
|
Array_Push<Route>(&routes, Route {
|
||||||
|
method: HttpMethod { tag: HttpMethod_GET },
|
||||||
|
path: "/api/health",
|
||||||
|
handler: Handler { tag: Handler_ApiHealth },
|
||||||
|
});
|
||||||
|
|
||||||
|
Array_Push<Route>(&routes, Route {
|
||||||
|
method: HttpMethod { tag: HttpMethod_GET },
|
||||||
|
path: "/api/info",
|
||||||
|
handler: Handler { tag: Handler_ApiInfo },
|
||||||
|
});
|
||||||
|
|
||||||
|
Array_Push<Route>(&routes, Route {
|
||||||
|
method: HttpMethod { tag: HttpMethod_GET },
|
||||||
|
path: "/ws",
|
||||||
|
handler: Handler { tag: Handler_WsUpgrade },
|
||||||
|
});
|
||||||
|
|
||||||
|
Array_Push<Route>(&routes, Route {
|
||||||
|
method: HttpMethod { tag: HttpMethod_GET },
|
||||||
|
path: "/",
|
||||||
|
handler: Handler { tag: Handler_StaticFile },
|
||||||
|
});
|
||||||
|
|
||||||
|
return Router {
|
||||||
|
routes: routes,
|
||||||
|
notFound: Handler { tag: Handler_NotFound },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply optional env overrides for benches / ops:
|
||||||
|
/// NEXUS_PORT, NEXUS_WORKERS, NEXUS_BIND, NEXUS_PUBLIC,
|
||||||
|
/// NEXUS_MAX_BODY (bytes), NEXUS_ACCESS_LOG (0/1/false/true),
|
||||||
|
/// NEXUS_TLS=1 + NEXUS_TLS_CERT + NEXUS_TLS_KEY (PEM paths)
|
||||||
|
/// NEXUS_TLS_CLIENT_CA (optional PEM → mTLS)
|
||||||
|
func ApplyEnvConfig(config: *ServerConfig) {
|
||||||
|
let portEnv: String = Os_GetEnv("NEXUS_PORT");
|
||||||
|
if String_Len(portEnv) > 0 {
|
||||||
|
let p: int64 = String_ToInt(portEnv);
|
||||||
|
if p > 0 && p < 65536 {
|
||||||
|
config.port = p as int;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let workersEnv: String = Os_GetEnv("NEXUS_WORKERS");
|
||||||
|
if String_Len(workersEnv) > 0 {
|
||||||
|
let w: int64 = String_ToInt(workersEnv);
|
||||||
|
if w > 0 && w <= 256 {
|
||||||
|
config.workerCount = w as int;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let bindEnv: String = Os_GetEnv("NEXUS_BIND");
|
||||||
|
if String_Len(bindEnv) > 0 {
|
||||||
|
config.bindAddr = bindEnv;
|
||||||
|
}
|
||||||
|
let pubEnv: String = Os_GetEnv("NEXUS_PUBLIC");
|
||||||
|
if String_Len(pubEnv) > 0 {
|
||||||
|
config.publicDir = pubEnv;
|
||||||
|
}
|
||||||
|
let bodyEnv: String = Os_GetEnv("NEXUS_MAX_BODY");
|
||||||
|
if String_Len(bodyEnv) > 0 {
|
||||||
|
let b: int64 = String_ToInt(bodyEnv);
|
||||||
|
if b >= 1024 && b <= 67108864 {
|
||||||
|
config.maxBodyBytes = b as int;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let logEnv: String = Os_GetEnv("NEXUS_ACCESS_LOG");
|
||||||
|
if String_Len(logEnv) > 0 {
|
||||||
|
if String_Eq(logEnv, "0") || String_Eq(logEnv, "false") || String_Eq(logEnv, "off") {
|
||||||
|
config.accessLog = false;
|
||||||
|
} else {
|
||||||
|
config.accessLog = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let tlsEnv: String = Os_GetEnv("NEXUS_TLS");
|
||||||
|
if String_Len(tlsEnv) > 0 {
|
||||||
|
if String_Eq(tlsEnv, "1") || String_Eq(tlsEnv, "true") || String_Eq(tlsEnv, "on") ||
|
||||||
|
String_Eq(tlsEnv, "https") {
|
||||||
|
config.tlsEnabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let certEnv: String = Os_GetEnv("NEXUS_TLS_CERT");
|
||||||
|
if String_Len(certEnv) > 0 {
|
||||||
|
config.tlsCertPath = certEnv;
|
||||||
|
config.tlsEnabled = true;
|
||||||
|
}
|
||||||
|
let keyEnv: String = Os_GetEnv("NEXUS_TLS_KEY");
|
||||||
|
if String_Len(keyEnv) > 0 {
|
||||||
|
config.tlsKeyPath = keyEnv;
|
||||||
|
config.tlsEnabled = true;
|
||||||
|
}
|
||||||
|
let caEnv: String = Os_GetEnv("NEXUS_TLS_CLIENT_CA");
|
||||||
|
if String_Len(caEnv) > 0 {
|
||||||
|
config.tlsClientCaPath = caEnv;
|
||||||
|
config.tlsEnabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
var config: ServerConfig = DefaultConfig();
|
||||||
|
ApplyEnvConfig(&config);
|
||||||
|
let router: Router = BuildRouter();
|
||||||
|
|
||||||
|
return RunServer(config, router);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
module Parser {
|
||||||
|
|
||||||
|
import Std::String::{String_Len, String_Eq, String_Trim};
|
||||||
|
import Std::Array::{Array, Array_New, Array_Push};
|
||||||
|
import Http::{HttpMethod, HttpRequest, HeaderEntry};
|
||||||
|
import Errors::{HttpError, ParseResult, ParseResult_NewOk, ParseResult_NewErr};
|
||||||
|
|
||||||
|
extern func bux_strlen(s: String) -> uint;
|
||||||
|
extern func bux_strstr(haystack: String, needle: String) -> String;
|
||||||
|
extern func bux_str_slice(s: String, start: uint, len: uint) -> String;
|
||||||
|
extern func bux_str_offset(pos: String, base: String) -> uint;
|
||||||
|
|
||||||
|
pub func ParseMethod(s: String) -> HttpMethod {
|
||||||
|
if String_Eq(s, "GET") { return HttpMethod { tag: HttpMethod_GET }; }
|
||||||
|
if String_Eq(s, "POST") { return HttpMethod { tag: HttpMethod_POST }; }
|
||||||
|
if String_Eq(s, "PUT") { return HttpMethod { tag: HttpMethod_PUT }; }
|
||||||
|
if String_Eq(s, "DELETE") { return HttpMethod { tag: HttpMethod_DELETE }; }
|
||||||
|
if String_Eq(s, "PATCH") { return HttpMethod { tag: HttpMethod_PATCH }; }
|
||||||
|
if String_Eq(s, "HEAD") { return HttpMethod { tag: HttpMethod_HEAD }; }
|
||||||
|
if String_Eq(s, "OPTIONS") { return HttpMethod { tag: HttpMethod_OPTIONS }; }
|
||||||
|
return HttpMethod { tag: HttpMethod_UNKNOWN };
|
||||||
|
}
|
||||||
|
|
||||||
|
func Slice(raw: String, start: int, len: int) -> String {
|
||||||
|
if start < 0 || len <= 0 { return ""; }
|
||||||
|
return bux_str_slice(raw, start as uint, len as uint);
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindCrlf(raw: String, start: int) -> int {
|
||||||
|
let rawLen: uint = bux_strlen(raw);
|
||||||
|
if start as uint >= rawLen { return -1; }
|
||||||
|
let tail: String = bux_str_slice(raw, start as uint, rawLen - start as uint);
|
||||||
|
let hit: String = bux_strstr(tail, "\r\n");
|
||||||
|
if String_Len(hit) == 0 { return -1; }
|
||||||
|
let offset: uint = bux_str_offset(hit, tail);
|
||||||
|
return start + offset as int;
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseHeaders(raw: String, start: int, end: int) -> Array<HeaderEntry> {
|
||||||
|
var headers: Array<HeaderEntry> = Array_New<HeaderEntry>(16);
|
||||||
|
var pos: int = start;
|
||||||
|
while pos < end {
|
||||||
|
let lineEnd: int = FindCrlf(raw, pos);
|
||||||
|
if lineEnd < 0 || lineEnd >= end { break; }
|
||||||
|
let lineLen: int = lineEnd - pos;
|
||||||
|
if lineLen > 0 {
|
||||||
|
let line: String = Slice(raw, pos, lineLen);
|
||||||
|
let colon: String = bux_strstr(line, ":");
|
||||||
|
if String_Len(colon) > 0 {
|
||||||
|
let keyLen: uint = bux_str_offset(colon, line);
|
||||||
|
let key: String = String_Trim(Slice(line, 0, keyLen as int));
|
||||||
|
let valStart: int = keyLen as int + 1;
|
||||||
|
let val: String = String_Trim(Slice(line, valStart, lineLen - valStart));
|
||||||
|
let entry: HeaderEntry = HeaderEntry { key: key, value: val };
|
||||||
|
Array_Push<HeaderEntry>(&headers, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pos = lineEnd + 2;
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func ParseRequest(raw: String) -> ParseResult {
|
||||||
|
let rawLen: uint = bux_strlen(raw);
|
||||||
|
if rawLen == 0 {
|
||||||
|
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find end of request line
|
||||||
|
let lineEnd: int = FindCrlf(raw, 0);
|
||||||
|
if lineEnd < 0 {
|
||||||
|
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split request line: METHOD PATH VERSION
|
||||||
|
var methodEnd: int = -1;
|
||||||
|
var pathStart: int = -1;
|
||||||
|
var pathEnd: int = -1;
|
||||||
|
var i: int = 0;
|
||||||
|
while i < lineEnd {
|
||||||
|
if raw[i] as int == 32 { // space
|
||||||
|
if methodEnd < 0 {
|
||||||
|
methodEnd = i;
|
||||||
|
pathStart = i + 1;
|
||||||
|
} else if pathEnd < 0 {
|
||||||
|
pathEnd = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if methodEnd < 0 || pathStart < 0 || pathEnd < 0 {
|
||||||
|
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
||||||
|
}
|
||||||
|
|
||||||
|
let methodStr: String = Slice(raw, 0, methodEnd);
|
||||||
|
let path: String = Slice(raw, pathStart, pathEnd - pathStart);
|
||||||
|
let version: String = Slice(raw, pathEnd + 1, lineEnd - pathEnd - 1);
|
||||||
|
|
||||||
|
// Find header/body boundary
|
||||||
|
let boundary: String = bux_strstr(raw, "\r\n\r\n");
|
||||||
|
// Single assignment path (no Array_New then overwrite). Field-move of
|
||||||
|
// `headers` into HttpRequest skips auto-Drop (compiler movedOutLocals).
|
||||||
|
var headers: Array<HeaderEntry>;
|
||||||
|
var body: String = "";
|
||||||
|
if String_Len(boundary) > 0 {
|
||||||
|
let headerEnd: uint = bux_str_offset(boundary, raw);
|
||||||
|
headers = ParseHeaders(raw, lineEnd + 2, headerEnd as int);
|
||||||
|
let bodyStart: uint = headerEnd + 4;
|
||||||
|
if bodyStart < rawLen {
|
||||||
|
body = bux_str_slice(raw, bodyStart, rawLen - bodyStart);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
headers = ParseHeaders(raw, lineEnd + 2, rawLen as int);
|
||||||
|
}
|
||||||
|
|
||||||
|
if String_Eq(path, "") {
|
||||||
|
// auto-drop of `headers` runs on error return
|
||||||
|
return ParseResult_NewErr(HttpError { tag: HttpError_BadRequest });
|
||||||
|
}
|
||||||
|
|
||||||
|
let req: HttpRequest = HttpRequest {
|
||||||
|
method: ParseMethod(methodStr),
|
||||||
|
path: path,
|
||||||
|
version: version,
|
||||||
|
body: body,
|
||||||
|
headers: headers,
|
||||||
|
};
|
||||||
|
return ParseResult_NewOk(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
module Router {
|
||||||
|
|
||||||
|
import Std::Array::{Array};
|
||||||
|
import Std::String::{String_Eq};
|
||||||
|
import Http::{HttpMethod, HttpRequest, HttpResponse, Http_NewResponse};
|
||||||
|
import Handlers::{ServeStaticFile, HandleApiHealth, HandleApiInfo, HandleWebSocketUpgrade, NotFoundResponse};
|
||||||
|
|
||||||
|
pub enum Handler {
|
||||||
|
StaticFile,
|
||||||
|
ApiHealth,
|
||||||
|
ApiInfo,
|
||||||
|
WsUpgrade,
|
||||||
|
NotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Route {
|
||||||
|
method: HttpMethod;
|
||||||
|
path: String;
|
||||||
|
handler: Handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Router {
|
||||||
|
routes: Array<Route>;
|
||||||
|
notFound: Handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func Handler_Handle(h: Handler, req: HttpRequest) -> HttpResponse {
|
||||||
|
match h {
|
||||||
|
Handler::StaticFile => ServeStaticFile(req),
|
||||||
|
Handler::ApiHealth => HandleApiHealth(),
|
||||||
|
Handler::ApiInfo => HandleApiInfo(),
|
||||||
|
Handler::WsUpgrade => HandleWebSocketUpgrade(req),
|
||||||
|
Handler::NotFound => NotFoundResponse(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func Router_Dispatch(r: Router, req: HttpRequest) -> HttpResponse {
|
||||||
|
for route in r.routes {
|
||||||
|
if route.method == req.method && String_Eq(route.path, req.path) {
|
||||||
|
return Handler_Handle(route.handler, req);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Handler_Handle(r.notFound, req);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
module Server {
|
||||||
|
|
||||||
|
import Std::Io::{Print, PrintLine, PrintInt};
|
||||||
|
import Std::Net::{
|
||||||
|
Net_Create, Net_SetReuse, Net_Bind, Net_Listen, Net_Accept, Net_Send, Net_Recv, Net_Close, Net_LastError,
|
||||||
|
Tls_ServerCtx, Tls_ServerCtxMtls, Tls_CtxFree, Tls_Accept, Tls_Send, Tls_Recv, Tls_Close, Tls_LastError
|
||||||
|
};
|
||||||
|
import Std::String::{String_Len, String_StartsWith, String_Eq};
|
||||||
|
import Std::Channel::{Channel, Channel_New, Channel_Send, Channel_Recv};
|
||||||
|
import Std::Array::{Array_Drop};
|
||||||
|
import Std::Os::{Os_InstallStopHandlers, Os_ShouldStop, Os_SetStopListenFd};
|
||||||
|
import Std::Time::{Time_NowMs};
|
||||||
|
import Config::{ServerConfig};
|
||||||
|
import Http::{HttpRequest, HttpResponse, Http_StatusText, Http_NewResponse, Http_MethodName, Request_WantsKeepAlive, HeaderEntry};
|
||||||
|
import Errors::{ParseResult};
|
||||||
|
import Parser::{ParseRequest};
|
||||||
|
import Router::{Router, Router_Dispatch};
|
||||||
|
|
||||||
|
extern func bux_strlen(s: String) -> uint;
|
||||||
|
extern func bux_sb_new(initial_cap: uint) -> *void;
|
||||||
|
extern func bux_sb_append(sb: *void, s: String);
|
||||||
|
extern func bux_sb_append_int(sb: *void, n: int64);
|
||||||
|
extern func bux_sb_build(sb: *void) -> String;
|
||||||
|
extern func bux_sb_free(sb: *void);
|
||||||
|
|
||||||
|
/// Cap requests per TCP connection (safety + fair scheduling).
|
||||||
|
const MAX_KEEPALIVE_REQUESTS: int = 1000;
|
||||||
|
|
||||||
|
/// fd < 0 is a poison pill: worker exits cleanly.
|
||||||
|
/// tls is null for plain HTTP; non-null SSL* for HTTPS (session 78).
|
||||||
|
pub struct ConnectionTask {
|
||||||
|
fd: int;
|
||||||
|
tls: *void;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func BuildResponse(resp: HttpResponse, keepAlive: bool) -> String {
|
||||||
|
let sb: *void = bux_sb_new(4096);
|
||||||
|
|
||||||
|
bux_sb_append(sb, "HTTP/1.1 ");
|
||||||
|
bux_sb_append_int(sb, resp.statusCode as int64);
|
||||||
|
bux_sb_append(sb, " ");
|
||||||
|
bux_sb_append(sb, Http_StatusText(resp.statusCode));
|
||||||
|
bux_sb_append(sb, "\r\n");
|
||||||
|
|
||||||
|
bux_sb_append(sb, "Server: Nexus/0.6.0 (Bux)\r\n");
|
||||||
|
|
||||||
|
if bux_strlen(resp.extraHeaders) > 0 {
|
||||||
|
bux_sb_append(sb, resp.extraHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
if bux_strlen(resp.contentType) > 0 {
|
||||||
|
bux_sb_append(sb, "Content-Type: ");
|
||||||
|
bux_sb_append(sb, resp.contentType);
|
||||||
|
bux_sb_append(sb, "\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
let bodyLen: uint = bux_strlen(resp.body);
|
||||||
|
bux_sb_append(sb, "Content-Length: ");
|
||||||
|
bux_sb_append_int(sb, bodyLen as int64);
|
||||||
|
bux_sb_append(sb, "\r\n");
|
||||||
|
|
||||||
|
if keepAlive {
|
||||||
|
bux_sb_append(sb, "Connection: keep-alive\r\n");
|
||||||
|
bux_sb_append(sb, "Keep-Alive: timeout=5, max=1000\r\n");
|
||||||
|
} else {
|
||||||
|
bux_sb_append(sb, "Connection: close\r\n");
|
||||||
|
}
|
||||||
|
bux_sb_append(sb, "\r\n");
|
||||||
|
|
||||||
|
if bodyLen > 0 {
|
||||||
|
bux_sb_append(sb, resp.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: String = bux_sb_build(sb);
|
||||||
|
bux_sb_free(sb);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConnRecv(fd: int, tls: *void, maxLen: int) -> String {
|
||||||
|
if tls != null as *void {
|
||||||
|
return Tls_Recv(tls, maxLen);
|
||||||
|
}
|
||||||
|
return Net_Recv(fd, maxLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConnSend(fd: int, tls: *void, data: String) -> int {
|
||||||
|
if tls != null as *void {
|
||||||
|
return Tls_Send(tls, data);
|
||||||
|
}
|
||||||
|
return Net_Send(fd, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Structured access log: method path status duration_ms
|
||||||
|
func AccessLog(method: String, path: String, status: int, ms: int64) {
|
||||||
|
Print(method);
|
||||||
|
Print(" ");
|
||||||
|
Print(path);
|
||||||
|
Print(" ");
|
||||||
|
PrintInt(status);
|
||||||
|
Print(" ");
|
||||||
|
PrintInt(ms as int);
|
||||||
|
PrintLine("ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serve one TCP (or TLS) client: zero or more HTTP/1.1 keep-alive requests.
|
||||||
|
pub func HandleConnection(fd: int, tls: *void, router: Router, config: ServerConfig) {
|
||||||
|
var reqCount: int = 0;
|
||||||
|
let maxRecv: int = config.maxBodyBytes;
|
||||||
|
while reqCount < MAX_KEEPALIVE_REQUESTS {
|
||||||
|
if Os_ShouldStop() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let raw: String = ConnRecv(fd, tls, maxRecv);
|
||||||
|
if String_Len(raw) == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if String_Len(raw) as int >= maxRecv {
|
||||||
|
let resp: HttpResponse = Http_NewResponse(413, "text/plain; charset=utf-8", "Payload Too Large");
|
||||||
|
ConnSend(fd, tls, BuildResponse(resp, false));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if String_StartsWith(raw, "PRI * HTTP/2.0") {
|
||||||
|
let resp: HttpResponse = Http_NewResponse(200, "text/plain; charset=utf-8",
|
||||||
|
"HTTP/2 detected — full support planned for future release.\r\n");
|
||||||
|
ConnSend(fd, tls, BuildResponse(resp, false));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let t0: int64 = Time_NowMs();
|
||||||
|
let parsed: ParseResult = ParseRequest(raw);
|
||||||
|
var keepAlive: bool = false;
|
||||||
|
var statusOut: int = 400;
|
||||||
|
var methodName: String = "?";
|
||||||
|
var pathOut: String = "-";
|
||||||
|
|
||||||
|
if parsed.tag == ParseResult_Ok {
|
||||||
|
var req: HttpRequest = parsed.data.Ok_0;
|
||||||
|
methodName = Http_MethodName(req.method);
|
||||||
|
pathOut = req.path;
|
||||||
|
if String_Len(req.body) as int > maxRecv {
|
||||||
|
let resp: HttpResponse = Http_NewResponse(413, "text/plain; charset=utf-8", "Payload Too Large");
|
||||||
|
ConnSend(fd, tls, BuildResponse(resp, false));
|
||||||
|
Array_Drop<HeaderEntry>(&req.headers);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
keepAlive = Request_WantsKeepAlive(&req);
|
||||||
|
if reqCount + 1 >= MAX_KEEPALIVE_REQUESTS {
|
||||||
|
keepAlive = false;
|
||||||
|
}
|
||||||
|
let resp: HttpResponse = Router_Dispatch(router, req);
|
||||||
|
statusOut = resp.statusCode;
|
||||||
|
ConnSend(fd, tls, BuildResponse(resp, keepAlive));
|
||||||
|
Array_Drop<HeaderEntry>(&req.headers);
|
||||||
|
} else {
|
||||||
|
let resp: HttpResponse = Http_NewResponse(400, "text/plain; charset=utf-8", "Bad Request");
|
||||||
|
statusOut = 400;
|
||||||
|
ConnSend(fd, tls, BuildResponse(resp, false));
|
||||||
|
if config.accessLog {
|
||||||
|
let dt: int64 = Time_NowMs() - t0;
|
||||||
|
AccessLog("?", "-", 400, dt);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.accessLog {
|
||||||
|
let dt: int64 = Time_NowMs() - t0;
|
||||||
|
AccessLog(methodName, pathOut, statusOut, dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
reqCount = reqCount + 1;
|
||||||
|
if !keepAlive {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WorkerCtx {
|
||||||
|
taskQueue: *Channel<ConnectionTask>;
|
||||||
|
router: Router;
|
||||||
|
config: ServerConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func Worker(ctx: *WorkerCtx) {
|
||||||
|
while true {
|
||||||
|
let task: ConnectionTask = Channel_Recv<ConnectionTask>(ctx.taskQueue);
|
||||||
|
if task.fd < 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
HandleConnection(task.fd, task.tls, ctx.router, ctx.config);
|
||||||
|
if task.tls != null as *void {
|
||||||
|
Tls_Close(task.tls);
|
||||||
|
}
|
||||||
|
Net_Close(task.fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AcceptorCtx {
|
||||||
|
serverFd: int;
|
||||||
|
taskQueue: *Channel<ConnectionTask>;
|
||||||
|
workerCount: int;
|
||||||
|
tlsCtx: *void;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func Acceptor(ctx: *AcceptorCtx) {
|
||||||
|
while !Os_ShouldStop() {
|
||||||
|
let fd: int = Net_Accept(ctx.serverFd);
|
||||||
|
if fd >= 0 {
|
||||||
|
var tls: *void = null as *void;
|
||||||
|
if ctx.tlsCtx != null as *void {
|
||||||
|
tls = Tls_Accept(ctx.tlsCtx, fd);
|
||||||
|
if tls == null as *void {
|
||||||
|
Print("WARN: TLS handshake failed: ");
|
||||||
|
PrintLine(Tls_LastError());
|
||||||
|
Net_Close(fd);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let task: ConnectionTask = ConnectionTask { fd: fd, tls: tls };
|
||||||
|
Channel_Send<ConnectionTask>(ctx.taskQueue, task);
|
||||||
|
} else {
|
||||||
|
if Os_ShouldStop() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var i: int = 0;
|
||||||
|
while i < ctx.workerCount {
|
||||||
|
let poison: ConnectionTask = ConnectionTask { fd: -1, tls: null as *void };
|
||||||
|
Channel_Send<ConnectionTask>(ctx.taskQueue, poison);
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
PrintLine("acceptor: stop — workers poisoned");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub func RunServer(config: ServerConfig, router: Router) -> int {
|
||||||
|
PrintLine("================================================");
|
||||||
|
PrintLine(" Nexus HTTP Server v0.6.0");
|
||||||
|
PrintLine(" HTTP/1.1 + TLS/mTLS + keep-alive + graceful stop");
|
||||||
|
PrintLine(" Built with Bux");
|
||||||
|
PrintLine("================================================");
|
||||||
|
PrintLine("");
|
||||||
|
|
||||||
|
Os_InstallStopHandlers();
|
||||||
|
|
||||||
|
var tlsCtx: *void = null as *void;
|
||||||
|
if config.tlsEnabled {
|
||||||
|
if String_Eq(config.tlsCertPath, "") || String_Eq(config.tlsKeyPath, "") {
|
||||||
|
PrintLine("FATAL: TLS enabled but NEXUS_TLS_CERT / NEXUS_TLS_KEY not set");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if !String_Eq(config.tlsClientCaPath, "") {
|
||||||
|
tlsCtx = Tls_ServerCtxMtls(config.tlsCertPath, config.tlsKeyPath, config.tlsClientCaPath);
|
||||||
|
PrintLine("mTLS: client certificates required");
|
||||||
|
Print("Client CA: ");
|
||||||
|
PrintLine(config.tlsClientCaPath);
|
||||||
|
} else {
|
||||||
|
tlsCtx = Tls_ServerCtx(config.tlsCertPath, config.tlsKeyPath);
|
||||||
|
}
|
||||||
|
if tlsCtx == null as *void {
|
||||||
|
Print("FATAL: TLS context failed: ");
|
||||||
|
PrintLine(Tls_LastError());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
Print("TLS cert: ");
|
||||||
|
PrintLine(config.tlsCertPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
let serverFd: int = Net_Create();
|
||||||
|
if serverFd < 0 {
|
||||||
|
PrintLine("FATAL: socket() failed");
|
||||||
|
if tlsCtx != null as *void { Tls_CtxFree(tlsCtx); }
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Net_SetReuse(serverFd) {
|
||||||
|
PrintLine("WARN: SO_REUSEADDR failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Net_Bind(serverFd, config.bindAddr, config.port) {
|
||||||
|
Print("FATAL: bind failed: ");
|
||||||
|
PrintLine(Net_LastError());
|
||||||
|
Net_Close(serverFd);
|
||||||
|
if tlsCtx != null as *void { Tls_CtxFree(tlsCtx); }
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Net_Listen(serverFd, config.backlog) {
|
||||||
|
PrintLine("FATAL: listen() failed");
|
||||||
|
Net_Close(serverFd);
|
||||||
|
if tlsCtx != null as *void { Tls_CtxFree(tlsCtx); }
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Os_SetStopListenFd(serverFd);
|
||||||
|
|
||||||
|
if config.tlsEnabled {
|
||||||
|
Print("Listening on https://");
|
||||||
|
} else {
|
||||||
|
Print("Listening on http://");
|
||||||
|
}
|
||||||
|
Print(config.bindAddr);
|
||||||
|
Print(":");
|
||||||
|
PrintInt(config.port);
|
||||||
|
PrintLine("");
|
||||||
|
PrintInt(config.workerCount);
|
||||||
|
Print(" worker threads | keep-alive: on | max-body: ");
|
||||||
|
PrintInt(config.maxBodyBytes);
|
||||||
|
PrintLine(" B");
|
||||||
|
PrintLine("Endpoints: / /api/health /api/info /ws");
|
||||||
|
PrintLine("Stop: Ctrl+C / SIGTERM (graceful).");
|
||||||
|
PrintLine("");
|
||||||
|
|
||||||
|
let taskQueue: Channel<ConnectionTask> = Channel_New<ConnectionTask>(config.backlog as int64);
|
||||||
|
let workerCtx: WorkerCtx = WorkerCtx {
|
||||||
|
taskQueue: &taskQueue,
|
||||||
|
router: router,
|
||||||
|
config: config
|
||||||
|
};
|
||||||
|
let acceptorCtx: AcceptorCtx = AcceptorCtx {
|
||||||
|
serverFd: serverFd,
|
||||||
|
taskQueue: &taskQueue,
|
||||||
|
workerCount: config.workerCount,
|
||||||
|
tlsCtx: tlsCtx
|
||||||
|
};
|
||||||
|
|
||||||
|
var i: int = 0;
|
||||||
|
while i < config.workerCount {
|
||||||
|
spawn Worker(&workerCtx);
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Acceptor(&acceptorCtx);
|
||||||
|
|
||||||
|
Os_SetStopListenFd(-1);
|
||||||
|
if serverFd >= 0 {
|
||||||
|
Net_Close(serverFd);
|
||||||
|
}
|
||||||
|
if tlsCtx != null as *void {
|
||||||
|
Tls_CtxFree(tlsCtx);
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintLine("nexus: acceptor stopped — exit");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
bux 0.1.0 (bootstrap)
|
||||||
|
EXIT:0
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# SimpleDB
|
||||||
|
|
||||||
|
File-backed key-value database for Bux.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
simpledb <dbfile> set <key> <value>
|
||||||
|
simpledb <dbfile> get <key>
|
||||||
|
simpledb <dbfile> del <key>
|
||||||
|
simpledb <dbfile> has <key>
|
||||||
|
simpledb <dbfile> keys
|
||||||
|
simpledb <dbfile> count
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ simpledb data.db set name BuxLang
|
||||||
|
OK name = BuxLang
|
||||||
|
|
||||||
|
$ simpledb data.db set version 0.1.0
|
||||||
|
OK version = 0.1.0
|
||||||
|
|
||||||
|
$ simpledb data.db get name
|
||||||
|
BuxLang
|
||||||
|
|
||||||
|
$ simpledb data.db has name
|
||||||
|
true name
|
||||||
|
|
||||||
|
$ simpledb data.db keys
|
||||||
|
keys: 2
|
||||||
|
name
|
||||||
|
version
|
||||||
|
|
||||||
|
$ simpledb data.db count
|
||||||
|
count: 2
|
||||||
|
|
||||||
|
$ simpledb data.db del version
|
||||||
|
DEL version
|
||||||
|
|
||||||
|
$ cat data.db
|
||||||
|
name=BuxLang
|
||||||
|
```
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
Data is stored as plain text, one `key=value` per line. The file is created automatically on first write.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd apps/simpledb
|
||||||
|
../../buxc build
|
||||||
|
./build/simpledb data.db set hello world
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
The `Database` struct and its functions are defined in `src/Main.bux` and can be imported into other Bux projects:
|
||||||
|
|
||||||
|
```bux
|
||||||
|
import Simpledb::{Database, DB_New, DB_Load, DB_Save, DB_Get, DB_Set, DB_Del, DB_Has, DB_Count, DB_Keys};
|
||||||
|
```
|
||||||
|
|
||||||
|
| Function | Signature |
|
||||||
|
|----------|-----------|
|
||||||
|
| `DB_New` | `(path: String) -> Database` |
|
||||||
|
| `DB_Load` | `(db: *Database) -> bool` |
|
||||||
|
| `DB_Save` | `(db: *Database) -> bool` |
|
||||||
|
| `DB_Get` | `(db: *Database, key: String) -> String` |
|
||||||
|
| `DB_Set` | `(db: *Database, key: String, value: String)` |
|
||||||
|
| `DB_Del` | `(db: *Database, key: String) -> bool` |
|
||||||
|
| `DB_Has` | `(db: *Database, key: String) -> bool` |
|
||||||
|
| `DB_Count` | `(db: *Database) -> uint` |
|
||||||
|
| `DB_Keys` | `(db: *Database) -> *String` |
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[Package]
|
||||||
|
Name = "simpledb"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
Description = "SimpleDB — file-backed JSON key-value database for Bux"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
module Main {
|
||||||
|
|
||||||
|
import Std::Io::{PrintLine, Print, PrintInt, ReadFile, WriteFile, FileExists};
|
||||||
|
import Std::Os::{Os_ArgsCount, Os_Args};
|
||||||
|
import Std::String::{String_Len, String_Eq, String_StartsWith, String_Find, String_Slice, String_Offset};
|
||||||
|
|
||||||
|
extern func bux_strlen(s: String) -> uint;
|
||||||
|
extern func bux_strstr(haystack: String, needle: String) -> String;
|
||||||
|
extern func bux_str_slice(s: String, start: uint, len: uint) -> String;
|
||||||
|
extern func bux_str_split_count(s: String, delim: String) -> uint;
|
||||||
|
extern func bux_str_split_part(s: String, delim: String, index: uint) -> String;
|
||||||
|
extern func bux_sb_new(initial_cap: uint) -> *void;
|
||||||
|
extern func bux_sb_append(sb: *void, s: String);
|
||||||
|
extern func bux_sb_append_char(sb: *void, c: char8);
|
||||||
|
|
||||||
|
func SB_AppendChar(sb: *void, c: int) {
|
||||||
|
bux_sb_append_char(sb, c as char8);
|
||||||
|
}
|
||||||
|
extern func bux_sb_build(sb: *void) -> String;
|
||||||
|
extern func bux_sb_free(sb: *void);
|
||||||
|
extern func bux_alloc(size: uint) -> *void;
|
||||||
|
extern func bux_free(ptr: *void);
|
||||||
|
|
||||||
|
struct Database {
|
||||||
|
path: String,
|
||||||
|
sb: *void,
|
||||||
|
loaded: bool,
|
||||||
|
count: uint,
|
||||||
|
}
|
||||||
|
|
||||||
|
func DB_New(filePath: String) -> Database {
|
||||||
|
var db: Database;
|
||||||
|
db.path = filePath;
|
||||||
|
db.sb = bux_sb_new(4096);
|
||||||
|
db.loaded = false;
|
||||||
|
db.count = 0;
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
func DB_Load(self: *Database) -> bool {
|
||||||
|
if self.loaded { return true; }
|
||||||
|
|
||||||
|
if !FileExists(self.path) {
|
||||||
|
self.loaded = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw: String = ReadFile(self.path);
|
||||||
|
if String_Len(raw) == 0 || bux_strlen(raw) == 0 {
|
||||||
|
self.loaded = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bux_sb_append(self.sb, raw);
|
||||||
|
|
||||||
|
let lineCount: uint = bux_str_split_count(raw, "\n");
|
||||||
|
var i: uint = 0;
|
||||||
|
var n: uint = 0;
|
||||||
|
while i < lineCount {
|
||||||
|
let line: String = bux_str_split_part(raw, "\n", i);
|
||||||
|
if bux_strlen(line) > 0 {
|
||||||
|
n = n + 1;
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
self.count = n;
|
||||||
|
self.loaded = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
func DB_Get(self: *Database, key: String) -> String {
|
||||||
|
let raw: String = bux_sb_build(self.sb);
|
||||||
|
let lineCount: uint = bux_str_split_count(raw, "\n");
|
||||||
|
var i: uint = 0;
|
||||||
|
while i < lineCount {
|
||||||
|
let line: String = bux_str_split_part(raw, "\n", i);
|
||||||
|
if String_StartsWith(line, key) {
|
||||||
|
let eqPos: String = String_Find(line, "=");
|
||||||
|
if String_Len(eqPos) > 0 {
|
||||||
|
let keyLen: uint = bux_strlen(key);
|
||||||
|
let valStart: uint = keyLen + 1;
|
||||||
|
let lineLen: uint = bux_strlen(line);
|
||||||
|
if valStart < lineLen {
|
||||||
|
return bux_str_slice(line, valStart, lineLen - valStart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
func DB_Set(self: *Database, key: String, value: String) {
|
||||||
|
let raw: String = bux_sb_build(self.sb);
|
||||||
|
let lineCount: uint = bux_str_split_count(raw, "\n");
|
||||||
|
|
||||||
|
var newSb: *void = bux_sb_new(4096);
|
||||||
|
var found: bool = false;
|
||||||
|
var i: uint = 0;
|
||||||
|
while i < lineCount {
|
||||||
|
let line: String = bux_str_split_part(raw, "\n", i);
|
||||||
|
if bux_strlen(line) > 0 {
|
||||||
|
if String_StartsWith(line, key) {
|
||||||
|
let eqPos: String = String_Find(line, "=");
|
||||||
|
if String_Len(eqPos) > 0 {
|
||||||
|
bux_sb_append(newSb, key);
|
||||||
|
SB_AppendChar(newSb, 61);
|
||||||
|
bux_sb_append(newSb, value);
|
||||||
|
SB_AppendChar(newSb, 10);
|
||||||
|
found = true;
|
||||||
|
} else {
|
||||||
|
bux_sb_append(newSb, line);
|
||||||
|
SB_AppendChar(newSb, 10);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bux_sb_append(newSb, line);
|
||||||
|
SB_AppendChar(newSb, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
bux_sb_append(newSb, key);
|
||||||
|
SB_AppendChar(newSb, 61);
|
||||||
|
bux_sb_append(newSb, value);
|
||||||
|
SB_AppendChar(newSb, 10);
|
||||||
|
self.count = self.count + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bux_sb_free(self.sb);
|
||||||
|
self.sb = newSb;
|
||||||
|
}
|
||||||
|
|
||||||
|
func DB_Del(self: *Database, key: String) {
|
||||||
|
let raw: String = bux_sb_build(self.sb);
|
||||||
|
let lineCount: uint = bux_str_split_count(raw, "\n");
|
||||||
|
|
||||||
|
var newSb: *void = bux_sb_new(4096);
|
||||||
|
var i: uint = 0;
|
||||||
|
var delCount: uint = 0;
|
||||||
|
while i < lineCount {
|
||||||
|
let line: String = bux_str_split_part(raw, "\n", i);
|
||||||
|
if bux_strlen(line) > 0 {
|
||||||
|
if String_StartsWith(line, key) {
|
||||||
|
let eqPos: String = String_Find(line, "=");
|
||||||
|
if String_Len(eqPos) > 0 {
|
||||||
|
delCount = delCount + 1;
|
||||||
|
} else {
|
||||||
|
bux_sb_append(newSb, line);
|
||||||
|
SB_AppendChar(newSb, 10);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bux_sb_append(newSb, line);
|
||||||
|
SB_AppendChar(newSb, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if delCount > 0 {
|
||||||
|
self.count = self.count - delCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
bux_sb_free(self.sb);
|
||||||
|
self.sb = newSb;
|
||||||
|
}
|
||||||
|
|
||||||
|
func DB_Has(self: *Database, key: String) -> bool {
|
||||||
|
let val: String = DB_Get(self, key);
|
||||||
|
return bux_strlen(val) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
func DB_Count(self: *Database) -> uint {
|
||||||
|
return self.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
func DB_Keys(self: *Database) -> *String {
|
||||||
|
if self.count == 0 { return null as *String; }
|
||||||
|
|
||||||
|
let raw: String = bux_sb_build(self.sb);
|
||||||
|
let lineCount: uint = bux_str_split_count(raw, "\n");
|
||||||
|
let keys: *String = bux_alloc(self.count * sizeof(String)) as *String;
|
||||||
|
|
||||||
|
var found: uint = 0;
|
||||||
|
var i: uint = 0;
|
||||||
|
while i < lineCount && found < self.count {
|
||||||
|
let line: String = bux_str_split_part(raw, "\n", i);
|
||||||
|
if bux_strlen(line) > 0 {
|
||||||
|
let eqPos: String = String_Find(line, "=");
|
||||||
|
if String_Len(eqPos) > 0 {
|
||||||
|
let keyLen: uint = String_Offset(eqPos, line);
|
||||||
|
keys[found] = bux_str_slice(line, 0, keyLen);
|
||||||
|
found = found + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
func DB_Save(self: *Database) -> bool {
|
||||||
|
let content: String = bux_sb_build(self.sb);
|
||||||
|
let ok: bool = WriteFile(self.path, content);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrintUsage() {
|
||||||
|
PrintLine("SimpleDB — file-backed key-value database");
|
||||||
|
PrintLine("Usage:");
|
||||||
|
PrintLine(" simpledb <dbfile> set <key> <value>");
|
||||||
|
PrintLine(" simpledb <dbfile> get <key>");
|
||||||
|
PrintLine(" simpledb <dbfile> del <key>");
|
||||||
|
PrintLine(" simpledb <dbfile> has <key>");
|
||||||
|
PrintLine(" simpledb <dbfile> keys");
|
||||||
|
PrintLine(" simpledb <dbfile> count");
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
let argc: int = Os_ArgsCount();
|
||||||
|
|
||||||
|
if argc < 3 {
|
||||||
|
PrintUsage();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dbFile: String = Os_Args(1);
|
||||||
|
let cmd: String = Os_Args(2);
|
||||||
|
|
||||||
|
var db: Database = DB_New(dbFile);
|
||||||
|
|
||||||
|
if String_Eq(cmd, "get") {
|
||||||
|
DB_Load(&db);
|
||||||
|
if argc < 4 {
|
||||||
|
PrintLine("get: missing key");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
let key: String = Os_Args(3);
|
||||||
|
let val: String = DB_Get(&db, key);
|
||||||
|
if bux_strlen(val) > 0 {
|
||||||
|
PrintLine(val);
|
||||||
|
} else {
|
||||||
|
Print("(not found) ");
|
||||||
|
PrintLine(key);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if String_Eq(cmd, "set") {
|
||||||
|
DB_Load(&db);
|
||||||
|
if argc < 5 {
|
||||||
|
PrintLine("set: missing key or value");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
let key: String = Os_Args(3);
|
||||||
|
let val: String = Os_Args(4);
|
||||||
|
DB_Set(&db, key, val);
|
||||||
|
let saved: bool = DB_Save(&db);
|
||||||
|
if saved {
|
||||||
|
Print("OK ");
|
||||||
|
Print(key);
|
||||||
|
Print(" = ");
|
||||||
|
PrintLine(val);
|
||||||
|
} else {
|
||||||
|
PrintLine("set: save failed");
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if String_Eq(cmd, "del") {
|
||||||
|
DB_Load(&db);
|
||||||
|
if argc < 4 {
|
||||||
|
PrintLine("del: missing key");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
let key: String = Os_Args(3);
|
||||||
|
DB_Del(&db, key);
|
||||||
|
DB_Save(&db);
|
||||||
|
Print("DEL ");
|
||||||
|
PrintLine(key);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if String_Eq(cmd, "has") {
|
||||||
|
DB_Load(&db);
|
||||||
|
if argc < 4 {
|
||||||
|
PrintLine("has: missing key");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
let key: String = Os_Args(3);
|
||||||
|
let found: bool = DB_Has(&db, key);
|
||||||
|
if found {
|
||||||
|
Print("true ");
|
||||||
|
PrintLine(key);
|
||||||
|
} else {
|
||||||
|
Print("false ");
|
||||||
|
PrintLine(key);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if String_Eq(cmd, "keys") {
|
||||||
|
DB_Load(&db);
|
||||||
|
let n: uint = DB_Count(&db);
|
||||||
|
Print("keys: ");
|
||||||
|
PrintInt(n as int);
|
||||||
|
PrintLine("");
|
||||||
|
|
||||||
|
if n == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let list: *String = DB_Keys(&db);
|
||||||
|
var i: uint = 0;
|
||||||
|
while i < n {
|
||||||
|
Print(" ");
|
||||||
|
PrintLine(list[i]);
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
bux_free(list as *void);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if String_Eq(cmd, "count") {
|
||||||
|
DB_Load(&db);
|
||||||
|
let n: uint = DB_Count(&db);
|
||||||
|
Print("count: ");
|
||||||
|
PrintInt(n as int);
|
||||||
|
PrintLine("");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Print("unknown command: ");
|
||||||
|
PrintLine(cmd);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Bux benchmarks (E.5)
|
||||||
|
|
||||||
|
Micro-kernels + optional Nexus HTTP throughput for regression and language comparison.
|
||||||
|
|
||||||
|
## Quick run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From repo root
|
||||||
|
make bench # Bux micro + C + Nim (+ Zig if installed)
|
||||||
|
make bench-nexus # wrk vs apps/nexus /api/health
|
||||||
|
BENCH_NEXUS=1 make bench
|
||||||
|
```
|
||||||
|
|
||||||
|
## Micro suites
|
||||||
|
|
||||||
|
| Suite | Kernels | Build |
|
||||||
|
|-------|---------|-------|
|
||||||
|
| `micro/` | Bux: `int_loop`, `fib30`, `string_concat`, `array_push` | `buxc build` |
|
||||||
|
| `c/` | C: `fib30`, `int_loop` | `gcc -O2` |
|
||||||
|
| `nim/` | Nim twins | `nim c -d:release --opt:speed` |
|
||||||
|
| `zig/` | Zig twins (optional) | `zig build-exe -OReleaseFast` |
|
||||||
|
|
||||||
|
Output lines:
|
||||||
|
|
||||||
|
```
|
||||||
|
BENCH <name> iters=N total_us=T us_per_op=P
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nexus throughput
|
||||||
|
|
||||||
|
`tools/bench_nexus.sh` / `make bench-nexus`:
|
||||||
|
|
||||||
|
1. Builds `apps/nexus`
|
||||||
|
2. Starts on `NEXUS_PORT` (default **18080**), bind `127.0.0.1`
|
||||||
|
3. `wrk -t4 -c64 -d5s` against `/api/health`
|
||||||
|
4. Prints `BENCH nexus_health rps=…`
|
||||||
|
|
||||||
|
Env knobs:
|
||||||
|
|
||||||
|
| Variable | Default | Meaning |
|
||||||
|
|----------|---------|---------|
|
||||||
|
| `NEXUS_PORT` | `18080` | Listen port (also used by the server binary) |
|
||||||
|
| `NEXUS_WORKERS` | `4` | Worker threads |
|
||||||
|
| `NEXUS_BENCH_DURATION` | `5s` | wrk `-d` |
|
||||||
|
| `NEXUS_BENCH_C` | `64` | wrk connections |
|
||||||
|
| `NEXUS_BENCH_T` | `4` | wrk threads |
|
||||||
|
|
||||||
|
Server-side (any nexus run):
|
||||||
|
|
||||||
|
- `NEXUS_PORT`, `NEXUS_WORKERS`, `NEXUS_BIND`, `NEXUS_PUBLIC`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Numbers vary by machine; use them relatively (same host, same day).
|
||||||
|
- Nexus **v0.3** uses HTTP/1.1 keep-alive (default). Sample RPS on one machine:
|
||||||
|
- close-only era: ~45k req/s
|
||||||
|
- keep-alive: ~80k+ req/s (`wrk -t4 -c64 -d5s /api/health`)
|
||||||
|
- Requires `wrk` for `bench-nexus` (`apt install wrk` on Debian/Ubuntu).
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/* C reference: recursive fib(30) — compare with benches/micro fib30 */
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
static int fib(int n) {
|
||||||
|
if (n <= 1) return n;
|
||||||
|
return fib(n - 1) + fib(n - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int64_t now_us(void) {
|
||||||
|
struct timespec ts;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
return (int64_t)ts.tv_sec * 1000000LL + (int64_t)ts.tv_nsec / 1000LL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
(void)fib(20); /* warmup */
|
||||||
|
int64_t t0 = now_us();
|
||||||
|
int r = fib(30);
|
||||||
|
int64_t t1 = now_us();
|
||||||
|
if (r < 0) return 1;
|
||||||
|
printf("BENCH fib30 iters=1 total_us=%lld us_per_op=%lld\n",
|
||||||
|
(long long)(t1 - t0), (long long)(t1 - t0));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/* C reference: 1e6 integer arithmetic loop */
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
static int64_t now_us(void) {
|
||||||
|
struct timespec ts;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
return (int64_t)ts.tv_sec * 1000000LL + (int64_t)ts.tv_nsec / 1000LL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
const int iters = 1000000;
|
||||||
|
int acc = 0;
|
||||||
|
int64_t t0 = now_us();
|
||||||
|
for (int i = 0; i < iters; i++) {
|
||||||
|
acc = acc + i * 3 - 1;
|
||||||
|
}
|
||||||
|
int64_t t1 = now_us();
|
||||||
|
if (acc == 0 && iters > 0) return 1;
|
||||||
|
printf("BENCH int_loop iters=%d total_us=%lld us_per_op=%lld\n",
|
||||||
|
iters, (long long)(t1 - t0), (long long)((t1 - t0) / iters));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[Package]
|
||||||
|
Name = "micro"
|
||||||
|
Version = "0.1.0"
|
||||||
|
Type = "bin"
|
||||||
|
Description = "Micro-benchmarks for Bux (E.5)"
|
||||||
|
|
||||||
|
[Build]
|
||||||
|
Output = "Bin"
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
// Micro-benchmarks (E.5) — recursive fib, string concat, array push.
|
||||||
|
// Prints lines: BENCH name iters total_us us_per_op
|
||||||
|
import Std::Io::{PrintLine, Print};
|
||||||
|
import Std::Time::{Time_NowUs};
|
||||||
|
import Std::String::{String_FromInt, String_Concat, String_Len};
|
||||||
|
import Std::Array::{Array, Array_New, Array_Push, Array_Len, Array_Get};
|
||||||
|
|
||||||
|
func Fib(n: int) -> int {
|
||||||
|
if n <= 1 {
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
return Fib(n - 1) + Fib(n - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Report(name: String, iters: int, usTotal: int64) {
|
||||||
|
var perOpUs: int64 = 0;
|
||||||
|
if iters > 0 {
|
||||||
|
perOpUs = usTotal / (iters as int64);
|
||||||
|
}
|
||||||
|
Print("BENCH ");
|
||||||
|
Print(name);
|
||||||
|
Print(" iters=");
|
||||||
|
Print(String_FromInt(iters as int64));
|
||||||
|
Print(" total_us=");
|
||||||
|
Print(String_FromInt(usTotal));
|
||||||
|
Print(" us_per_op=");
|
||||||
|
PrintLine(String_FromInt(perOpUs));
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchIntLoop() {
|
||||||
|
let iters: int = 1000000;
|
||||||
|
var acc: int = 0;
|
||||||
|
var i: int = 0;
|
||||||
|
let t0: int64 = Time_NowUs();
|
||||||
|
while i < iters {
|
||||||
|
acc = acc + i * 3 - 1;
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
let t1: int64 = Time_NowUs();
|
||||||
|
if acc == 0 && iters > 0 {
|
||||||
|
PrintLine("unreachable");
|
||||||
|
}
|
||||||
|
Report("int_loop", iters, t1 - t0);
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchFib() {
|
||||||
|
discard Fib(20);
|
||||||
|
let n: int = 30;
|
||||||
|
let t0: int64 = Time_NowUs();
|
||||||
|
let r: int = Fib(n);
|
||||||
|
let t1: int64 = Time_NowUs();
|
||||||
|
if r < 0 {
|
||||||
|
PrintLine("unreachable");
|
||||||
|
}
|
||||||
|
Report("fib30", 1, t1 - t0);
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchStringConcat() {
|
||||||
|
let iters: int = 50000;
|
||||||
|
var i: int = 0;
|
||||||
|
let t0: int64 = Time_NowUs();
|
||||||
|
var s: String = "";
|
||||||
|
while i < iters {
|
||||||
|
s = String_Concat(s, "x");
|
||||||
|
if String_Len(s) > 256 {
|
||||||
|
s = "x";
|
||||||
|
}
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
let t1: int64 = Time_NowUs();
|
||||||
|
if String_Len(s) < 0 {
|
||||||
|
PrintLine("unreachable");
|
||||||
|
}
|
||||||
|
Report("string_concat", iters, t1 - t0);
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchArrayPush() {
|
||||||
|
let iters: int = 100000;
|
||||||
|
var arr: Array<int> = Array_New<int>(16);
|
||||||
|
var i: int = 0;
|
||||||
|
let t0: int64 = Time_NowUs();
|
||||||
|
while i < iters {
|
||||||
|
Array_Push<int>(&arr, i);
|
||||||
|
i = i + 1;
|
||||||
|
}
|
||||||
|
let t1: int64 = Time_NowUs();
|
||||||
|
let n: uint = Array_Len<int>(&arr);
|
||||||
|
if n as int != iters {
|
||||||
|
Print("array len mismatch: ");
|
||||||
|
PrintLine(String_FromInt(n as int64));
|
||||||
|
}
|
||||||
|
discard Array_Get<int>(&arr, (iters - 1) as uint);
|
||||||
|
Report("array_push", iters, t1 - t0);
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() -> int {
|
||||||
|
PrintLine("=== Bux micro-benchmarks (E.5) ===");
|
||||||
|
BenchIntLoop();
|
||||||
|
BenchFib();
|
||||||
|
BenchStringConcat();
|
||||||
|
BenchArrayPush();
|
||||||
|
PrintLine("=== done ===");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
## Nim twin of benches/c/fib.c — recursive fib(30)
|
||||||
|
import std/[times, strformat]
|
||||||
|
|
||||||
|
proc fib(n: int): int =
|
||||||
|
if n <= 1: return n
|
||||||
|
fib(n - 1) + fib(n - 2)
|
||||||
|
|
||||||
|
proc nowUs(): int64 =
|
||||||
|
int64(epochTime() * 1_000_000.0)
|
||||||
|
|
||||||
|
discard fib(20)
|
||||||
|
let t0 = nowUs()
|
||||||
|
let r = fib(30)
|
||||||
|
let t1 = nowUs()
|
||||||
|
if r < 0: quit(1)
|
||||||
|
echo &"BENCH fib30 iters=1 total_us={t1 - t0} us_per_op={t1 - t0}"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
## Nim twin of benches/c/int_loop.c — 1e6 integer arithmetic loop
|
||||||
|
import std/[times, strformat]
|
||||||
|
|
||||||
|
proc nowUs(): int64 =
|
||||||
|
int64(epochTime() * 1_000_000.0)
|
||||||
|
|
||||||
|
const iters = 1_000_000
|
||||||
|
var acc = 0
|
||||||
|
let t0 = nowUs()
|
||||||
|
for i in 0 ..< iters:
|
||||||
|
acc = acc + i * 3 - 1
|
||||||
|
let t1 = nowUs()
|
||||||
|
if acc == 0 and iters > 0: quit(1)
|
||||||
|
let total = t1 - t0
|
||||||
|
echo &"BENCH int_loop iters={iters} total_us={total} us_per_op={total div iters}"
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// Zig twin of benches/c/fib.c — recursive fib(30)
|
||||||
|
// Build: zig build-exe -OReleaseFast -femit-bin=build/fib fib.zig
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
fn fib(n: i32) i32 {
|
||||||
|
if (n <= 1) return n;
|
||||||
|
return fib(n - 1) + fib(n - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nowUs() i64 {
|
||||||
|
return @divTrunc(@as(i64, @intCast(std.time.nanoTimestamp())), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
_ = fib(20);
|
||||||
|
const t0 = nowUs();
|
||||||
|
const r = fib(30);
|
||||||
|
const t1 = nowUs();
|
||||||
|
if (r < 0) return error.Unreachable;
|
||||||
|
const out = std.io.getStdOut().writer();
|
||||||
|
try out.print("BENCH fib30 iters=1 total_us={d} us_per_op={d}\n", .{ t1 - t0, t1 - t0 });
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// Zig twin of benches/c/int_loop.c — 1e6 integer arithmetic loop
|
||||||
|
// Build: zig build-exe -OReleaseFast -femit-bin=build/int_loop int_loop.zig
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
fn nowUs() i64 {
|
||||||
|
return @divTrunc(@as(i64, @intCast(std.time.nanoTimestamp())), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
const iters: i32 = 1_000_000;
|
||||||
|
var acc: i32 = 0;
|
||||||
|
const t0 = nowUs();
|
||||||
|
var i: i32 = 0;
|
||||||
|
while (i < iters) : (i += 1) {
|
||||||
|
acc = acc + i * 3 - 1;
|
||||||
|
}
|
||||||
|
const t1 = nowUs();
|
||||||
|
if (acc == 0 and iters > 0) return error.Unreachable;
|
||||||
|
const total = t1 - t0;
|
||||||
|
const out = std.io.getStdOut().writer();
|
||||||
|
try out.print("BENCH int_loop iters={d} total_us={d} us_per_op={d}\n", .{ iters, total, @divTrunc(total, iters) });
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ type
|
|||||||
tekDynRef ## &dyn Trait — trait object (fat pointer)
|
tekDynRef ## &dyn Trait — trait object (fat pointer)
|
||||||
tekTuple
|
tekTuple
|
||||||
tekSelf
|
tekSelf
|
||||||
|
tekFunc
|
||||||
|
|
||||||
TypeExpr* = ref object
|
TypeExpr* = ref object
|
||||||
loc*: SourceLocation
|
loc*: SourceLocation
|
||||||
@@ -54,6 +55,9 @@ type
|
|||||||
tupleElements*: seq[TypeExpr]
|
tupleElements*: seq[TypeExpr]
|
||||||
of tekSelf:
|
of tekSelf:
|
||||||
discard
|
discard
|
||||||
|
of tekFunc:
|
||||||
|
funcParams*: seq[TypeExpr]
|
||||||
|
funcRet*: TypeExpr
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Patterns
|
# Patterns
|
||||||
@@ -124,8 +128,17 @@ type
|
|||||||
ekUnwrap ## expr! — unwrap or panic
|
ekUnwrap ## expr! — unwrap or panic
|
||||||
ekSpawn ## spawn expr — create a new task
|
ekSpawn ## spawn expr — create a new task
|
||||||
ekAwait ## expr.await — suspend until future resolves
|
ekAwait ## expr.await — suspend until future resolves
|
||||||
|
ekBorrow ## borrow &mut expr — explicit borrow expression
|
||||||
ekBlock
|
ekBlock
|
||||||
ekMatch
|
ekMatch
|
||||||
|
ekStringInterp
|
||||||
|
ekClosure
|
||||||
|
ekMacroCall ## name!(args) — expanded before sema
|
||||||
|
ekMacroStmt ## `$s:stmt` arg wrapper (expand only)
|
||||||
|
ekMacroPat ## `$p:pat` arg wrapper (expand only)
|
||||||
|
ekMacroTt ## `$x:tt` bound fragment (expand only; may flatten groups)
|
||||||
|
ekMacroRep ## `$( expr ),*` expression-level rep in templates (expand only)
|
||||||
|
ekMacroType ## `$t:type` bound type (expand only)
|
||||||
|
|
||||||
MatchArm* = object
|
MatchArm* = object
|
||||||
loc*: SourceLocation
|
loc*: SourceLocation
|
||||||
@@ -172,6 +185,7 @@ type
|
|||||||
of ekCall:
|
of ekCall:
|
||||||
exprCallCallee*: Expr
|
exprCallCallee*: Expr
|
||||||
exprCallArgs*: seq[Expr]
|
exprCallArgs*: seq[Expr]
|
||||||
|
exprCallArgNames*: seq[string] ## empty = positional, non-empty = named arg
|
||||||
exprCallInferredTypeArgs*: seq[TypeExpr] ## filled by sema for inferred generic calls
|
exprCallInferredTypeArgs*: seq[TypeExpr] ## filled by sema for inferred generic calls
|
||||||
of ekGenericCall:
|
of ekGenericCall:
|
||||||
exprGenericCallee*: string
|
exprGenericCallee*: string
|
||||||
@@ -210,11 +224,48 @@ type
|
|||||||
exprSpawnAsync*: bool
|
exprSpawnAsync*: bool
|
||||||
of ekAwait:
|
of ekAwait:
|
||||||
exprAwaitOperand*: Expr
|
exprAwaitOperand*: Expr
|
||||||
|
of ekBorrow:
|
||||||
|
exprBorrowOperand*: Expr
|
||||||
|
exprBorrowMutable*: bool
|
||||||
of ekBlock:
|
of ekBlock:
|
||||||
exprBlock*: Block
|
exprBlock*: Block
|
||||||
of ekMatch:
|
of ekMatch:
|
||||||
exprMatchSubject*: Expr
|
exprMatchSubject*: Expr
|
||||||
exprMatchArms*: seq[MatchArm]
|
exprMatchArms*: seq[MatchArm]
|
||||||
|
of ekStringInterp:
|
||||||
|
exprInterpTexts*: seq[string]
|
||||||
|
exprInterpExprs*: seq[Expr]
|
||||||
|
of ekClosure:
|
||||||
|
exprClosureParams*: seq[Param]
|
||||||
|
exprClosureBody*: Block
|
||||||
|
exprClosureReturnType*: TypeExpr
|
||||||
|
captureCount*: int
|
||||||
|
captureNames*: seq[string]
|
||||||
|
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]
|
||||||
|
of ekMacroStmt:
|
||||||
|
## Statement fragment argument (`$s:stmt`) — only during expand
|
||||||
|
exprMacroStmt*: Stmt
|
||||||
|
of ekMacroPat:
|
||||||
|
## Pattern fragment argument (`$p:pat`) — only during expand
|
||||||
|
exprMacroPat*: Pattern
|
||||||
|
of ekMacroTt:
|
||||||
|
## Bound `:tt` fragment. `exprMacroTtGroup` is true for parenthesized
|
||||||
|
## multi-element groups (parsed as tuples) — flattened when spliced as
|
||||||
|
## the sole argument of a call (`$f($args)` → `f(a, b)`).
|
||||||
|
exprMacroTtInner*: Expr
|
||||||
|
exprMacroTtGroup*: bool
|
||||||
|
of ekMacroRep:
|
||||||
|
## Expression-level `$( body ),*` / `$( body )*` in macro templates.
|
||||||
|
exprMacroRepBody*: Expr
|
||||||
|
of ekMacroType:
|
||||||
|
## Bound `:type` fragment.
|
||||||
|
exprMacroType*: TypeExpr
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Statements
|
# Statements
|
||||||
@@ -234,13 +285,21 @@ type
|
|||||||
skStaticAssert
|
skStaticAssert
|
||||||
skComptime
|
skComptime
|
||||||
skEmit
|
skEmit
|
||||||
|
skDefer
|
||||||
|
skSwitch
|
||||||
skDecl
|
skDecl
|
||||||
|
skMacroRep ## $( … )* template repetition (macro body only)
|
||||||
|
|
||||||
ElseIf* = object
|
ElseIf* = object
|
||||||
loc*: SourceLocation
|
loc*: SourceLocation
|
||||||
cond*: Expr
|
cond*: Expr
|
||||||
blk*: Block
|
blk*: Block
|
||||||
|
|
||||||
|
SwitchCase* = object
|
||||||
|
loc*: SourceLocation
|
||||||
|
caseValue*: Expr
|
||||||
|
caseBody*: Block
|
||||||
|
|
||||||
Block* = ref object
|
Block* = ref object
|
||||||
loc*: SourceLocation
|
loc*: SourceLocation
|
||||||
stmts*: seq[Stmt]
|
stmts*: seq[Stmt]
|
||||||
@@ -294,8 +353,16 @@ type
|
|||||||
of skEmit:
|
of skEmit:
|
||||||
stmtEmitExpr*: Expr
|
stmtEmitExpr*: Expr
|
||||||
stmtEmitEvaluated*: string ## filled by sema CTFE
|
stmtEmitEvaluated*: string ## filled by sema CTFE
|
||||||
|
of skDefer:
|
||||||
|
stmtDeferBody*: Expr
|
||||||
|
of skSwitch:
|
||||||
|
stmtSwitchExpr*: Expr
|
||||||
|
stmtSwitchCases*: seq[SwitchCase]
|
||||||
|
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)
|
||||||
@@ -322,6 +389,31 @@ 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 `{ … }`
|
||||||
|
mfkStmt ## one statement (let/if/… or expression-stmt)
|
||||||
|
mfkPat ## match/let pattern
|
||||||
|
mfkType ## type expression (`int`, `*int`, `String`, …)
|
||||||
|
|
||||||
|
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
|
||||||
@@ -414,6 +506,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)
|
||||||
@@ -441,3 +536,6 @@ proc newBinaryExpr*(op: TokenKind, left, right: Expr, loc: SourceLocation): Expr
|
|||||||
|
|
||||||
proc newUnaryExpr*(op: TokenKind, operand: Expr, loc: SourceLocation): Expr =
|
proc newUnaryExpr*(op: TokenKind, operand: Expr, loc: SourceLocation): Expr =
|
||||||
result = Expr(kind: ekUnary, loc: loc, exprUnaryOp: op, exprUnaryOperand: operand)
|
result = Expr(kind: ekUnary, loc: loc, exprUnaryOp: op, exprUnaryOperand: operand)
|
||||||
|
|
||||||
|
proc newStringInterpExpr*(texts: seq[string], exprs: seq[Expr], loc: SourceLocation): Expr =
|
||||||
|
result = Expr(kind: ekStringInterp, loc: loc, exprInterpTexts: texts, exprInterpExprs: exprs)
|
||||||
|
|||||||
+113
-23
@@ -8,6 +8,7 @@ type
|
|||||||
varCounter*: int
|
varCounter*: int
|
||||||
declaredVars*: seq[string]
|
declaredVars*: seq[string]
|
||||||
sliceTypeDefs*: seq[tuple[name: string, elem: string]] ## Generated Slice_<T> typedefs
|
sliceTypeDefs*: seq[tuple[name: string, elem: string]] ## Generated Slice_<T> typedefs
|
||||||
|
deferStack*: seq[seq[string]] ## Function-level deferred statements (LIFO)
|
||||||
|
|
||||||
proc cEscape(s: string): string =
|
proc cEscape(s: string): string =
|
||||||
## Escape a string for use as a C string literal.
|
## Escape a string for use as a C string literal.
|
||||||
@@ -45,6 +46,28 @@ proc freshVar(be: var CBackend): string =
|
|||||||
inc be.varCounter
|
inc be.varCounter
|
||||||
result = &"__tmp_{be.varCounter}"
|
result = &"__tmp_{be.varCounter}"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Defer helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
proc pushDeferScope(be: var CBackend) =
|
||||||
|
be.deferStack.add(@[])
|
||||||
|
|
||||||
|
proc popDeferScope(be: var CBackend) =
|
||||||
|
if be.deferStack.len > 0:
|
||||||
|
be.deferStack.setLen(be.deferStack.len - 1)
|
||||||
|
|
||||||
|
proc emitCurrentDefers(be: var CBackend) =
|
||||||
|
## Emit all deferred statements in current scope, LIFO order
|
||||||
|
if be.deferStack.len == 0: return
|
||||||
|
let idx = be.deferStack.len - 1
|
||||||
|
for i in countdown(be.deferStack[idx].len - 1, 0):
|
||||||
|
be.emitLine(be.deferStack[idx][i])
|
||||||
|
|
||||||
|
proc clearCurrentDefers(be: var CBackend) =
|
||||||
|
if be.deferStack.len > 0:
|
||||||
|
let idx = be.deferStack.len - 1
|
||||||
|
be.deferStack[idx].setLen(0)
|
||||||
|
|
||||||
# Type conversion: Bux Type → C type string
|
# Type conversion: Bux Type → C type string
|
||||||
proc typeToC*(be: var CBackend, typ: Type): string =
|
proc typeToC*(be: var CBackend, typ: Type): string =
|
||||||
if typ == nil: return "void"
|
if typ == nil: return "void"
|
||||||
@@ -105,8 +128,31 @@ proc typeToC*(be: var CBackend, typ: Type): string =
|
|||||||
of "float64": return "double"
|
of "float64": return "double"
|
||||||
of "bool": return "bool"
|
of "bool": return "bool"
|
||||||
else: return typ.name
|
else: return typ.name
|
||||||
of tkTuple: return "void*" # TODO: proper tuple struct
|
of tkTuple:
|
||||||
of tkFunc: return "void*" # TODO: function pointer
|
if typ.inner.len == 0:
|
||||||
|
return "Tuple_Empty"
|
||||||
|
var parts: seq[string] = @[]
|
||||||
|
for e in typ.inner:
|
||||||
|
var p = typeToC(be, e)
|
||||||
|
p = p.replace("const char*", "cstr").replace("unsigned int", "uint")
|
||||||
|
p = p.replace(" ", "_").replace("*", "Ptr").replace("(", "").replace(")", "").replace(",", "_")
|
||||||
|
parts.add(p)
|
||||||
|
let tname = "Tuple_" & parts.join("_")
|
||||||
|
# Ensure typedef is collected alongside slices
|
||||||
|
var already = false
|
||||||
|
for d in be.sliceTypeDefs:
|
||||||
|
if d.name == tname:
|
||||||
|
already = true
|
||||||
|
break
|
||||||
|
if not already:
|
||||||
|
# Reuse sliceTypeDefs as a generic "extra typedef" bag: elem holds field list markup
|
||||||
|
be.sliceTypeDefs.add((name: tname, elem: "/*tuple*/"))
|
||||||
|
return tname
|
||||||
|
of tkFunc:
|
||||||
|
if typ.inner.len == 0: return "void (*)(void)"
|
||||||
|
let params = typ.inner[0..^2].mapIt(typeToC(be, it)).join(", ")
|
||||||
|
let ret = typeToC(be, typ.inner[^1])
|
||||||
|
return ret & " (*)(" & params & ")"
|
||||||
else:
|
else:
|
||||||
when defined(release):
|
when defined(release):
|
||||||
return "void*"
|
return "void*"
|
||||||
@@ -249,6 +295,10 @@ proc emitExpr(be: var CBackend, node: HirNode): string =
|
|||||||
let base = be.emitExpr(node.fieldPtrBase)
|
let base = be.emitExpr(node.fieldPtrBase)
|
||||||
return &"(&({base}.{node.fieldName}))"
|
return &"(&({base}.{node.fieldName}))"
|
||||||
|
|
||||||
|
of hFieldAccess:
|
||||||
|
let base = be.emitExpr(node.fieldAccessBase)
|
||||||
|
return &"({base}.{node.fieldAccessName})"
|
||||||
|
|
||||||
of hArrowField:
|
of hArrowField:
|
||||||
let base = be.emitExpr(node.arrowFieldBase)
|
let base = be.emitExpr(node.arrowFieldBase)
|
||||||
return &"(&({base}->{node.arrowFieldName}))"
|
return &"(&({base}->{node.arrowFieldName}))"
|
||||||
@@ -284,10 +334,11 @@ proc emitExpr(be: var CBackend, node: HirNode): string =
|
|||||||
return &"({base}).data[{idx}]"
|
return &"({base}).data[{idx}]"
|
||||||
|
|
||||||
of hTupleInit:
|
of hTupleInit:
|
||||||
var elems: seq[string] = @[]
|
let typeName = typeToC(be, node.typ)
|
||||||
for e in node.tupleInitElements:
|
var fields: seq[string] = @[]
|
||||||
elems.add(be.emitExpr(e))
|
for i, e in node.tupleInitElements:
|
||||||
return &"{{{elems.join(\", \")}}}"
|
fields.add(&"._{i} = {be.emitExpr(e)}")
|
||||||
|
return &"(({typeName}){{{fields.join(\", \")}}})"
|
||||||
|
|
||||||
of hCast:
|
of hCast:
|
||||||
let operand = be.emitExpr(node.castOperand)
|
let operand = be.emitExpr(node.castOperand)
|
||||||
@@ -350,7 +401,8 @@ proc emitExpr(be: var CBackend, node: HirNode): string =
|
|||||||
return "0"
|
return "0"
|
||||||
|
|
||||||
of hMatch:
|
of hMatch:
|
||||||
return "0" # TODO: match expression lowering
|
# Match should be desugared in hir_lower to if-else. Fallback: 0.
|
||||||
|
return "0"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return "0"
|
return "0"
|
||||||
@@ -358,7 +410,14 @@ proc emitExpr(be: var CBackend, node: HirNode): string =
|
|||||||
proc emitStmt(be: var CBackend, node: HirNode) =
|
proc emitStmt(be: var CBackend, node: HirNode) =
|
||||||
if node == nil: return
|
if node == nil: return
|
||||||
case node.kind
|
case node.kind
|
||||||
|
of hDefer:
|
||||||
|
let expr = be.emitExpr(node.deferBody)
|
||||||
|
if be.deferStack.len > 0:
|
||||||
|
let idx = be.deferStack.len - 1
|
||||||
|
be.deferStack[idx].add(expr & ";")
|
||||||
|
|
||||||
of hReturn:
|
of hReturn:
|
||||||
|
be.emitCurrentDefers()
|
||||||
if node.returnValue != nil:
|
if node.returnValue != nil:
|
||||||
let val = be.emitExpr(node.returnValue)
|
let val = be.emitExpr(node.returnValue)
|
||||||
be.emitLine(&"return {val};")
|
be.emitLine(&"return {val};")
|
||||||
@@ -394,9 +453,11 @@ proc emitStmt(be: var CBackend, node: HirNode) =
|
|||||||
be.emitLine("}")
|
be.emitLine("}")
|
||||||
|
|
||||||
of hBreak:
|
of hBreak:
|
||||||
|
be.emitCurrentDefers()
|
||||||
be.emitLine("break;")
|
be.emitLine("break;")
|
||||||
|
|
||||||
of hContinue:
|
of hContinue:
|
||||||
|
be.emitCurrentDefers()
|
||||||
be.emitLine("continue;")
|
be.emitLine("continue;")
|
||||||
|
|
||||||
of hEmit:
|
of hEmit:
|
||||||
@@ -444,6 +505,18 @@ proc emitStmt(be: var CBackend, node: HirNode) =
|
|||||||
be.emitLine(&"{expr};")
|
be.emitLine(&"{expr};")
|
||||||
|
|
||||||
proc emitFunc*(be: var CBackend, hfunc: HirFunc) =
|
proc emitFunc*(be: var CBackend, hfunc: HirFunc) =
|
||||||
|
# Emit env struct and global instance for closures with captures
|
||||||
|
if hfunc.captureNames.len > 0 and hfunc.envStructName != "":
|
||||||
|
be.emitLine(&"struct {hfunc.envStructName} {{")
|
||||||
|
inc be.indent
|
||||||
|
for i in 0 ..< hfunc.captureNames.len:
|
||||||
|
let capName = hfunc.captureNames[i]
|
||||||
|
let capType = if i < hfunc.captureTypes.len: typeToC(be, hfunc.captureTypes[i]) else: "int"
|
||||||
|
be.emitLine(&"{capType} {capName};")
|
||||||
|
dec be.indent
|
||||||
|
be.emitLine("};")
|
||||||
|
be.emitLine(&"static struct {hfunc.envStructName} {hfunc.envInstanceName};")
|
||||||
|
be.emitLine("")
|
||||||
let retType = typeToC(be, hfunc.retType)
|
let retType = typeToC(be, hfunc.retType)
|
||||||
var params: seq[string] = @[]
|
var params: seq[string] = @[]
|
||||||
for p in hfunc.params:
|
for p in hfunc.params:
|
||||||
@@ -453,15 +526,23 @@ proc emitFunc*(be: var CBackend, hfunc: HirFunc) =
|
|||||||
let paramsStr = params.join(", ")
|
let paramsStr = params.join(", ")
|
||||||
be.emitLine(&"{retType} {hfunc.name}({paramsStr}) {{")
|
be.emitLine(&"{retType} {hfunc.name}({paramsStr}) {{")
|
||||||
inc be.indent
|
inc be.indent
|
||||||
|
be.pushDeferScope()
|
||||||
if hfunc.body != nil:
|
if hfunc.body != nil:
|
||||||
if hfunc.body.kind == hBlock:
|
if hfunc.body.kind == hBlock:
|
||||||
for stmt in hfunc.body.blockStmts:
|
for stmt in hfunc.body.blockStmts:
|
||||||
be.emitStmt(stmt)
|
be.emitStmt(stmt)
|
||||||
if hfunc.body.blockExpr != nil and hfunc.retType.kind != tkVoid:
|
if hfunc.body.blockExpr != nil and hfunc.retType.kind != tkVoid:
|
||||||
|
be.emitCurrentDefers()
|
||||||
let val = be.emitExpr(hfunc.body.blockExpr)
|
let val = be.emitExpr(hfunc.body.blockExpr)
|
||||||
be.emitLine(&"return {val};")
|
be.emitLine(&"return {val};")
|
||||||
|
else:
|
||||||
|
be.emitCurrentDefers()
|
||||||
else:
|
else:
|
||||||
be.emitStmt(hfunc.body)
|
be.emitStmt(hfunc.body)
|
||||||
|
be.emitCurrentDefers()
|
||||||
|
else:
|
||||||
|
be.emitCurrentDefers()
|
||||||
|
be.popDeferScope()
|
||||||
dec be.indent
|
dec be.indent
|
||||||
be.emitLine("}")
|
be.emitLine("}")
|
||||||
be.emitLine("")
|
be.emitLine("")
|
||||||
@@ -514,20 +595,17 @@ proc emitEnum*(be: var CBackend, name: string, variants: seq[HirEnumVariant]) =
|
|||||||
be.emitLine(&"typedef union {{")
|
be.emitLine(&"typedef union {{")
|
||||||
inc be.indent
|
inc be.indent
|
||||||
for v in variants:
|
for v in variants:
|
||||||
if v.fields.len > 0:
|
if v.fields.len == 1:
|
||||||
# Positional fields
|
# Single positional field — flat union member (compat: data.Variant_0)
|
||||||
for i, f in v.fields:
|
let typ = typeToC(be, v.fields[0])
|
||||||
let typ = typeToC(be, f)
|
be.emitLine(&"{typ} {v.name}_0;")
|
||||||
be.emitLine(&"{typ} {v.name}_{i};")
|
elif v.fields.len > 1:
|
||||||
|
# Multi positional — named nested typedef Enum_Variant_Payload
|
||||||
|
let nestedName = name & "_" & v.name & "_Payload"
|
||||||
|
be.emitLine(&"{nestedName} {v.name};")
|
||||||
elif v.namedFields.len > 0:
|
elif v.namedFields.len > 0:
|
||||||
# Named fields - generate as struct
|
let nestedName = name & "_" & v.name & "_Payload"
|
||||||
be.emitLine(&"struct {{")
|
be.emitLine(&"{nestedName} {v.name};")
|
||||||
inc be.indent
|
|
||||||
for nf in v.namedFields:
|
|
||||||
let typ = typeToC(be, nf.typ)
|
|
||||||
be.emitLine(&"{typ} {nf.name};")
|
|
||||||
dec be.indent
|
|
||||||
be.emitLine(&"}} {v.name};")
|
|
||||||
dec be.indent
|
dec be.indent
|
||||||
be.emitLine(&"}} {name}_Data;")
|
be.emitLine(&"}} {name}_Data;")
|
||||||
be.emitLine("")
|
be.emitLine("")
|
||||||
@@ -627,15 +705,27 @@ proc emitModule*(be: var CBackend, module: HirModule): string =
|
|||||||
if module.structs.len > 0:
|
if module.structs.len > 0:
|
||||||
be.emitLine("")
|
be.emitLine("")
|
||||||
|
|
||||||
# Enum definitions (must come before structs that reference them)
|
# Nested multi-field enum payloads (Enum_Variant_Payload) must be fully
|
||||||
|
# defined before the algebraic enum union that embeds them by value.
|
||||||
|
var payloadStructNames: seq[string] = @[]
|
||||||
|
for e in module.enums:
|
||||||
|
for v in e.variants:
|
||||||
|
if v.fields.len > 1 or v.namedFields.len > 0:
|
||||||
|
payloadStructNames.add(e.name & "_" & v.name & "_Payload")
|
||||||
|
for s in module.structs:
|
||||||
|
if s.name in payloadStructNames:
|
||||||
|
be.emitStruct(s.name, s.fields)
|
||||||
|
|
||||||
|
# Enum definitions (after payload structs; before user structs that may use them)
|
||||||
for e in module.enums:
|
for e in module.enums:
|
||||||
be.emitEnum(e.name, e.variants)
|
be.emitEnum(e.name, e.variants)
|
||||||
if module.enums.len > 0:
|
if module.enums.len > 0:
|
||||||
be.emitLine("")
|
be.emitLine("")
|
||||||
|
|
||||||
# Struct definitions
|
# Remaining struct definitions (skip payloads already emitted)
|
||||||
for s in module.structs:
|
for s in module.structs:
|
||||||
be.emitStruct(s.name, s.fields)
|
if s.name notin payloadStructNames:
|
||||||
|
be.emitStruct(s.name, s.fields)
|
||||||
|
|
||||||
# Slice fat-pointer typedefs
|
# Slice fat-pointer typedefs
|
||||||
if sliceTypes.len > 0:
|
if sliceTypes.len > 0:
|
||||||
|
|||||||
+841
-58
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,202 @@
|
|||||||
|
## docgen.nim — Extract `///` (and adjacent `/* */`) docs into Markdown.
|
||||||
|
## Used by `bux doc [path...]`.
|
||||||
|
|
||||||
|
import std/[os, strutils, strformat, algorithm]
|
||||||
|
|
||||||
|
type
|
||||||
|
DocItem* = object
|
||||||
|
kind*: string ## module | func | struct | enum | interface | extern
|
||||||
|
name*: string
|
||||||
|
signature*: string ## first declaration line (trimmed)
|
||||||
|
docs*: string
|
||||||
|
file*: string
|
||||||
|
line*: int
|
||||||
|
|
||||||
|
proc isDeclStart(line: string): bool =
|
||||||
|
let s = line.strip()
|
||||||
|
if s.len == 0: return false
|
||||||
|
# Leading attributes @[Checked] etc. — not a decl by themselves
|
||||||
|
if s.startsWith("@["): return false
|
||||||
|
if s.startsWith("func ") or s.startsWith("pub func ") or
|
||||||
|
s.startsWith("extern func ") or s.startsWith("const func ") or
|
||||||
|
s.startsWith("async func "):
|
||||||
|
return true
|
||||||
|
if s.startsWith("struct ") or s.startsWith("pub struct ") or
|
||||||
|
s.startsWith("enum ") or s.startsWith("pub enum ") or
|
||||||
|
s.startsWith("union ") or s.startsWith("interface ") or
|
||||||
|
s.startsWith("module ") or s.startsWith("type "):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
proc declKindAndName(line: string): tuple[kind, name: string] =
|
||||||
|
var s = line.strip()
|
||||||
|
# Strip leading pub/extern/const/async
|
||||||
|
for prefix in ["pub ", "extern ", "const ", "async "]:
|
||||||
|
if s.startsWith(prefix):
|
||||||
|
s = s[prefix.len .. ^1].strip()
|
||||||
|
var kind = "item"
|
||||||
|
if s.startsWith("func "):
|
||||||
|
kind = "func"
|
||||||
|
s = s["func ".len .. ^1]
|
||||||
|
elif s.startsWith("struct "):
|
||||||
|
kind = "struct"
|
||||||
|
s = s["struct ".len .. ^1]
|
||||||
|
elif s.startsWith("enum "):
|
||||||
|
kind = "enum"
|
||||||
|
s = s["enum ".len .. ^1]
|
||||||
|
elif s.startsWith("union "):
|
||||||
|
kind = "union"
|
||||||
|
s = s["union ".len .. ^1]
|
||||||
|
elif s.startsWith("interface "):
|
||||||
|
kind = "interface"
|
||||||
|
s = s["interface ".len .. ^1]
|
||||||
|
elif s.startsWith("module "):
|
||||||
|
kind = "module"
|
||||||
|
s = s["module ".len .. ^1]
|
||||||
|
elif s.startsWith("type "):
|
||||||
|
kind = "type"
|
||||||
|
s = s["type ".len .. ^1]
|
||||||
|
# Name: until `<` `(` `{` `:` space
|
||||||
|
var name = ""
|
||||||
|
for ch in s:
|
||||||
|
if ch in {' ', '<', '(', '{', ':', ';'}:
|
||||||
|
break
|
||||||
|
name.add(ch)
|
||||||
|
if name.len == 0:
|
||||||
|
name = s
|
||||||
|
# extern funcs already stripped "extern "
|
||||||
|
if kind == "func" and line.strip().startsWith("extern"):
|
||||||
|
kind = "extern"
|
||||||
|
return (kind, name)
|
||||||
|
|
||||||
|
proc extractDocsFromSource*(source, path: string): seq[DocItem] =
|
||||||
|
result = @[]
|
||||||
|
var pending: seq[string] = @[]
|
||||||
|
var inBlockComment = false
|
||||||
|
var blockDoc: seq[string] = @[]
|
||||||
|
var lineNo = 0
|
||||||
|
for rawLine in source.splitLines():
|
||||||
|
inc lineNo
|
||||||
|
var line = rawLine
|
||||||
|
let stripped = line.strip()
|
||||||
|
|
||||||
|
# Block comment handling (/* ... */ used in stdlib today)
|
||||||
|
if inBlockComment:
|
||||||
|
let endIdx = stripped.find("*/")
|
||||||
|
if endIdx >= 0:
|
||||||
|
let before = stripped[0 ..< endIdx].strip()
|
||||||
|
if before.len > 0:
|
||||||
|
blockDoc.add(before)
|
||||||
|
inBlockComment = false
|
||||||
|
# Treat completed block as pending doc if non-empty
|
||||||
|
if blockDoc.len > 0:
|
||||||
|
pending = blockDoc
|
||||||
|
blockDoc = @[]
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
blockDoc.add(stripped)
|
||||||
|
continue
|
||||||
|
if stripped.startsWith("/*") and not stripped.startsWith("/***"):
|
||||||
|
let rest = stripped["/*".len .. ^1]
|
||||||
|
let endIdx = rest.find("*/")
|
||||||
|
if endIdx >= 0:
|
||||||
|
let body = rest[0 ..< endIdx].strip()
|
||||||
|
if body.len > 0:
|
||||||
|
pending = @[body]
|
||||||
|
else:
|
||||||
|
inBlockComment = true
|
||||||
|
blockDoc = @[]
|
||||||
|
let body = rest.strip()
|
||||||
|
if body.len > 0:
|
||||||
|
blockDoc.add(body)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Triple-slash doc comments
|
||||||
|
if stripped.startsWith("///"):
|
||||||
|
var body = stripped["///".len .. ^1]
|
||||||
|
if body.startsWith(" "):
|
||||||
|
body = body[1 .. ^1]
|
||||||
|
pending.add(body)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Empty line: keep pending docs (allow blank lines inside doc blocks)
|
||||||
|
if stripped.len == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Attributes immediately before decl: keep pending
|
||||||
|
if stripped.startsWith("@["):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isDeclStart(line):
|
||||||
|
if pending.len > 0:
|
||||||
|
let (kind, name) = declKindAndName(line)
|
||||||
|
result.add(DocItem(
|
||||||
|
kind: kind,
|
||||||
|
name: name,
|
||||||
|
signature: stripped,
|
||||||
|
docs: pending.join("\n"),
|
||||||
|
file: path,
|
||||||
|
line: lineNo
|
||||||
|
))
|
||||||
|
pending = @[]
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Other code clears pending (except plain // comments)
|
||||||
|
if stripped.startsWith("//"):
|
||||||
|
continue
|
||||||
|
pending = @[]
|
||||||
|
|
||||||
|
proc collectBuxFilesForDoc*(root: string): seq[string] =
|
||||||
|
result = @[]
|
||||||
|
if fileExists(root) and root.endsWith(".bux"):
|
||||||
|
result.add(root)
|
||||||
|
return
|
||||||
|
if not dirExists(root):
|
||||||
|
return
|
||||||
|
for path in walkDirRec(root):
|
||||||
|
if path.endsWith(".bux"):
|
||||||
|
result.add(path)
|
||||||
|
result.sort(system.cmp)
|
||||||
|
|
||||||
|
proc renderMarkdown*(items: seq[DocItem], title: string = "API Reference"): string =
|
||||||
|
var sb: string
|
||||||
|
sb.add(&"# {title}\n\n")
|
||||||
|
sb.add("Generated by `bux doc` from `///` and `/* */` documentation comments.\n\n")
|
||||||
|
if items.len == 0:
|
||||||
|
sb.add("_No documented items found._\n")
|
||||||
|
return sb
|
||||||
|
|
||||||
|
# Group by file
|
||||||
|
var byFile: seq[string] = @[]
|
||||||
|
for it in items:
|
||||||
|
if it.file notin byFile:
|
||||||
|
byFile.add(it.file)
|
||||||
|
byFile.sort(system.cmp)
|
||||||
|
|
||||||
|
for f in byFile:
|
||||||
|
let base = splitFile(f).name
|
||||||
|
sb.add(&"## `{base}`\n\n")
|
||||||
|
sb.add(&"_Source: `{f}`_\n\n")
|
||||||
|
for it in items:
|
||||||
|
if it.file != f:
|
||||||
|
continue
|
||||||
|
sb.add(&"### `{it.name}` _{it.kind}_\n\n")
|
||||||
|
sb.add("```bux\n")
|
||||||
|
sb.add(it.signature)
|
||||||
|
sb.add("\n```\n\n")
|
||||||
|
if it.docs.len > 0:
|
||||||
|
sb.add(it.docs)
|
||||||
|
sb.add("\n\n")
|
||||||
|
return sb
|
||||||
|
|
||||||
|
proc generateDocs*(paths: seq[string]): seq[DocItem] =
|
||||||
|
result = @[]
|
||||||
|
var files: seq[string] = @[]
|
||||||
|
for p in paths:
|
||||||
|
for f in collectBuxFilesForDoc(p):
|
||||||
|
if f notin files:
|
||||||
|
files.add(f)
|
||||||
|
files.sort(system.cmp)
|
||||||
|
for f in files:
|
||||||
|
let src = readFile(f)
|
||||||
|
result.add(extractDocsFromSource(src, f))
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
## fmt.nim — Indentation-based Bux source formatter (bootstrap).
|
||||||
|
## Mirrors selfhost `src/fmt.bux`: re-indent by brace depth, preserve content.
|
||||||
|
|
||||||
|
import std/[strutils, os, algorithm]
|
||||||
|
|
||||||
|
proc isInStringOrComment(line: string, pos: int): bool =
|
||||||
|
## Simplified: track `//`, `"..."`, and `'...'` up to `pos`.
|
||||||
|
var inString = false
|
||||||
|
var inChar = false
|
||||||
|
var inComment = false
|
||||||
|
var i = 0
|
||||||
|
while i < pos and i < line.len:
|
||||||
|
let c = line[i]
|
||||||
|
let n = if i + 1 < line.len: line[i + 1] else: '\0'
|
||||||
|
if inComment:
|
||||||
|
inc i
|
||||||
|
continue
|
||||||
|
if c == '/' and n == '/':
|
||||||
|
inComment = true
|
||||||
|
inc i
|
||||||
|
continue
|
||||||
|
if c == '"' and not inChar:
|
||||||
|
inString = not inString
|
||||||
|
if c == '\'' and not inString:
|
||||||
|
inChar = not inChar
|
||||||
|
inc i
|
||||||
|
return inString or inChar or inComment
|
||||||
|
|
||||||
|
proc countBraceDelta(line: string): int =
|
||||||
|
var delta = 0
|
||||||
|
for i in 0 ..< line.len:
|
||||||
|
if isInStringOrComment(line, i):
|
||||||
|
continue
|
||||||
|
let c = line[i]
|
||||||
|
if c == '{':
|
||||||
|
inc delta
|
||||||
|
elif c == '}':
|
||||||
|
dec delta
|
||||||
|
return delta
|
||||||
|
|
||||||
|
proc formatSource*(source: string): string =
|
||||||
|
## Re-indent each non-empty line to 4 spaces × brace depth.
|
||||||
|
## Idempotent: formatting a clean file is a no-op.
|
||||||
|
var sb: string
|
||||||
|
var indent = 0
|
||||||
|
# Nim's splitLines leaves a trailing "" when the source ends with '\n'.
|
||||||
|
# Drop that artifact so we don't accumulate blank lines on re-format.
|
||||||
|
var lines = source.splitLines(keepEol = false)
|
||||||
|
if source.len > 0 and source.endsWith('\n') and lines.len > 0 and lines[^1].len == 0:
|
||||||
|
lines.setLen(lines.len - 1)
|
||||||
|
for line in lines:
|
||||||
|
let trimmed = line.strip(leading = true, trailing = false)
|
||||||
|
if trimmed.len == 0:
|
||||||
|
sb.add('\n')
|
||||||
|
continue
|
||||||
|
|
||||||
|
let delta = countBraceDelta(trimmed)
|
||||||
|
let firstChar = trimmed[0]
|
||||||
|
if firstChar == '}':
|
||||||
|
dec indent
|
||||||
|
if indent < 0:
|
||||||
|
indent = 0
|
||||||
|
|
||||||
|
for _ in 0 ..< indent:
|
||||||
|
sb.add(" ")
|
||||||
|
sb.add(trimmed)
|
||||||
|
sb.add('\n')
|
||||||
|
|
||||||
|
if firstChar != '}':
|
||||||
|
indent = indent + delta
|
||||||
|
else:
|
||||||
|
# Net delta after the initial decrease for a leading `}`
|
||||||
|
indent = indent + delta + 1
|
||||||
|
if indent < 0:
|
||||||
|
indent = 0
|
||||||
|
|
||||||
|
return sb
|
||||||
|
|
||||||
|
proc formatFile*(path: string, checkOnly: bool): tuple[ok: bool, changed: bool, msg: string] =
|
||||||
|
## Format `path` in place, or only check if reformatting would change it.
|
||||||
|
if not fileExists(path):
|
||||||
|
return (false, false, "file not found: " & path)
|
||||||
|
let source = readFile(path)
|
||||||
|
let formatted = formatSource(source)
|
||||||
|
if formatted == source:
|
||||||
|
return (true, false, "")
|
||||||
|
if checkOnly:
|
||||||
|
return (true, true, "would reformat")
|
||||||
|
try:
|
||||||
|
writeFile(path, formatted)
|
||||||
|
return (true, true, "formatted")
|
||||||
|
except CatchableError as e:
|
||||||
|
return (false, false, e.msg)
|
||||||
|
|
||||||
|
proc collectBuxFiles*(root: string): seq[string] =
|
||||||
|
## Collect `.bux` files: single file, or recursive directory walk.
|
||||||
|
result = @[]
|
||||||
|
if fileExists(root) and root.endsWith(".bux"):
|
||||||
|
result.add(root)
|
||||||
|
return
|
||||||
|
if not dirExists(root):
|
||||||
|
return
|
||||||
|
for path in walkDirRec(root):
|
||||||
|
if path.endsWith(".bux"):
|
||||||
|
result.add(path)
|
||||||
|
result.sort(system.cmp)
|
||||||
@@ -17,11 +17,13 @@ type
|
|||||||
hBreak
|
hBreak
|
||||||
hContinue
|
hContinue
|
||||||
hReturn
|
hReturn
|
||||||
|
hDefer
|
||||||
# Memory
|
# Memory
|
||||||
hAlloca
|
hAlloca
|
||||||
hLoad
|
hLoad
|
||||||
hStore
|
hStore
|
||||||
hFieldPtr
|
hFieldPtr
|
||||||
|
hFieldAccess
|
||||||
hArrowField
|
hArrowField
|
||||||
hIndexPtr
|
hIndexPtr
|
||||||
hSliceIndex
|
hSliceIndex
|
||||||
@@ -84,6 +86,8 @@ type
|
|||||||
continueLabel*: string
|
continueLabel*: string
|
||||||
of hReturn:
|
of hReturn:
|
||||||
returnValue*: HirNode
|
returnValue*: HirNode
|
||||||
|
of hDefer:
|
||||||
|
deferBody*: HirNode
|
||||||
of hAlloca:
|
of hAlloca:
|
||||||
allocaType*: Type
|
allocaType*: Type
|
||||||
allocaName*: string
|
allocaName*: string
|
||||||
@@ -95,6 +99,9 @@ type
|
|||||||
of hFieldPtr:
|
of hFieldPtr:
|
||||||
fieldPtrBase*: HirNode
|
fieldPtrBase*: HirNode
|
||||||
fieldName*: string
|
fieldName*: string
|
||||||
|
of hFieldAccess:
|
||||||
|
fieldAccessBase*: HirNode
|
||||||
|
fieldAccessName*: string
|
||||||
of hArrowField:
|
of hArrowField:
|
||||||
arrowFieldBase*: HirNode
|
arrowFieldBase*: HirNode
|
||||||
arrowFieldName*: string
|
arrowFieldName*: string
|
||||||
@@ -156,6 +163,7 @@ type
|
|||||||
HirMatchArm* = object
|
HirMatchArm* = object
|
||||||
pattern*: Pattern
|
pattern*: Pattern
|
||||||
body*: HirNode
|
body*: HirNode
|
||||||
|
guard*: HirNode ## optional: lowered `if cond` from `p if cond => body`
|
||||||
|
|
||||||
HirFunc* = object
|
HirFunc* = object
|
||||||
name*: string
|
name*: string
|
||||||
@@ -163,6 +171,11 @@ type
|
|||||||
retType*: Type
|
retType*: Type
|
||||||
body*: HirNode
|
body*: HirNode
|
||||||
isPublic*: bool
|
isPublic*: bool
|
||||||
|
# Closure capture metadata
|
||||||
|
captureNames*: seq[string]
|
||||||
|
captureTypes*: seq[Type]
|
||||||
|
envStructName*: string
|
||||||
|
envInstanceName*: string
|
||||||
|
|
||||||
HirEnumVariant* = object
|
HirEnumVariant* = object
|
||||||
name*: string
|
name*: string
|
||||||
@@ -177,6 +190,10 @@ type
|
|||||||
consts*: seq[tuple[name: string, typ: Type, value: HirNode]]
|
consts*: seq[tuple[name: string, typ: Type, value: HirNode]]
|
||||||
interfaces*: seq[tuple[name: string, hasAssocTypes: bool, methods: seq[tuple[name: string, params: seq[Type], ret: Type]]]]
|
interfaces*: seq[tuple[name: string, hasAssocTypes: bool, methods: seq[tuple[name: string, params: seq[Type], ret: Type]]]]
|
||||||
vtables*: seq[tuple[interfaceName: string, concreteType: string, methodNames: seq[string], hasAssocTypes: bool]]
|
vtables*: seq[tuple[interfaceName: string, concreteType: string, methodNames: seq[string], hasAssocTypes: bool]]
|
||||||
|
## Named functions used as values → need __adapt_ wrappers for fat-func ABI
|
||||||
|
funcAdapters*: seq[tuple[name: string, typ: Type]]
|
||||||
|
## Extra func types seen in locals/closures that need BuxFn_* typedefs
|
||||||
|
seenFatTypes*: seq[Type]
|
||||||
|
|
||||||
# Constructor helpers
|
# Constructor helpers
|
||||||
proc hirLit*(tok: Token, typ: Type, loc: SourceLocation): HirNode =
|
proc hirLit*(tok: Token, typ: Type, loc: SourceLocation): HirNode =
|
||||||
@@ -200,15 +217,25 @@ proc hirCall*(callee: string, args: seq[HirNode], typ: Type, loc: SourceLocation
|
|||||||
proc hirReturn*(value: HirNode, loc: SourceLocation): HirNode =
|
proc hirReturn*(value: HirNode, loc: SourceLocation): HirNode =
|
||||||
HirNode(kind: hReturn, returnValue: value, typ: makeVoid(), loc: loc)
|
HirNode(kind: hReturn, returnValue: value, typ: makeVoid(), loc: loc)
|
||||||
|
|
||||||
|
proc hirDefer*(body: HirNode, loc: SourceLocation): HirNode =
|
||||||
|
HirNode(kind: hDefer, deferBody: body, typ: makeVoid(), loc: loc)
|
||||||
|
|
||||||
proc hirBlock*(stmts: seq[HirNode], expr: HirNode, typ: Type, loc: SourceLocation, isScope: bool = false): HirNode =
|
proc hirBlock*(stmts: seq[HirNode], expr: HirNode, typ: Type, loc: SourceLocation, isScope: bool = false): HirNode =
|
||||||
HirNode(kind: hBlock, blockStmts: stmts, blockExpr: expr, typ: typ, loc: loc, isScope: isScope)
|
HirNode(kind: hBlock, blockStmts: stmts, blockExpr: expr, typ: typ, loc: loc, isScope: isScope)
|
||||||
|
|
||||||
|
proc hirIf*(cond, thenBranch, elseBranch: HirNode, loc: SourceLocation): HirNode =
|
||||||
|
HirNode(kind: hIf, ifCond: cond, ifThen: thenBranch, ifElse: elseBranch, typ: makeVoid(), loc: loc)
|
||||||
|
|
||||||
|
|
||||||
proc hirAlloca*(name: string, typ: Type, loc: SourceLocation): HirNode =
|
proc hirAlloca*(name: string, typ: Type, loc: SourceLocation): HirNode =
|
||||||
HirNode(kind: hAlloca, allocaType: typ, allocaName: name, typ: makePointer(typ), loc: loc)
|
HirNode(kind: hAlloca, allocaType: typ, allocaName: name, typ: makePointer(typ), loc: loc)
|
||||||
|
|
||||||
proc hirStore*(ptrNode, value: HirNode, loc: SourceLocation): HirNode =
|
proc hirStore*(ptrNode, value: HirNode, loc: SourceLocation): HirNode =
|
||||||
HirNode(kind: hStore, storePtr: ptrNode, storeValue: value, typ: makeVoid(), loc: loc)
|
HirNode(kind: hStore, storePtr: ptrNode, storeValue: value, typ: makeVoid(), loc: loc)
|
||||||
|
|
||||||
|
proc hirAssign*(target, value: HirNode, loc: SourceLocation): HirNode =
|
||||||
|
HirNode(kind: hAssign, assignOp: tkAssign, assignTarget: target, assignValue: value, typ: makeVoid(), loc: loc)
|
||||||
|
|
||||||
proc hirLoad*(ptrNode: HirNode, typ: Type, loc: SourceLocation): HirNode =
|
proc hirLoad*(ptrNode: HirNode, typ: Type, loc: SourceLocation): HirNode =
|
||||||
HirNode(kind: hLoad, loadPtr: ptrNode, typ: typ, loc: loc)
|
HirNode(kind: hLoad, loadPtr: ptrNode, typ: typ, loc: loc)
|
||||||
|
|
||||||
|
|||||||
+1823
-169
File diff suppressed because it is too large
Load Diff
+20
-24
@@ -61,12 +61,6 @@ proc advance(lex: var Lexer): char =
|
|||||||
else:
|
else:
|
||||||
inc lex.col
|
inc lex.col
|
||||||
|
|
||||||
proc match(lex: var Lexer, expected: char): bool =
|
|
||||||
if lex.isAtEnd(): return false
|
|
||||||
if lex.peek() != expected: return false
|
|
||||||
discard lex.advance()
|
|
||||||
return true
|
|
||||||
|
|
||||||
proc matchStr(lex: var Lexer, s: string): bool =
|
proc matchStr(lex: var Lexer, s: string): bool =
|
||||||
for i, c in s:
|
for i, c in s:
|
||||||
if lex.peek(i) != c:
|
if lex.peek(i) != c:
|
||||||
@@ -76,14 +70,12 @@ proc matchStr(lex: var Lexer, s: string): bool =
|
|||||||
return true
|
return true
|
||||||
|
|
||||||
proc currentLocation(lex: Lexer): SourceLocation =
|
proc currentLocation(lex: Lexer): SourceLocation =
|
||||||
result = SourceLocation(line: lex.line, column: lex.col, offset: uint32(lex.pos))
|
result = SourceLocation(line: lex.line, column: lex.col, offset: uint32(lex.pos),
|
||||||
|
file: lex.sourceName)
|
||||||
|
|
||||||
proc emitError(lex: var Lexer, loc: SourceLocation, message: string) =
|
proc emitError(lex: var Lexer, loc: SourceLocation, message: string) =
|
||||||
lex.diagnostics.add(LexerDiagnostic(severity: ldsError, loc: loc, message: message))
|
lex.diagnostics.add(LexerDiagnostic(severity: ldsError, loc: loc, message: message))
|
||||||
|
|
||||||
proc emitWarning(lex: var Lexer, loc: SourceLocation, message: string) =
|
|
||||||
lex.diagnostics.add(LexerDiagnostic(severity: ldsWarning, loc: loc, message: message))
|
|
||||||
|
|
||||||
proc makeToken(lex: Lexer, kind: TokenKind, startLoc: SourceLocation, startPos: int): Token =
|
proc makeToken(lex: Lexer, kind: TokenKind, startLoc: SourceLocation, startPos: int): Token =
|
||||||
let text = lex.source[startPos ..< lex.pos]
|
let text = lex.source[startPos ..< lex.pos]
|
||||||
result = Token(kind: kind, text: text, loc: startLoc)
|
result = Token(kind: kind, text: text, loc: startLoc)
|
||||||
@@ -236,6 +228,9 @@ proc scanEscapeSequence(lex: var Lexer): string =
|
|||||||
of 'r': result = "\r"
|
of 'r': result = "\r"
|
||||||
of 't': result = "\t"
|
of 't': result = "\t"
|
||||||
of '0': result = "\0"
|
of '0': result = "\0"
|
||||||
|
of '{', '}':
|
||||||
|
# Preserve \{ and \} so f"..." interpolation can treat them as literal braces
|
||||||
|
result = "\\" & $c
|
||||||
of 'x':
|
of 'x':
|
||||||
var hexVal = ""
|
var hexVal = ""
|
||||||
for _ in 0..<2:
|
for _ in 0..<2:
|
||||||
@@ -337,19 +332,6 @@ proc scanSymbol(lex: var Lexer, startLoc: SourceLocation): Token =
|
|||||||
else:
|
else:
|
||||||
return lex.makeToken(kind1, startLoc, startPos)
|
return lex.makeToken(kind1, startLoc, startPos)
|
||||||
|
|
||||||
template check3(c2: char, kind2: TokenKind, c3: char, kind3: TokenKind, kind1: TokenKind) =
|
|
||||||
if lex.peek() == c2:
|
|
||||||
discard lex.advance()
|
|
||||||
if lex.peek() == c3:
|
|
||||||
discard lex.advance()
|
|
||||||
return lex.makeToken(kind3, startLoc, startPos)
|
|
||||||
return lex.makeToken(kind2, startLoc, startPos)
|
|
||||||
else:
|
|
||||||
return lex.makeToken(kind1, startLoc, startPos)
|
|
||||||
|
|
||||||
template checkEq(c2: char, kind2: TokenKind, kind1: TokenKind) =
|
|
||||||
check2(c2, kind2, kind1)
|
|
||||||
|
|
||||||
case c1
|
case c1
|
||||||
of '(': return lex.makeToken(tkLParen, startLoc, startPos)
|
of '(': return lex.makeToken(tkLParen, startLoc, startPos)
|
||||||
of ')': return lex.makeToken(tkRParen, startLoc, startPos)
|
of ')': return lex.makeToken(tkRParen, startLoc, startPos)
|
||||||
@@ -482,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()
|
||||||
@@ -527,7 +520,10 @@ proc nextToken(lex: var Lexer): Token =
|
|||||||
discard lex.advance()
|
discard lex.advance()
|
||||||
return Token(kind: tkNewLine, text: "\n", loc: startLoc)
|
return Token(kind: tkNewLine, text: "\n", loc: startLoc)
|
||||||
|
|
||||||
# String prefixes: c8" c16" c32" — must come before ident check
|
# String prefixes: f" c8" c16" c32" — must come before ident check
|
||||||
|
if c == 'f' and lex.peek(1) == '"':
|
||||||
|
discard lex.advance() # f
|
||||||
|
return lex.scanString(startLoc, 1)
|
||||||
if c == 'c' and lex.peek(1) in {'8', '1', '3'}:
|
if c == 'c' and lex.peek(1) in {'8', '1', '3'}:
|
||||||
let d = lex.peek(1)
|
let d = lex.peek(1)
|
||||||
if d == '8' and lex.peek(2) == '"':
|
if d == '8' and lex.peek(2) == '"':
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
## LIR — Low-level Intermediate Representation
|
||||||
|
## Linear 3-address code IR, designed for straightforward C emission.
|
||||||
|
## Each HIR construct lowers to 5-30 LIR instructions.
|
||||||
|
|
||||||
|
type
|
||||||
|
LirKind* = enum
|
||||||
|
# ── Data movement ──
|
||||||
|
lirMov ## dst = src
|
||||||
|
lirLoad ## dst = *(base + offset) [type: base elem type]
|
||||||
|
lirStore ## *(base + offset) = src
|
||||||
|
lirLoadGlobal ## dst = global_name
|
||||||
|
|
||||||
|
# ── Arithmetic (3-address, signed) ──
|
||||||
|
lirAdd
|
||||||
|
lirSub
|
||||||
|
lirMul
|
||||||
|
lirDiv
|
||||||
|
lirMod
|
||||||
|
|
||||||
|
# ── Bitwise ──
|
||||||
|
lirAnd
|
||||||
|
lirOr
|
||||||
|
lirXor
|
||||||
|
lirShl
|
||||||
|
lirShr
|
||||||
|
|
||||||
|
# ── Unary ──
|
||||||
|
lirNeg ## dst = -src
|
||||||
|
lirNot ## dst = !src (logical)
|
||||||
|
lirBNot ## dst = ~src (bitwise)
|
||||||
|
|
||||||
|
# ── Comparison (dst = 0 or 1) ──
|
||||||
|
lirCmpEq
|
||||||
|
lirCmpNe
|
||||||
|
lirCmpLt
|
||||||
|
lirCmpLe
|
||||||
|
lirCmpGt
|
||||||
|
lirCmpGe
|
||||||
|
|
||||||
|
# ── Control flow ──
|
||||||
|
lirLabel ## label_name:
|
||||||
|
lirJmp ## goto target
|
||||||
|
lirJz ## if (!cond) goto target
|
||||||
|
lirJnz ## if (cond) goto target
|
||||||
|
|
||||||
|
# ── Calls ──
|
||||||
|
lirCall ## dst = callee(args...)
|
||||||
|
lirCallVoid ## callee(args...) [no return]
|
||||||
|
lirCallIndirect ## dst = (*fn_ptr)(args...)
|
||||||
|
|
||||||
|
# ── Return ──
|
||||||
|
lirRet ## return [val]
|
||||||
|
|
||||||
|
# ── Stack allocation ──
|
||||||
|
lirAlloca ## type dst; (declare a C local)
|
||||||
|
|
||||||
|
# ── Pointers / addressing ──
|
||||||
|
lirAddrOf ## dst = &source_var
|
||||||
|
lirFieldPtr ## dst = &(base.field_name)
|
||||||
|
lirArrowFieldPtr ## dst = &(base->field_name)
|
||||||
|
lirIndexPtr ## dst = &base[idx]
|
||||||
|
lirPtrAdd ## dst = base + offset_bytes (raw pointer arithmetic)
|
||||||
|
|
||||||
|
# ── Type conversion ──
|
||||||
|
lirCast ## dst = (target_type)src
|
||||||
|
|
||||||
|
# ── Composite literals ──
|
||||||
|
lirStructInit ## dst = (StructType){.f1=v1, .f2=v2, ...}
|
||||||
|
lirSliceInit ## dst = (SliceType){.data=arr, .len=n}
|
||||||
|
|
||||||
|
# ── Ternary (convenience; lowered from if-expr) ──
|
||||||
|
lirSelect ## dst = cond ? a : b
|
||||||
|
|
||||||
|
# ── Inline C (for runtime calls that C handles natively) ──
|
||||||
|
lirRawC ## emit raw C line
|
||||||
|
|
||||||
|
# ── Source annotation ──
|
||||||
|
lirComment ## /* text */
|
||||||
|
|
||||||
|
LirValueKind* = enum
|
||||||
|
lvkTemp ## Virtual register / temp (e.g. "%42")
|
||||||
|
lvkVar ## Named variable / parameter
|
||||||
|
lvkInt ## Integer literal
|
||||||
|
lvkFloat ## Float literal
|
||||||
|
lvkString ## String literal (already C-escaped)
|
||||||
|
lvkGlobal ## Global variable / constant name
|
||||||
|
lvkLabel ## Label reference
|
||||||
|
lvkField ## Field name (for struct operations)
|
||||||
|
lvkType ## C type name (for casts, alloca, struct init)
|
||||||
|
lvkVoid ## No value
|
||||||
|
|
||||||
|
LirValue* = object
|
||||||
|
case kind*: LirValueKind
|
||||||
|
of lvkVoid: discard
|
||||||
|
of lvkTemp, lvkVar, lvkGlobal, lvkLabel, lvkField, lvkType, lvkString:
|
||||||
|
strVal*: string
|
||||||
|
of lvkInt:
|
||||||
|
intVal*: int64
|
||||||
|
of lvkFloat:
|
||||||
|
floatVal*: float64
|
||||||
|
|
||||||
|
LirInstr* = object
|
||||||
|
kind*: LirKind
|
||||||
|
dst*: LirValue ## Destination (temp or void)
|
||||||
|
src*: LirValue ## Source operand
|
||||||
|
src2*: LirValue ## Second source operand (for binary ops)
|
||||||
|
extra*: seq[LirValue] ## Extra operands (call args, struct fields, etc.)
|
||||||
|
locLine*: int ## Source line for debug comments (0 = none)
|
||||||
|
locFile*: string ## Source file for debug comments ("" = none)
|
||||||
|
|
||||||
|
# ── Constructor helpers ──
|
||||||
|
|
||||||
|
proc lirTemp*(name: string): LirValue =
|
||||||
|
LirValue(kind: lvkTemp, strVal: name)
|
||||||
|
|
||||||
|
proc lirVar*(name: string): LirValue =
|
||||||
|
LirValue(kind: lvkVar, strVal: name)
|
||||||
|
|
||||||
|
proc lirInt*(v: int64): LirValue =
|
||||||
|
LirValue(kind: lvkInt, intVal: v)
|
||||||
|
|
||||||
|
proc lirFloatLit*(v: float64): LirValue =
|
||||||
|
LirValue(kind: lvkFloat, floatVal: v)
|
||||||
|
|
||||||
|
proc lirStr*(v: string): LirValue =
|
||||||
|
## Already C-escaped and quoted string
|
||||||
|
LirValue(kind: lvkString, strVal: v)
|
||||||
|
|
||||||
|
proc lirGlobal*(name: string): LirValue =
|
||||||
|
LirValue(kind: lvkGlobal, strVal: name)
|
||||||
|
|
||||||
|
proc lirLabel*(name: string): LirValue =
|
||||||
|
LirValue(kind: lvkLabel, strVal: name)
|
||||||
|
|
||||||
|
proc lirField*(name: string): LirValue =
|
||||||
|
LirValue(kind: lvkField, strVal: name)
|
||||||
|
|
||||||
|
proc lirType*(name: string): LirValue =
|
||||||
|
LirValue(kind: lvkType, strVal: name)
|
||||||
|
|
||||||
|
proc lirVoid*(): LirValue =
|
||||||
|
LirValue(kind: lvkVoid)
|
||||||
|
|
||||||
|
proc `$`*(v: LirValue): string =
|
||||||
|
case v.kind
|
||||||
|
of lvkVoid: "void"
|
||||||
|
of lvkTemp: "%" & v.strVal
|
||||||
|
of lvkVar: v.strVal
|
||||||
|
of lvkInt: $v.intVal
|
||||||
|
of lvkFloat: $v.floatVal
|
||||||
|
of lvkString: v.strVal
|
||||||
|
of lvkGlobal: "@" & v.strVal
|
||||||
|
of lvkLabel: ":" & v.strVal
|
||||||
|
of lvkField: "." & v.strVal
|
||||||
|
of lvkType: "<" & v.strVal & ">"
|
||||||
|
|
||||||
|
# ── LIR function ──
|
||||||
|
|
||||||
|
type
|
||||||
|
LirFunc* = object
|
||||||
|
name*: string
|
||||||
|
params*: seq[tuple[name: string, cType: string]]
|
||||||
|
retType*: string ## C return type ("int", "void", etc.)
|
||||||
|
instrs*: seq[LirInstr]
|
||||||
|
isPublic*: bool
|
||||||
|
|
||||||
|
LirModule* = object
|
||||||
|
funcs*: seq[LirFunc]
|
||||||
|
globals*: seq[tuple[name: string, cType: string, initVal: string]]
|
||||||
|
structDefs*: seq[string] ## Raw C struct typedef strings
|
||||||
|
enumDefs*: seq[string] ## Raw C enum typedef strings
|
||||||
|
externs*: seq[string] ## Raw C extern declarations
|
||||||
|
includes*: seq[string] ## #include <...>
|
||||||
|
preamble*: string ## Raw C code at top of file
|
||||||
|
|
||||||
|
# ── Builder context (temp counter, label counter) ──
|
||||||
|
|
||||||
|
type
|
||||||
|
LirBuilder* = object
|
||||||
|
funcs*: seq[LirFunc]
|
||||||
|
tempCounter*: int
|
||||||
|
labelCounter*: int
|
||||||
|
commentLine*: int
|
||||||
|
commentFile*: string
|
||||||
|
## Current function being built
|
||||||
|
curFunc*: LirFunc
|
||||||
|
curFuncActive*: bool
|
||||||
|
|
||||||
|
proc initLirBuilder*(): LirBuilder =
|
||||||
|
result = LirBuilder()
|
||||||
|
result.tempCounter = 0
|
||||||
|
result.labelCounter = 0
|
||||||
|
|
||||||
|
proc freshTemp*(b: var LirBuilder): LirValue =
|
||||||
|
inc b.tempCounter
|
||||||
|
result = lirTemp("_t" & $b.tempCounter)
|
||||||
|
|
||||||
|
proc freshLabel*(b: var LirBuilder, prefix: string = "L"): LirValue =
|
||||||
|
inc b.labelCounter
|
||||||
|
result = lirLabel(prefix & $b.labelCounter)
|
||||||
|
|
||||||
|
# ── Instruction emitters ──
|
||||||
|
|
||||||
|
proc emit*(b: var LirBuilder, instr: LirInstr) =
|
||||||
|
var i = instr
|
||||||
|
if b.commentLine > 0:
|
||||||
|
i.locLine = b.commentLine
|
||||||
|
i.locFile = b.commentFile
|
||||||
|
b.curFunc.instrs.add(i)
|
||||||
|
|
||||||
|
proc emitMov*(b: var LirBuilder, dst, src: LirValue) =
|
||||||
|
b.emit(LirInstr(kind: lirMov, dst: dst, src: src))
|
||||||
|
|
||||||
|
proc emitLoad*(b: var LirBuilder, dst, base: LirValue, offset: int = 0) =
|
||||||
|
b.emit(LirInstr(kind: lirLoad, dst: dst, src: base, src2: lirInt(offset)))
|
||||||
|
|
||||||
|
proc emitStore*(b: var LirBuilder, base: LirValue, src: LirValue, offset: int = 0) =
|
||||||
|
b.emit(LirInstr(kind: lirStore, src: src, src2: base, dst: lirInt(offset)))
|
||||||
|
|
||||||
|
proc emitBinOp*(b: var LirBuilder, op: LirKind, dst, a, bl: LirValue) =
|
||||||
|
b.emit(LirInstr(kind: op, dst: dst, src: a, src2: bl))
|
||||||
|
|
||||||
|
proc emitUnary*(b: var LirBuilder, op: LirKind, dst, src: LirValue) =
|
||||||
|
b.emit(LirInstr(kind: op, dst: dst, src: src))
|
||||||
|
|
||||||
|
proc emitCmp*(b: var LirBuilder, op: LirKind, dst, a, bl: LirValue) =
|
||||||
|
b.emit(LirInstr(kind: op, dst: dst, src: a, src2: bl))
|
||||||
|
|
||||||
|
proc emitLabel*(b: var LirBuilder, label: LirValue) =
|
||||||
|
b.emit(LirInstr(kind: lirLabel, src: label))
|
||||||
|
|
||||||
|
proc emitJmp*(b: var LirBuilder, target: LirValue) =
|
||||||
|
b.emit(LirInstr(kind: lirJmp, src: target))
|
||||||
|
|
||||||
|
proc emitJz*(b: var LirBuilder, target, cond: LirValue) =
|
||||||
|
b.emit(LirInstr(kind: lirJz, src: target, src2: cond))
|
||||||
|
|
||||||
|
proc emitJnz*(b: var LirBuilder, target, cond: LirValue) =
|
||||||
|
b.emit(LirInstr(kind: lirJnz, src: target, src2: cond))
|
||||||
|
|
||||||
|
proc emitCall*(b: var LirBuilder, dst: LirValue, callee: string, args: seq[LirValue]) =
|
||||||
|
b.emit(LirInstr(kind: lirCall, dst: dst, src: lirGlobal(callee), extra: args))
|
||||||
|
|
||||||
|
proc emitCallVoid*(b: var LirBuilder, callee: string, args: seq[LirValue]) =
|
||||||
|
b.emit(LirInstr(kind: lirCallVoid, dst: lirVoid(), src: lirGlobal(callee), extra: args))
|
||||||
|
|
||||||
|
proc emitRet*(b: var LirBuilder, val: LirValue = lirVoid()) =
|
||||||
|
b.emit(LirInstr(kind: lirRet, src: val))
|
||||||
|
|
||||||
|
proc emitAlloca*(b: var LirBuilder, name: string, cType: string) =
|
||||||
|
b.emit(LirInstr(kind: lirAlloca, dst: lirVar(name), src: lirType(cType)))
|
||||||
|
|
||||||
|
proc emitAddrOf*(b: var LirBuilder, dst, src: LirValue) =
|
||||||
|
b.emit(LirInstr(kind: lirAddrOf, dst: dst, src: src))
|
||||||
|
|
||||||
|
proc emitFieldPtr*(b: var LirBuilder, dst, base: LirValue, field: string) =
|
||||||
|
b.emit(LirInstr(kind: lirFieldPtr, dst: dst, src: base, src2: lirField(field)))
|
||||||
|
|
||||||
|
proc emitArrowFieldPtr*(b: var LirBuilder, dst, base: LirValue, field: string) =
|
||||||
|
b.emit(LirInstr(kind: lirArrowFieldPtr, dst: dst, src: base, src2: lirField(field)))
|
||||||
|
|
||||||
|
proc emitIndexPtr*(b: var LirBuilder, dst, base, idx: LirValue) =
|
||||||
|
b.emit(LirInstr(kind: lirIndexPtr, dst: dst, src: base, src2: idx))
|
||||||
|
|
||||||
|
proc emitPtrAdd*(b: var LirBuilder, dst, base, offset: LirValue) =
|
||||||
|
b.emit(LirInstr(kind: lirPtrAdd, dst: dst, src: base, src2: offset))
|
||||||
|
|
||||||
|
proc emitCast*(b: var LirBuilder, dst, src: LirValue, targetType: string) =
|
||||||
|
b.emit(LirInstr(kind: lirCast, dst: dst, src: src, src2: lirType(targetType)))
|
||||||
|
|
||||||
|
proc emitStructInit*(b: var LirBuilder, dst: LirValue, structType: string,
|
||||||
|
fields: seq[tuple[name: string, val: LirValue]]) =
|
||||||
|
var extras: seq[LirValue] = @[lirType(structType)]
|
||||||
|
for f in fields:
|
||||||
|
extras.add(lirField(f.name))
|
||||||
|
extras.add(f.val)
|
||||||
|
b.emit(LirInstr(kind: lirStructInit, dst: dst, extra: extras))
|
||||||
|
|
||||||
|
proc emitSliceInit*(b: var LirBuilder, dst: LirValue, elemType: string,
|
||||||
|
dataPtr: LirValue, length: LirValue) =
|
||||||
|
b.emit(LirInstr(kind: lirSliceInit, dst: dst, src: dataPtr, src2: length,
|
||||||
|
extra: @[lirType(elemType)]))
|
||||||
|
|
||||||
|
proc emitSelect*(b: var LirBuilder, dst, cond, thenVal, elseVal: LirValue) =
|
||||||
|
b.emit(LirInstr(kind: lirSelect, dst: dst, src: cond, src2: thenVal,
|
||||||
|
extra: @[elseVal]))
|
||||||
|
|
||||||
|
proc emitRawC*(b: var LirBuilder, code: string) =
|
||||||
|
b.emit(LirInstr(kind: lirRawC, src: lirStr(code)))
|
||||||
|
|
||||||
|
proc emitComment*(b: var LirBuilder, text: string) =
|
||||||
|
b.emit(LirInstr(kind: lirComment, src: lirStr(text)))
|
||||||
|
|
||||||
|
# ── Function management ──
|
||||||
|
|
||||||
|
proc beginFunc*(b: var LirBuilder, name: string, params: seq[tuple[name: string, cType: string]],
|
||||||
|
retType: string, isPublic: bool = true) =
|
||||||
|
if b.curFuncActive:
|
||||||
|
b.funcs.add(b.curFunc)
|
||||||
|
b.curFunc = LirFunc(name: name, params: params, retType: retType,
|
||||||
|
isPublic: isPublic)
|
||||||
|
b.curFuncActive = true
|
||||||
|
|
||||||
|
proc endFunc*(b: var LirBuilder) =
|
||||||
|
if b.curFuncActive:
|
||||||
|
b.funcs.add(b.curFunc)
|
||||||
|
b.curFuncActive = false
|
||||||
|
b.curFunc = LirFunc()
|
||||||
|
|
||||||
|
proc setSourceLoc*(b: var LirBuilder, line: int, file: string) =
|
||||||
|
b.commentLine = line
|
||||||
|
b.commentFile = file
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user