From 323121f80c2ca106959b33b51499bc332f814b2c Mon Sep 17 00:00:00 2001
From: LunaStev
Date: Mon, 7 Sep 2026 14:50:18 +0900
Subject: [PATCH 1/3] fix(ci): provision native Windows ARM64 LLVM XML
dependency
Signed-off-by: LunaStev
---
.github/workflows/cases.yml | 4 +
.github/workflows/release.yml | 6 +-
.github/workflows/rust.yml | 8 ++
CONTRIBUTING.md | 26 +++++++
tools/provision_windows_arm64_libxml2.ps1 | 94 +++++++++++++++++++++++
tools/test_windows_arm64_libxml2.ps1 | 49 ++++++++++++
x.py | 5 ++
7 files changed, 191 insertions(+), 1 deletion(-)
create mode 100644 tools/provision_windows_arm64_libxml2.ps1
create mode 100644 tools/test_windows_arm64_libxml2.ps1
diff --git a/.github/workflows/cases.yml b/.github/workflows/cases.yml
index 9fb023a9..f5006844 100644
--- a/.github/workflows/cases.yml
+++ b/.github/workflows/cases.yml
@@ -322,6 +322,10 @@ jobs:
"$llvmDirectory\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
"$mingwRoot\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
+ - name: Provision pinned ARM64 libxml2 for LLVM
+ shell: pwsh
+ run: ./tools/provision_windows_arm64_libxml2.ps1
+
- name: Verify native ARM64 toolchains
shell: pwsh
run: |
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 04892292..cb447afd 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1001,6 +1001,10 @@ jobs:
) | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
"$llvmDirectory\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
+ - name: Provision pinned ARM64 libxml2 for LLVM
+ shell: pwsh
+ run: ./tools/provision_windows_arm64_libxml2.ps1
+
- name: Verify native ARM64 toolchains
shell: pwsh
run: |
@@ -1042,7 +1046,7 @@ jobs:
$source = Join-Path $temporary "smoke.wave"
$outputDirectory = Join-Path $temporary "output"
'fun main() { println("release smoke"); }' | Set-Content -Encoding utf8 $source
- $output = & $compiler run $source --out-dir $outputDirectory
+ $output = & $compiler build $source --run --out-dir $outputDirectory
if ($LASTEXITCODE -ne 0 -or $output -ne "release smoke") {
throw "Windows ARM64 packaged compiler smoke test failed: $output"
}
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index de9ea50b..53842baa 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -82,6 +82,10 @@ jobs:
python3 -m py_compile x.py tools/check_wave_corpus.py tools/case_manifest.py tools/populate_case_matrix.py tools/run_tests.py tools/test_contracts.py tools/test_case_manifest.py tools/test_test_contracts.py
python3 -m unittest tools.test_case_manifest tools.test_test_contracts
+ - name: Test Windows ARM64 dependency archive validation
+ shell: pwsh
+ run: ./tools/test_windows_arm64_libxml2.ps1
+
- name: Build release compiler
run: cargo build --locked --release --verbose
@@ -872,6 +876,10 @@ jobs:
) | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
"$directory\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
+ - name: Provision pinned ARM64 libxml2 for LLVM
+ shell: pwsh
+ run: ./tools/provision_windows_arm64_libxml2.ps1
+
- name: Verify native toolchain
shell: pwsh
run: |
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 59a453c8..e6444c31 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -111,6 +111,32 @@ Notes:
- Wave language corpus / std examples are checked with `tools/check_wave_corpus.py`
after a release `wavec` build.
+### Native Windows ARM64 LLVM dependency
+
+The official LLVM 21.1.8 ARM64 MSVC SDK lists `xml2s.lib` in
+`llvm-config --system-libs --link-static` without shipping the library.
+`llvm-sys` 211 rejects dynamic LLVM linking on MSVC, so `prefer-dynamic`
+still falls back to this static dependency.
+
+The build, cases, and release workflows run
+`tools/provision_windows_arm64_libxml2.ps1` after installing LLVM. It builds
+libxml2 2.13.9 from a SHA-256-pinned
+[GNOME source archive](https://download.gnome.org/sources/libxml2/2.13/),
+retaining the pre-2.14 XML ABI used by LLVM's static code. The build uses
+native ARM64 clang-cl/MSVC tools, the DLL CRT used by default Rust MSVC
+builds, and no optional iconv, compression, Python, or XML DLL dependencies.
+It supplies the SDK's `xml2s.lib` name and the Windows `bcrypt`/`ws2_32`
+imports, then checks every COFF archive member for machine `0xaa64` before
+Cargo links it. Release packaging includes the libxml2 copyright notice.
+The upstream LLVM release configuration is available in
+[the LLVM 21.1.8 release script](https://github.com/llvm/llvm-project/blob/llvmorg-21.1.8/llvm/utils/release/build_llvm_release.bat).
+
+With LLVM tools and PowerShell available, run
+`pwsh -NoProfile -File tools/test_windows_arm64_libxml2.ps1` to check the
+script syntax and its rejection of x64, mixed, and empty archives. This
+portable check complements the actual native Windows ARM64 build and cases;
+it does not replace them.
+
### 4.1 Patch Verification (Maintainers Only)
Maintainers must verify incoming email patches using:
diff --git a/tools/provision_windows_arm64_libxml2.ps1 b/tools/provision_windows_arm64_libxml2.ps1
new file mode 100644
index 00000000..45ed3d95
--- /dev/null
+++ b/tools/provision_windows_arm64_libxml2.ps1
@@ -0,0 +1,94 @@
+# This file is part of the Wave language project.
+# SPDX-License-Identifier: MPL-2.0
+# AI TRAINING NOTICE: Prohibited without prior written permission.
+
+# llvm-sys 211 cannot dynamically link LLVM on MSVC, even with prefer-dynamic.
+# The official LLVM ARM64 SDK requests xml2s.lib through --system-libs, but
+# does not ship it. Build an ABI-compatible libxml2 2.13 static library from
+# checksum-pinned GNOME sources; never substitute an x64 library.
+$ErrorActionPreference = "Stop"
+Set-StrictMode -Version Latest
+
+function Invoke-Checked([string]$Program, [string[]]$Arguments) {
+ & $Program @Arguments
+ if ($LASTEXITCODE -ne 0) { throw "$Program failed with exit code $LASTEXITCODE" }
+}
+
+function Assert-Arm64Archive([string]$Library, [string]$Readobj) {
+ # llvm-readobj visits every COFF member, so mixed-architecture archives fail
+ # here before Cargo/MSVC reaches a confusing unresolved-symbol diagnostic.
+ $headers = & $Readobj --file-headers $Library
+ if ($LASTEXITCODE -ne 0) { throw "Could not inspect libxml2 archive" }
+ $machines = @($headers | Select-String '^\s*Machine:')
+ if ($machines.Count -eq 0) { throw "No COFF objects found in libxml2 archive" }
+ foreach ($machine in $machines) {
+ if ($machine.Line -notmatch 'Machine: IMAGE_FILE_MACHINE_ARM64 \(0xAA64\)') {
+ throw "Non-ARM64 member in libxml2 archive: $machine"
+ }
+ }
+ Write-Host "Verified $($machines.Count) ARM64 libxml2 objects"
+}
+
+if ($env:PROCESSOR_ARCHITECTURE -ne "ARM64") { throw "Expected a native ARM64 runner" }
+$version = "2.13.9"
+$sha256 = "a2c9ae7b770da34860050c309f903221c67830c86e4a7e760692b803df95143a"
+$root = Join-Path $env:RUNNER_TEMP "wave-libxml2-arm64-$version"
+$archive = Join-Path $root "libxml2-$version.tar.xz"
+$source = Join-Path $root "libxml2-$version"
+$build = Join-Path $root "build"
+$install = Join-Path $root "install"
+New-Item -ItemType Directory -Force -Path $root | Out-Null
+Invoke-Checked "curl.exe" @("--fail", "--location", "--retry", "3",
+ "https://download.gnome.org/sources/libxml2/2.13/libxml2-$version.tar.xz", "--output", $archive)
+if ((Get-FileHash $archive -Algorithm SHA256).Hash.ToLowerInvariant() -ne $sha256) {
+ throw "libxml2 source checksum mismatch"
+}
+Invoke-Checked "tar.exe" @("-xJf", $archive, "-C", $root)
+
+# Import the native ARM64 MSVC/Windows SDK environment for clang-cl and Ninja.
+$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"
+$visualStudio = & $vswhere -latest -products '*' -property installationPath
+if ($LASTEXITCODE -ne 0 -or -not $visualStudio) { throw "Visual Studio was not found" }
+$devCmd = Join-Path $visualStudio "Common7\Tools\VsDevCmd.bat"
+$environment = & cmd.exe /d /s /c "`"$devCmd`" -no_logo -arch=arm64 -host_arch=arm64 >nul && set"
+if ($LASTEXITCODE -ne 0) { throw "Could not initialize native ARM64 MSVC" }
+foreach ($line in $environment) {
+ if ($line -match '^([^=]+)=(.*)$') {
+ [Environment]::SetEnvironmentVariable($Matches[1], $Matches[2], "Process")
+ }
+}
+if ($env:VSCMD_ARG_TGT_ARCH -ne "arm64") { throw "MSVC is not targeting ARM64" }
+
+$llvmBin = Split-Path $env:LLVM_CONFIG_PATH
+$clang = Join-Path $llvmBin "clang-cl.exe"
+$systemLibraryOutput = & $env:LLVM_CONFIG_PATH --system-libs --link-static
+if ($LASTEXITCODE -ne 0) { throw "llvm-config --system-libs failed" }
+$systemLibraries = $systemLibraryOutput -join " "
+Write-Host "LLVM static system libraries: $systemLibraries"
+if ($systemLibraries -notmatch '\bxml2s\.lib\b') {
+ throw "The pinned LLVM SDK no longer requests xml2s.lib; review this provisioning contract"
+}
+
+# /MD matches Rust's default MSVC CRT. No zlib, lzma, iconv, Python or DLL
+# dependencies are introduced. Keep the pre-2.14 XML ABI used by LLVM 21.
+Invoke-Checked "cmake" @("-S", $source, "-B", $build, "-G", "Ninja",
+ "-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_INSTALL_PREFIX=$install",
+ "-DCMAKE_C_COMPILER=$clang", "-DCMAKE_C_COMPILER_TARGET=aarch64-pc-windows-msvc",
+ "-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL", "-DBUILD_SHARED_LIBS=OFF",
+ "-DLIBXML2_WITH_ICONV=OFF", "-DLIBXML2_WITH_ICU=OFF", "-DLIBXML2_WITH_LZMA=OFF",
+ "-DLIBXML2_WITH_ZLIB=OFF", "-DLIBXML2_WITH_PYTHON=OFF", "-DLIBXML2_WITH_PROGRAMS=OFF",
+ "-DLIBXML2_WITH_TESTS=OFF", "-DLIBXML2_WITH_FTP=OFF", "-DLIBXML2_WITH_HTTP=OFF",
+ "-DLIBXML2_WITH_MODULES=OFF", "-DLIBXML2_WITH_TLS=OFF")
+Invoke-Checked "cmake" @("--build", $build, "--config", "Release", "--parallel", "2")
+Invoke-Checked "cmake" @("--install", $build, "--config", "Release")
+$libDir = Join-Path $install "lib"
+$library = Join-Path $libDir "xml2s.lib"
+Copy-Item (Join-Path $libDir "libxml2s.lib") $library -Force
+
+Assert-Arm64Archive $library (Join-Path $llvmBin "llvm-readobj.exe")
+
+# llvm-sys supplies xml2s; libxml2's Windows entropy/socket helpers also use
+# these Windows SDK import libraries. Preserve pre-existing Rust flags.
+"LIB=$libDir;$env:LIB" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append
+"RUSTFLAGS=$env:RUSTFLAGS -l bcrypt -l ws2_32" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append
+"WAVE_LIBXML2_LICENSE=$source\Copyright" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append
diff --git a/tools/test_windows_arm64_libxml2.ps1 b/tools/test_windows_arm64_libxml2.ps1
new file mode 100644
index 00000000..c0c1f340
--- /dev/null
+++ b/tools/test_windows_arm64_libxml2.ps1
@@ -0,0 +1,49 @@
+# This file is part of the Wave language project.
+# SPDX-License-Identifier: MPL-2.0
+# AI TRAINING NOTICE: Prohibited without prior written permission.
+
+# Test the provisioning script's archive guard using real cross-compiled COFF
+# objects without downloading libxml2 or requiring a Windows build host.
+$ErrorActionPreference = "Stop"
+Set-StrictMode -Version Latest
+$script = Join-Path $PSScriptRoot "provision_windows_arm64_libxml2.ps1"
+$tokens = $null
+$parseErrors = $null
+$ast = [System.Management.Automation.Language.Parser]::ParseFile($script, [ref]$tokens, [ref]$parseErrors)
+if ($parseErrors.Count) { throw ($parseErrors | Out-String) }
+foreach ($name in @("Assert-Arm64Archive", "Invoke-Checked")) {
+ $definition = $ast.Find({ param($node)
+ $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq $name
+ }, $false)
+ if (-not $definition) { throw "Missing $name in provisioning script" }
+ Invoke-Expression $definition.Extent.Text
+}
+
+$directory = Join-Path ([IO.Path]::GetTempPath()) ("wave-arm64-archive-test-" + [guid]::NewGuid())
+New-Item -ItemType Directory $directory | Out-Null
+try {
+ $source = Join-Path $directory "probe.c"
+ 'int probe(void) { return 42; }' | Set-Content -Encoding ascii $source
+ $arm = Join-Path $directory "arm64.obj"
+ $x64 = Join-Path $directory "x64.obj"
+ Invoke-Checked "clang" @("--target=aarch64-pc-windows-msvc", "-c", $source, "-o", $arm)
+ Invoke-Checked "clang" @("--target=x86_64-pc-windows-msvc", "-c", $source, "-o", $x64)
+ $valid = Join-Path $directory "valid.lib"
+ $foreign = Join-Path $directory "x64.lib"
+ $mixed = Join-Path $directory "mixed.lib"
+ $empty = Join-Path $directory "empty.lib"
+ Invoke-Checked "llvm-ar" @("rcs", $valid, $arm)
+ Invoke-Checked "llvm-ar" @("rcs", $foreign, $x64)
+ Invoke-Checked "llvm-ar" @("rcs", $mixed, $arm, $x64)
+ Invoke-Checked "llvm-ar" @("rcs", $empty)
+ Assert-Arm64Archive $valid "llvm-readobj"
+ foreach ($invalid in @($foreign, $mixed, $empty)) {
+ $rejected = $false
+ try { Assert-Arm64Archive $invalid "llvm-readobj" }
+ catch { $rejected = $true }
+ if (-not $rejected) { throw "Archive guard accepted $invalid" }
+ }
+ Write-Host "ARM64 archive guard passed valid, x64, mixed, and empty archive cases"
+} finally {
+ Remove-Item -Recurse -Force $directory
+}
diff --git a/x.py b/x.py
index 281259c1..bdbff768 100644
--- a/x.py
+++ b/x.py
@@ -1237,6 +1237,11 @@ def stage_release_package(target, binary, out_name):
sys.exit(1)
if target == WINDOWS_ARM64_HOST_TARGET:
copy_windows_arm64_mingw_toolchain(stage_dir, target)
+ libxml2_license = os.environ.get("WAVE_LIBXML2_LICENSE")
+ if libxml2_license:
+ license_dir = stage_dir / "licenses"
+ license_dir.mkdir(exist_ok=True)
+ shutil.copy2(libxml2_license, license_dir / "libxml2.txt")
else:
copy_windows_mingw_self_contained_libs(stage_dir, target)
patch_staged_runtime(stage_dir, target, staged_binary, lld_tools)
From b5312052c2bedbf4f05434f6f15f914e60400d1c Mon Sep 17 00:00:00 2001
From: LunaStev
Date: Mon, 7 Sep 2026 14:50:42 +0900
Subject: [PATCH 2/3] fix(codegen): preserve projected pointer types and target
feature isolation
Signed-off-by: LunaStev
---
.github/workflows/rust.yml | 13 +-
llvm/src/expression/rvalue/dispatch.rs | 2 +-
llvm/src/expression/rvalue/pointers.rs | 214 +++----------------------
tests/cases/shared/test103.wave | 41 +++++
tests/codegen_regressions.rs | 64 +++++++-
5 files changed, 132 insertions(+), 202 deletions(-)
create mode 100644 tests/cases/shared/test103.wave
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 53842baa..059a0067 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -332,9 +332,9 @@ jobs:
cargo build --locked --no-default-features
--features llvm-target-aarch64 --jobs 2
- - name: Build with the core 64-bit LLVM feature set
+ - name: Run Rust tests with the core 64-bit LLVM feature set
run: >-
- cargo build --locked --no-default-features
+ cargo test --locked --all-targets --no-default-features
--features llvm-target-core64 --jobs 2
- name: Verify bundled Linux CRT matrix
@@ -457,10 +457,13 @@ jobs:
- name: Build release compiler
run: cargo build --locked --release --jobs 2
- - name: Build LoongArch-only LLVM feature set
+ - name: Run LoongArch-only LLVM regression tests
+ env:
+ WAVE_RUN_LOONGARCH64_INTEROP_TESTS: "1"
run: >-
- cargo build --locked --no-default-features
- --features llvm-target-loongarch --jobs 2
+ cargo test --locked --no-default-features
+ --features llvm-target-loongarch --test codegen_regressions
+ loongarch64_ --jobs 2
- name: Verify LoongArch64 CRT and target contracts
env:
diff --git a/llvm/src/expression/rvalue/dispatch.rs b/llvm/src/expression/rvalue/dispatch.rs
index ae3717c2..8a248b45 100644
--- a/llvm/src/expression/rvalue/dispatch.rs
+++ b/llvm/src/expression/rvalue/dispatch.rs
@@ -37,7 +37,7 @@ pub(crate) fn gen_expr<'ctx, 'a>(
}
}
- Expression::Deref(inner) => pointers::gen_deref(env, inner, expected_type),
+ Expression::Deref(inner) => pointers::gen_deref(env, inner),
Expression::AddressOf(inner) => pointers::gen_addressof(env, inner, expected_type),
Expression::MethodCall { object, name, args } => {
diff --git a/llvm/src/expression/rvalue/pointers.rs b/llvm/src/expression/rvalue/pointers.rs
index 4852f663..87f9c0ed 100644
--- a/llvm/src/expression/rvalue/pointers.rs
+++ b/llvm/src/expression/rvalue/pointers.rs
@@ -13,195 +13,29 @@
//! Address-of and dereference expression lowering.
//!
//! LLVM pointers are opaque, so dereference loads recover their value type from
-//! the Wave expression and struct tables. Address-of returns the existing lvalue
+//! typed HIR and the projected storage type. Address-of returns the existing lvalue
//! address and never allocates replacement storage.
use super::ExprGenEnv;
use crate::codegen::types::{wave_type_to_llvm_type, TypeFlavor};
use crate::codegen::{generate_address_and_type_ir, generate_address_ir};
use crate::statement::variable::{coerce_basic_value, wave_type_is_unsigned, CoercionMode};
-use inkwell::types::AsTypeRef;
use inkwell::types::{BasicType, BasicTypeEnum};
use inkwell::values::{BasicValue, BasicValueEnum};
use parser::ast::{Expression, WaveType};
-#[allow(dead_code)]
-fn push_deref_into_base(expr: &Expression) -> Expression {
- match expr {
- Expression::Grouped(inner) => Expression::Grouped(Box::new(push_deref_into_base(inner))),
- Expression::IndexAccess { target, index } => Expression::IndexAccess {
- target: Box::new(push_deref_into_base(target)),
- index: index.clone(),
- },
- Expression::FieldAccess { object, field } => Expression::FieldAccess {
- object: Box::new(push_deref_into_base(object)),
- field: field.clone(),
- },
- other => Expression::Deref(Box::new(other.clone())),
- }
-}
-
-fn normalize_struct_name(raw: &str) -> &str {
- raw.strip_prefix("struct.")
- .unwrap_or(raw)
- .trim_start_matches('%')
-}
-
-fn resolve_struct_key<'ctx>(
- st: inkwell::types::StructType<'ctx>,
- struct_types: &std::collections::HashMap>,
-) -> Option {
- if let Some(raw) = st.get_name().and_then(|n| n.to_str().ok()) {
- return Some(normalize_struct_name(raw).to_string());
- }
-
- let st_ref = st.as_type_ref();
- for (name, ty) in struct_types {
- if ty.as_type_ref() == st_ref {
- return Some(name.clone());
- }
- }
-
- None
-}
-
-fn basic_ty_to_wave_ty<'ctx>(
- ty: BasicTypeEnum<'ctx>,
- struct_types: &std::collections::HashMap>,
-) -> Option {
- match ty {
- BasicTypeEnum::IntType(it) => {
- let bw = it.get_bit_width() as u16;
- if bw == 1 {
- Some(WaveType::Bool)
- } else {
- Some(WaveType::Int(bw))
- }
- }
- BasicTypeEnum::FloatType(ft) => Some(WaveType::Float(ft.get_bit_width() as u16)),
- BasicTypeEnum::PointerType(_) => Some(WaveType::Pointer(Box::new(WaveType::Byte))),
- BasicTypeEnum::ArrayType(at) => {
- let elem = basic_ty_to_wave_ty(at.get_element_type(), struct_types)?;
- Some(WaveType::Array(Box::new(elem), at.len()))
- }
- BasicTypeEnum::StructType(st) => {
- let name = resolve_struct_key(st, struct_types)?;
- Some(WaveType::Struct(name))
- }
- _ => None,
- }
-}
-
-fn infer_wave_type_of_expr<'ctx, 'a>(
- env: &mut ExprGenEnv<'ctx, 'a>,
- expr: &Expression,
-) -> Option {
- match expr {
- Expression::Variable(name) => env.variables.get(name).map(|vi| vi.ty.clone()),
-
- Expression::Grouped(inner) => infer_wave_type_of_expr(env, inner),
-
- Expression::AddressOf(inner) => {
- let inner_ty = infer_wave_type_of_expr(env, inner)?;
- Some(WaveType::Pointer(Box::new(inner_ty)))
- }
-
- Expression::Deref(inner) => {
- let inner_ty = infer_wave_type_of_expr(env, inner)?;
- if matches!(
- inner.as_ref(),
- Expression::IndexAccess { .. } | Expression::FieldAccess { .. }
- ) {
- return Some(inner_ty);
- }
- match inner_ty {
- WaveType::Pointer(t) => Some(*t),
- WaveType::String => Some(WaveType::Byte),
- _ => None,
- }
- }
-
- Expression::IndexAccess { target, .. } => {
- let target_ty = infer_wave_type_of_expr(env, target)?;
- match target_ty {
- WaveType::Array(inner, _) => Some(*inner),
- WaveType::Pointer(inner) => match *inner {
- WaveType::Array(elem, _) => Some(*elem),
- other => Some(other),
- },
- WaveType::String => Some(WaveType::Byte),
- _ => None,
- }
- }
-
- Expression::FieldAccess { object, field } => {
- let full = Expression::FieldAccess {
- object: Box::new((**object).clone()),
- field: field.clone(),
- };
- let (_, field_ty) = generate_address_and_type_ir(
- env.context,
- env.builder,
- env.program,
- &full,
- env.variables,
- env.module,
- env.struct_types,
- env.struct_field_indices,
- );
- basic_ty_to_wave_ty(field_ty, env.struct_types)
- }
-
- _ => None,
- }
-}
-
-fn infer_deref_load_ty<'ctx, 'a>(
- env: &mut ExprGenEnv<'ctx, 'a>,
- inner_expr: &Expression,
- expected_type: Option>,
-) -> BasicTypeEnum<'ctx> {
- if let Some(t) = expected_type {
- return t;
- }
-
- let inferred = infer_wave_type_of_expr(env, inner_expr).unwrap_or_else(|| {
- panic!(
- "deref needs expected_type in opaque-pointer mode (cannot infer load type from {:?})",
- inner_expr
- )
- });
-
- match inferred {
- WaveType::Pointer(inner) => {
- wave_type_to_llvm_type(env.context, &inner, env.struct_types, TypeFlavor::Value)
- }
- WaveType::String => env.context.i8_type().as_basic_type_enum(),
- other => match inner_expr {
- // Preserve legacy behavior for lvalues like `deref visited[x]`.
- Expression::IndexAccess { .. }
- | Expression::FieldAccess { .. }
- | Expression::Grouped(_) => {
- wave_type_to_llvm_type(env.context, &other, env.struct_types, TypeFlavor::Value)
- }
- _ => panic!("deref expects pointer type, got {:?}", other),
- },
- }
-}
-
pub(crate) fn gen_deref<'ctx, 'a>(
env: &mut ExprGenEnv<'ctx, 'a>,
inner_expr: &Expression,
- expected_type: Option>,
) -> BasicValueEnum<'ctx> {
- let load_ty = infer_deref_load_ty(env, inner_expr, expected_type);
-
match inner_expr {
- Expression::Grouped(inner) => return gen_deref(env, inner, Some(load_ty)),
+ Expression::Grouped(inner) => return gen_deref(env, inner),
- // lvalue (x[i], x.field) -> address -> typed load
+ // Wave's projected deref reads the field/element itself. Its storage
+ // type may be a pointer; do not strip another pointer layer or infer
+ // the load width from the surrounding expression's expected type.
Expression::IndexAccess { .. } | Expression::FieldAccess { .. } => {
- let addr = generate_address_ir(
+ let (addr, load_ty) = generate_address_and_type_ir(
env.context,
env.builder,
env.program,
@@ -211,31 +45,25 @@ pub(crate) fn gen_deref<'ctx, 'a>(
env.struct_types,
env.struct_field_indices,
);
-
- return env
- .builder
- .build_load(load_ty, addr, "deref_load")
- .unwrap()
- .as_basic_value_enum();
+ return env.builder.build_load(load_ty, addr, "deref_load").unwrap();
}
-
_ => {}
}
- // pointer value -> typed load
- let v = env.gen(inner_expr, None);
- if let BasicValueEnum::PointerValue(p) = v {
- return env
- .builder
- .build_load(load_ty, p, "deref_load")
- .unwrap()
- .as_basic_value_enum();
- }
-
- panic!(
- "deref expects pointer or lvalue (x[i], x.field), got: {:?}",
- inner_expr
- );
+ let pointee = match env.wave_type(inner_expr) {
+ Some(WaveType::Pointer(inner)) => *inner,
+ Some(WaveType::String) => WaveType::Byte,
+ other => panic!(
+ "typed HIR did not provide a pointer type for deref: {:?}",
+ other
+ ),
+ };
+ let load_ty =
+ wave_type_to_llvm_type(env.context, &pointee, env.struct_types, TypeFlavor::Value);
+ let pointer = env.gen(inner_expr, None).into_pointer_value();
+ env.builder
+ .build_load(load_ty, pointer, "deref_load")
+ .unwrap()
}
pub(crate) fn gen_addressof<'ctx, 'a>(
diff --git a/tests/cases/shared/test103.wave b/tests/cases/shared/test103.wave
new file mode 100644
index 00000000..f751e8ce
--- /dev/null
+++ b/tests/cases/shared/test103.wave
@@ -0,0 +1,41 @@
+// A pointer field with a zero low byte must remain non-null after aggregate
+// return, local storage, address-of, and projected dereference. These pointer
+// values are only compared, never dereferenced as memory addresses.
+struct ByteWriter { data: ptr; len: i64; position: i64; }
+
+fun bytes_writer(data: ptr, len: i64) -> ByteWriter {
+ return ByteWriter { data: data, len: len, position: 0 };
+}
+
+fun check_writer(writer: ptr) -> i32 {
+ if (deref writer.data == null) { return 50; }
+ if (deref (writer.data) != (4096 as ptr)) { return 51; }
+ if (deref writer.len != 15 || deref writer.position != 0) { return 52; }
+ return 0;
+}
+
+fun check_index(values: ptr>) -> bool {
+ return deref values[0] == (4096 as ptr);
+}
+
+fun check_depth(value: ptr>) -> bool {
+ return deref value == (4096 as ptr);
+}
+
+struct WidthProbe { small: u8; guard: u8; }
+fun widen_field(value: ptr) -> i64 { return deref value.small; }
+fun widen_scalar(value: ptr) -> i64 { return deref value; }
+
+fun main() -> i32 {
+ var writer: ByteWriter = bytes_writer(4096 as ptr, 15);
+ if (writer.data == null) { return 1; }
+ var result: i32 = check_writer(&writer);
+ if (result != 0) { return result; }
+ var values: array, 1> = [writer.data];
+ if (!check_index(&values[0])) { return 53; }
+ if (!check_depth(&writer.data)) { return 54; }
+ var width: WidthProbe = WidthProbe { small: 255, guard: 127 };
+ if (widen_field(&width) != 255) { return 55; }
+ if (widen_scalar(&width.small) != 255) { return 56; }
+ return 0;
+}
diff --git a/tests/codegen_regressions.rs b/tests/codegen_regressions.rs
index 7715bc66..5efa145c 100644
--- a/tests/codegen_regressions.rs
+++ b/tests/codegen_regressions.rs
@@ -15,9 +15,9 @@ use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
-use wavec::link_validation::{
- validate_loongarch64_link_inputs, validate_riscv_link_inputs, LoongArchFloatAbi, RiscvFloatAbi,
-};
+#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-loongarch"))]
+use wavec::link_validation::{validate_loongarch64_link_inputs, LoongArchFloatAbi};
+use wavec::link_validation::{validate_riscv_link_inputs, RiscvFloatAbi};
static NEXT_TEMP_CASE: AtomicU64 = AtomicU64::new(0);
@@ -330,6 +330,7 @@ fn riscv64_elf_flags(path: &Path) -> u32 {
u32::from_le_bytes([object[48], object[49], object[50], object[51]])
}
+#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-loongarch"))]
fn loongarch64_elf_flags(path: &Path) -> u32 {
let object = fs::read(path).unwrap();
assert!(
@@ -922,6 +923,7 @@ fn std_net_compiles_for_every_supported_socket_abi() {
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu",
"riscv64-unknown-linux-gnu",
+ #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-loongarch"))]
"loongarch64-unknown-linux-gnu",
"x86_64-apple-darwin",
"aarch64-apple-darwin",
@@ -3048,7 +3050,9 @@ fn hosted_linux_link_plans_use_wave_crt_for_every_architecture_and_mode() {
("riscv64-unknown-linux-gnu", Some("lp64")),
("riscv64-unknown-linux-gnu", Some("lp64f")),
("riscv64-unknown-linux-gnu", Some("lp64d")),
+ #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-loongarch"))]
("loongarch64-unknown-linux-gnu", Some("lp64s")),
+ #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-loongarch"))]
("loongarch64-unknown-linux-gnu", Some("lp64d")),
] {
for (options, object_name) in [
@@ -3460,6 +3464,54 @@ fun main() -> i32 {
}
}
+#[test]
+fn projected_deref_preserves_pointer_storage_types() {
+ let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/cases/shared/test103.wave");
+ let dir = temp_case_dir("projected-pointer-loads");
+ // Use the advertised targets so this regression also runs in isolated
+ // backend builds and covers WebAssembly's 32-bit pointer representation.
+ let (targets, _) = run_wavec_capture(["print", "target-list"]);
+ for target in targets
+ .lines()
+ .map(str::trim)
+ .filter(|line| !line.is_empty())
+ {
+ let out = dir.join(target);
+ run_wavec([
+ OsStr::new("build"),
+ source.as_os_str(),
+ OsStr::new("--target"),
+ OsStr::new(target),
+ OsStr::new("--emit=ir"),
+ OsStr::new("--out-dir"),
+ out.as_os_str(),
+ ]);
+ let ir = fs::read_to_string(out.join("test103.ll")).unwrap();
+ for name in ["check_writer", "check_index", "check_depth"] {
+ let body = ir
+ .split(&format!("@{name}("))
+ .nth(1)
+ .unwrap()
+ .split("\n}")
+ .next()
+ .unwrap();
+ assert!(body.contains("load ptr"), "{target} {name}: {body}");
+ assert!(!body.contains("load i8"), "{target} {name}: {body}");
+ }
+ for name in ["widen_field", "widen_scalar"] {
+ let body = ir
+ .split(&format!("@{name}("))
+ .nth(1)
+ .unwrap()
+ .split("\n}")
+ .next()
+ .unwrap();
+ assert!(body.contains("load i8"), "{target} {name}: {body}");
+ assert!(!body.contains("load i64"), "{target} {name}: {body}");
+ }
+ }
+}
+
#[test]
fn freestanding_codegen_marks_functions_no_red_zone() {
let dir = temp_case_dir("freestanding-noredzone");
@@ -4127,6 +4179,7 @@ fn odd_sized_aggregate_transport_matches_clang_ir_contracts() {
"aarch64-apple-darwin",
"riscv64-unknown-linux-gnu",
"aarch64-w64-windows-gnu",
+ #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-loongarch"))]
"loongarch64-unknown-linux-gnu",
] {
let tag = target.split('-').next().unwrap();
@@ -4317,6 +4370,7 @@ fun main() -> i32 { return c_i8(-1) as i32 + c_u8(1) as i32 + c_i16(-1) as i32 +
"x86_64-pc-windows-gnu",
"riscv64-unknown-linux-gnu",
"aarch64-w64-windows-gnu",
+ #[cfg(any(feature = "llvm-target-all", feature = "llvm-target-loongarch"))]
"loongarch64-unknown-linux-gnu",
] {
let out = dir.join(target);
@@ -4573,6 +4627,7 @@ fn riscv_link_input_abi_is_validated_before_linking() {
}
}
+#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-loongarch"))]
#[test]
fn loongarch64_link_inputs_require_matching_abi_before_linking() {
let dir = temp_case_dir("loongarch64-pre-link-abi");
@@ -4629,6 +4684,7 @@ fn loongarch64_link_inputs_require_matching_abi_before_linking() {
);
}
+#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-loongarch"))]
#[test]
fn loongarch64_lp64f_hosted_linking_is_rejected_before_the_linker() {
let dir = temp_case_dir("loongarch64-lp64f-hosted-link");
@@ -4655,6 +4711,7 @@ fn loongarch64_lp64f_hosted_linking_is_rejected_before_the_linker() {
);
}
+#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-loongarch"))]
#[test]
fn loongarch64_float_abi_modes_match_clang_contracts() {
let dir = temp_case_dir("loongarch64-float-abi-modes");
@@ -5032,6 +5089,7 @@ fn riscv64_c_abi_interoperates_with_c_under_qemu() {
);
}
+#[cfg(any(feature = "llvm-target-all", feature = "llvm-target-loongarch"))]
#[test]
fn loongarch64_lp64d_c_abi_interoperates_with_clang_under_qemu() {
if std::env::var_os("WAVE_RUN_LOONGARCH64_INTEROP_TESTS").is_none() {
From 6fb6ed78260b07cd1c92ca03e418c3ea7e93ef5c Mon Sep 17 00:00:00 2001
From: LunaStev
Date: Mon, 7 Sep 2026 14:51:03 +0900
Subject: [PATCH 3/3] feat(frontend): stabilize Alpha parsing, literals, and
source provenance
Signed-off-by: LunaStev
---
.github/workflows/rust.yml | 2 +-
CONTRIBUTING.md | 16 +-
README.md | 3 +
front/error/src/error.rs | 88 +++-
front/error/src/lib.rs | 3 +
front/error/src/span.rs | 38 ++
front/lexer/src/core.rs | 37 +-
front/lexer/src/ident.rs | 73 ++-
front/lexer/src/lib.rs | 20 +
front/lexer/src/literals.rs | 12 +-
front/lexer/src/number.rs | 131 ++++++
front/lexer/src/scan.rs | 184 ++++----
front/lexer/src/token.rs | 19 +
front/lexer/tests/numeric_contract.rs | 58 +++
front/parser/src/ast.rs | 151 ++++++-
front/parser/src/expr/assign.rs | 52 ++-
front/parser/src/expr/binary.rs | 70 +--
front/parser/src/expr/helpers.rs | 2 +-
front/parser/src/expr/postfix.rs | 7 +
front/parser/src/expr/primary.rs | 587 +++++++++++++------------
front/parser/src/expr/unary.rs | 152 ++++---
front/parser/src/generics.rs | 89 +++-
front/parser/src/hir.rs | 107 ++++-
front/parser/src/import.rs | 25 +-
front/parser/src/lib.rs | 2 +
front/parser/src/parser/asm.rs | 67 +--
front/parser/src/parser/control.rs | 208 +++++----
front/parser/src/parser/decl.rs | 53 +--
front/parser/src/parser/functions.rs | 256 ++---------
front/parser/src/parser/items.rs | 4 +
front/parser/src/parser/mod.rs | 2 +-
front/parser/src/parser/parse.rs | 43 +-
front/parser/src/parser/stmt.rs | 322 +++++---------
front/parser/src/parser/types.rs | 87 ++--
front/parser/src/source.rs | 266 +++++++++++
front/parser/src/verification.rs | 259 +++++++----
front/parser/tests/alpha_frontend.rs | 234 ++++++++++
front/parser/tests/grammar_contract.rs | 94 ++++
front/parser/tests/source_spans.rs | 185 ++++++++
llvm/src/codegen/abi_c.rs | 9 +-
llvm/src/codegen/consts.rs | 51 +--
llvm/src/codegen/ir.rs | 55 ++-
llvm/src/codegen/mod.rs | 2 +
llvm/src/codegen/number.rs | 13 +
llvm/src/codegen/types.rs | 3 +
llvm/src/codegen/variants.rs | 4 +
llvm/src/expression/rvalue/calls.rs | 2 +-
llvm/src/expression/rvalue/dispatch.rs | 1 +
llvm/src/expression/rvalue/literals.rs | 56 +--
llvm/src/statement/control.rs | 30 +-
llvm/src/statement/mod.rs | 11 +
spec/README.md | 151 +++++++
spec/alpha-0.ebnf | 89 ++++
spec/fixtures.tsv | 13 +
spec/fixtures/aliases.accept.wave | 1 +
spec/fixtures/aliases.reject.wave | 1 +
spec/fixtures/control.accept.wave | 1 +
spec/fixtures/control.reject.wave | 1 +
spec/fixtures/enums.accept.wave | 1 +
spec/fixtures/enums.reject.wave | 1 +
spec/fixtures/expressions.accept.wave | 1 +
spec/fixtures/expressions.reject.wave | 1 +
spec/fixtures/ffi.accept.wave | 1 +
spec/fixtures/ffi.reject.wave | 1 +
spec/fixtures/functions.accept.wave | 2 +
spec/fixtures/functions.reject.wave | 1 +
spec/fixtures/imports.accept.wave | 1 +
spec/fixtures/imports.reject.wave | 1 +
spec/fixtures/io_asm.accept.wave | 1 +
spec/fixtures/io_asm.reject.wave | 1 +
spec/fixtures/matching.accept.wave | 1 +
spec/fixtures/matching.reject.wave | 1 +
spec/fixtures/numbers.accept.wave | 1 +
spec/fixtures/numbers.reject.wave | 1 +
spec/fixtures/records.accept.wave | 2 +
spec/fixtures/records.reject.wave | 1 +
spec/fixtures/variants.accept.wave | 1 +
spec/fixtures/variants.reject.wave | 1 +
spec/tokens.tsv | 110 +++++
src/module_resolver.rs | 44 +-
src/runner.rs | 245 ++---------
tests/codegen_regressions.rs | 4 +-
tests/frontend_regressions.rs | 255 +++++++++++
83 files changed, 3430 insertions(+), 1752 deletions(-)
create mode 100644 front/error/src/span.rs
create mode 100644 front/lexer/src/number.rs
create mode 100644 front/lexer/tests/numeric_contract.rs
create mode 100644 front/parser/src/source.rs
create mode 100644 front/parser/tests/alpha_frontend.rs
create mode 100644 front/parser/tests/grammar_contract.rs
create mode 100644 front/parser/tests/source_spans.rs
create mode 100644 llvm/src/codegen/number.rs
create mode 100644 spec/README.md
create mode 100644 spec/alpha-0.ebnf
create mode 100644 spec/fixtures.tsv
create mode 100644 spec/fixtures/aliases.accept.wave
create mode 100644 spec/fixtures/aliases.reject.wave
create mode 100644 spec/fixtures/control.accept.wave
create mode 100644 spec/fixtures/control.reject.wave
create mode 100644 spec/fixtures/enums.accept.wave
create mode 100644 spec/fixtures/enums.reject.wave
create mode 100644 spec/fixtures/expressions.accept.wave
create mode 100644 spec/fixtures/expressions.reject.wave
create mode 100644 spec/fixtures/ffi.accept.wave
create mode 100644 spec/fixtures/ffi.reject.wave
create mode 100644 spec/fixtures/functions.accept.wave
create mode 100644 spec/fixtures/functions.reject.wave
create mode 100644 spec/fixtures/imports.accept.wave
create mode 100644 spec/fixtures/imports.reject.wave
create mode 100644 spec/fixtures/io_asm.accept.wave
create mode 100644 spec/fixtures/io_asm.reject.wave
create mode 100644 spec/fixtures/matching.accept.wave
create mode 100644 spec/fixtures/matching.reject.wave
create mode 100644 spec/fixtures/numbers.accept.wave
create mode 100644 spec/fixtures/numbers.reject.wave
create mode 100644 spec/fixtures/records.accept.wave
create mode 100644 spec/fixtures/records.reject.wave
create mode 100644 spec/fixtures/variants.accept.wave
create mode 100644 spec/fixtures/variants.reject.wave
create mode 100644 spec/tokens.tsv
create mode 100644 tests/frontend_regressions.rs
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 059a0067..2df8e904 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -90,7 +90,7 @@ jobs:
run: cargo build --locked --release --verbose
- name: Run Rust tests
- run: cargo test --locked --all-targets --verbose
+ run: cargo test --locked --workspace --all-targets --verbose
- name: Check examples and standard library corpus
run: >-
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e6444c31..4f4878ed 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -91,21 +91,21 @@ resource-intensive Cargo commands (CI sets `CARGO_BUILD_JOBS=2`).
```bash
cargo fmt --all --check
./tools/check_std_policy.sh
-RUSTDOCFLAGS="-D warnings" cargo doc --locked --no-deps --jobs 2
-cargo clippy --locked --all-targets -- -D warnings
+RUSTDOCFLAGS="-D warnings" cargo doc --locked --workspace --no-deps --jobs 2
+cargo clippy --locked --workspace --all-targets -- -D warnings
python3 -m py_compile x.py tools/check_wave_corpus.py tools/case_manifest.py \
tools/populate_case_matrix.py tools/run_tests.py tools/test_contracts.py \
tools/test_case_manifest.py tools/test_test_contracts.py
python3 -m unittest tools.test_case_manifest tools.test_test_contracts
cargo build --locked --release --jobs 2
-cargo test --locked --all-targets --verbose
+cargo test --locked --workspace --all-targets --verbose
python3 tools/check_wave_corpus.py --wavec target/release/wavec --run-std-examples
```
Notes:
- Formatting must use `cargo fmt --all --check` (not bare `cargo fmt --check`).
-- Clippy denies warnings: `cargo clippy --locked --all-targets -- -D warnings`.
+- Clippy denies warnings: `cargo clippy --locked --workspace --all-targets -- -D warnings`.
- rustdoc must be warning-free via `RUSTDOCFLAGS="-D warnings"`.
- Standard-library policy is enforced by `./tools/check_std_policy.sh`.
- Wave language corpus / std examples are checked with `tools/check_wave_corpus.py`
@@ -178,7 +178,7 @@ All formatting and lint rules must pass:
```bash
cargo fmt --all --check
-cargo clippy --locked --all-targets -- -D warnings
+cargo clippy --locked --workspace --all-targets -- -D warnings
```
---
@@ -203,7 +203,7 @@ All additional functionality should be provided through external libraries
Wave uses:
-- Locked Rust tests: `cargo test --locked --all-targets`
+- Locked Rust tests: `cargo test --locked --workspace --all-targets`
- Automated `.wave` language cases and std examples via
`python3 tools/check_wave_corpus.py`
- Python tooling unit tests: `python3 -m unittest tools.test_case_manifest tools.test_test_contracts`
@@ -215,6 +215,10 @@ Contributors should:
---
+Frontend syntax changes must update [the Alpha grammar and token inventory](spec/README.md)
+and its positive/negative fixtures. Run `cargo test --locked -p lexer -p parser --jobs 2`
+for backend-independent frontend tests, then the workspace tests for driver and codegen coverage.
+
## 9. Pull Request Guidelines
A PR should include:
diff --git a/README.md b/README.md
index d320c279..b4321b0b 100644
--- a/README.md
+++ b/README.md
@@ -231,3 +231,6 @@ Wave is developed in public with support from individuals and organizations. You
Thank you to everyone who contributes code, documentation, testing, funding, or time to Wave.
+
+The [Alpha language contract](spec/README.md) defines the grammar, numeric literals,
+token status and source-location conventions checked by frontend conformance tests.
diff --git a/front/error/src/error.rs b/front/error/src/error.rs
index bbce070f..65cd8e62 100644
--- a/front/error/src/error.rs
+++ b/front/error/src/error.rs
@@ -95,6 +95,7 @@ pub struct WaveError {
pub source: Option,
pub source_code: Option,
pub span_len: usize,
+ pub span: Option,
pub label: Option,
pub context: Option,
pub expected: Vec,
@@ -131,6 +132,7 @@ impl WaveError {
source: None,
source_code: None,
span_len: 1,
+ span: None,
label: None,
context: None,
expected: Vec::new(),
@@ -152,6 +154,22 @@ impl WaveError {
self
}
+ pub fn with_span(mut self, span: Option<&crate::SourceSpan>) -> Self {
+ if let Some(span) = span {
+ let span = span.focus.as_deref().unwrap_or(span);
+ self.file = span.file.clone();
+ self.line = span.line;
+ self.column = span.column;
+ self.span_len = if span.line == span.end_line {
+ span.end_column.saturating_sub(span.column).max(1)
+ } else {
+ 1
+ };
+ self.span = Some(span.clone());
+ }
+ self
+ }
+
pub fn with_span_len(mut self, span_len: usize) -> Self {
self.span_len = span_len.max(1);
self
@@ -221,10 +239,26 @@ impl WaveError {
push_json_field(&mut out, "file", &self.file);
out.push_str(&format!(
",\"line\":{},\"column\":{},\"span_len\":{}",
- self.line.max(1),
- self.column.max(1),
+ self.line,
+ self.column,
self.span_len.max(1)
));
+ out.push_str(",\"span\":");
+ if let Some(span) = &self.span {
+ out.push('{');
+ push_json_field(&mut out, "file", &span.file);
+ out.push_str(&format!(",\"start\":{},\"end\":{},\"line\":{},\"column\":{},\"end_line\":{},\"end_column\":{}", span.start, span.end, span.line, span.column, span.end_line, span.end_column));
+ out.push_str(",\"expansion\":[");
+ for (i, reason) in span.expansion.iter().enumerate() {
+ if i > 0 {
+ out.push(',');
+ }
+ out.push_str(&json_string(reason));
+ }
+ out.push_str("]}");
+ } else {
+ out.push_str("null");
+ }
out.push(',');
push_json_field(
&mut out,
@@ -333,8 +367,11 @@ impl WaveError {
use utils::colorex::*;
let pipe = "|".color("38,139,235").bold();
- let line = self.line.max(1);
- let col = self.column.max(1);
+ if self.line == 0 || self.column == 0 {
+ return;
+ }
+ let line = self.line;
+ let col = self.column;
if let Some(source_code) = &self.source_code {
let lines: Vec<&str> = source_code.lines().collect();
@@ -353,19 +390,20 @@ impl WaveError {
pipe,
source_line
);
- }
-
- let pad = " ".repeat(width);
- let spaces = " ".repeat(col.saturating_sub(1));
- let marks = "^"
- .repeat(self.span_len.max(1))
- .color(self.severity_color())
- .bold();
- match &self.label {
- Some(label) => {
- eprintln!(" {} {} {}{} {}", pad, pipe, spaces, marks, label.dim())
+ if ln == line {
+ let pad = " ".repeat(width);
+ let spaces = " ".repeat(col.saturating_sub(1));
+ let marks = "^"
+ .repeat(self.span_len.max(1))
+ .color(self.severity_color())
+ .bold();
+ match &self.label {
+ Some(label) => {
+ eprintln!(" {} {} {}{} {}", pad, pipe, spaces, marks, label.dim())
+ }
+ None => eprintln!(" {} {} {}{}", pad, pipe, spaces, marks),
+ }
}
- None => eprintln!(" {} {} {}{}", pad, pipe, spaces, marks),
}
return;
@@ -423,13 +461,17 @@ impl WaveError {
eprintln!("{}{}: {}", severity_str, code, self.message.bold());
}
- eprintln!(
- " {} {}:{}:{}",
- "-->".color("38,139,235").bold(),
- self.file,
- self.line.max(1),
- self.column.max(1)
- );
+ if self.line > 0 && self.column > 0 {
+ eprintln!(
+ " {} {}:{}:{}",
+ "-->".color("38,139,235").bold(),
+ self.file,
+ self.line,
+ self.column
+ );
+ } else {
+ eprintln!(" {} {}", "-->".color("38,139,235").bold(), self.file);
+ }
self.display_source_block();
if let Some(context) = &self.context {
diff --git a/front/error/src/lib.rs b/front/error/src/lib.rs
index bce5065a..af11d9cc 100644
--- a/front/error/src/lib.rs
+++ b/front/error/src/lib.rs
@@ -6,3 +6,6 @@
pub mod error;
pub use error::*;
+
+pub mod span;
+pub use span::SourceSpan;
diff --git a/front/error/src/span.rs b/front/error/src/span.rs
new file mode 100644
index 00000000..177ec958
--- /dev/null
+++ b/front/error/src/span.rs
@@ -0,0 +1,38 @@
+//! UTF-8 byte ranges with one-based Unicode scalar line/column coordinates.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct SourceSpan {
+ pub file: String,
+ pub start: usize,
+ pub end: usize,
+ pub line: usize,
+ pub column: usize,
+ pub end_line: usize,
+ pub end_column: usize,
+ /// Empty for physical syntax; generated syntax records its transformation.
+ pub expansion: Vec,
+ /// Optional parser-selected name token for declaration diagnostics.
+ pub focus: Option>,
+}
+
+impl SourceSpan {
+ pub fn through(&self, last: &Self) -> Self {
+ if self.file != last.file {
+ return self.clone();
+ }
+ Self {
+ end: last.end,
+ end_line: last.end_line,
+ end_column: last.end_column,
+ ..self.clone()
+ }
+ }
+
+ pub fn generated(mut self, reason: impl Into) -> Self {
+ let reason = reason.into();
+ if let Some(focus) = &mut self.focus {
+ focus.expansion.push(reason.clone());
+ }
+ self.expansion.push(reason);
+ self
+ }
+}
diff --git a/front/lexer/src/core.rs b/front/lexer/src/core.rs
index e601234d..2cc9db62 100644
--- a/front/lexer/src/core.rs
+++ b/front/lexer/src/core.rs
@@ -24,6 +24,7 @@ pub struct Token {
pub token_type: TokenType,
pub lexeme: String,
pub line: usize,
+ pub span: Option,
}
impl Token {
@@ -32,6 +33,7 @@ impl Token {
token_type,
lexeme,
line,
+ span: None,
}
}
}
@@ -42,6 +44,7 @@ impl Default for Token {
token_type: TokenType::Eof,
lexeme: String::new(),
line: 0,
+ span: None,
}
}
}
@@ -103,8 +106,40 @@ impl<'a> Lexer<'a> {
line: usize,
column: usize,
) -> WaveError {
- WaveError::new(kind, message, self.file.clone(), line.max(1), column.max(1))
+ let line_start: usize = self
+ .source
+ .split_inclusive('\n')
+ .take(line.saturating_sub(1))
+ .map(str::len)
+ .sum();
+ let line_text = self
+ .source
+ .get(line_start..)
+ .unwrap_or("")
+ .split('\n')
+ .next()
+ .unwrap_or("");
+ let start = line_start
+ + line_text
+ .char_indices()
+ .nth(column.saturating_sub(1))
+ .map_or(line_text.len(), |(offset, _)| offset);
+ let end = self.current.max(start).min(self.source.len());
+ let prefix = &self.source[..end];
+ let span = error::SourceSpan {
+ file: self.file.clone(),
+ start,
+ end,
+ line,
+ column,
+ end_line: prefix.bytes().filter(|byte| *byte == b'\n').count() + 1,
+ end_column: prefix.rsplit('\n').next().unwrap_or("").chars().count() + 1,
+ expansion: Vec::new(),
+ focus: None,
+ };
+ WaveError::new(kind, message, self.file.clone(), line, column)
.with_source_code(self.source.to_string())
+ .with_span(Some(&span))
}
pub(crate) fn make_error_here(
diff --git a/front/lexer/src/ident.rs b/front/lexer/src/ident.rs
index 7d042546..3d743f4c 100644
--- a/front/lexer/src/ident.rs
+++ b/front/lexer/src/ident.rs
@@ -20,12 +20,8 @@ use crate::token::*;
use crate::{Lexer, Token};
impl<'a> Lexer<'a> {
- pub(crate) fn identifier(&mut self) -> String {
- let start = if self.current > 0 {
- self.current - 1
- } else {
- 0
- };
+ pub(crate) fn identifier(&mut self, first: char) -> String {
+ let start = self.current - first.len_utf8();
while !self.is_at_end() {
let c = self.peek();
@@ -45,326 +41,391 @@ impl<'a> Lexer<'a> {
token_type: TokenType::Fun,
lexeme: "fun".to_string(),
line: self.line,
+ span: None,
},
"extern" => Token {
token_type: TokenType::Extern,
lexeme: "extern".to_string(),
line: self.line,
+ span: None,
},
"export" => Token {
token_type: TokenType::Export,
lexeme: "export".to_string(),
line: self.line,
+ span: None,
},
"pub" => Token {
token_type: TokenType::Pub,
lexeme: "pub".to_string(),
line: self.line,
+ span: None,
},
"type" => Token {
token_type: TokenType::Type,
lexeme: "type".to_string(),
line: self.line,
+ span: None,
},
"enum" => Token {
token_type: TokenType::Enum,
lexeme: "enum".to_string(),
line: self.line,
+ span: None,
},
"variant" => Token {
token_type: TokenType::Variant,
lexeme: "variant".to_string(),
line: self.line,
+ span: None,
},
"static" => Token {
token_type: TokenType::Static,
lexeme: "static".to_string(),
line: self.line,
+ span: None,
},
"var" => Token {
token_type: TokenType::Var,
lexeme: "var".to_string(),
line: self.line,
+ span: None,
},
"deref" => Token {
token_type: TokenType::Deref,
lexeme: "deref".to_string(),
line: self.line,
+ span: None,
},
"let" => Token {
token_type: TokenType::Let,
lexeme: "let".to_string(),
line: self.line,
+ span: None,
},
"mut" => Token {
token_type: TokenType::Mut,
lexeme: "mut".to_string(),
line: self.line,
+ span: None,
},
"const" => Token {
token_type: TokenType::Const,
lexeme: "const".to_string(),
line: self.line,
+ span: None,
},
"if" => Token {
token_type: TokenType::If,
lexeme: "if".to_string(),
line: self.line,
+ span: None,
},
"else" => Token {
token_type: TokenType::Else,
lexeme: "else".to_string(),
line: self.line,
+ span: None,
},
"proto" => Token {
token_type: TokenType::Proto,
lexeme: "proto".to_string(),
line: self.line,
+ span: None,
},
"struct" => Token {
token_type: TokenType::Struct,
lexeme: "struct".to_string(),
line: self.line,
+ span: None,
},
"while" => Token {
token_type: TokenType::While,
lexeme: "while".to_string(),
line: self.line,
+ span: None,
},
"for" => Token {
token_type: TokenType::For,
lexeme: "for".to_string(),
line: self.line,
+ span: None,
},
"module" => Token {
token_type: TokenType::Module,
lexeme: "module".to_string(),
line: self.line,
+ span: None,
},
"class" => Token {
token_type: TokenType::Class,
lexeme: "class".to_string(),
line: self.line,
+ span: None,
},
"in" => Token {
token_type: TokenType::In,
lexeme: "in".to_string(),
line: self.line,
+ span: None,
},
"out" => Token {
token_type: TokenType::Out,
lexeme: "out".to_string(),
line: self.line,
+ span: None,
},
"clobber" => Token {
token_type: TokenType::Clobber,
lexeme: "clobber".to_string(),
line: self.line,
+ span: None,
},
"is" => Token {
token_type: TokenType::Is,
lexeme: "is".to_string(),
line: self.line,
+ span: None,
},
"as" => Token {
token_type: TokenType::As,
lexeme: "as".to_string(),
line: self.line,
+ span: None,
},
"asm" => Token {
token_type: TokenType::Asm,
lexeme: "asm".to_string(),
line: self.line,
+ span: None,
},
"xnand" => Token {
token_type: TokenType::Xnand,
lexeme: "xnand".to_string(),
line: self.line,
+ span: None,
},
"import" => Token {
token_type: TokenType::Import,
lexeme: "import".to_string(),
line: self.line,
+ span: None,
},
"return" => Token {
token_type: TokenType::Return,
lexeme: "return".to_string(),
line: self.line,
+ span: None,
},
"continue" => Token {
token_type: TokenType::Continue,
lexeme: "continue".to_string(),
line: self.line,
+ span: None,
},
"print" => Token {
token_type: TokenType::Print,
lexeme: "print".to_string(),
line: self.line,
+ span: None,
},
"input" => Token {
token_type: TokenType::Input,
lexeme: "input".to_string(),
line: self.line,
+ span: None,
},
"println" => Token {
token_type: TokenType::Println,
lexeme: "println".to_string(),
line: self.line,
+ span: None,
},
"match" => Token {
token_type: TokenType::Match,
lexeme: "match".to_string(),
line: self.line,
+ span: None,
},
"char" => Token {
token_type: TokenType::TypeChar,
lexeme: "char".to_string(),
line: self.line,
+ span: None,
},
"byte" => Token {
token_type: TokenType::TypeByte,
lexeme: "byte".to_string(),
line: self.line,
+ span: None,
},
"ptr" => Token {
token_type: TokenType::Identifier("ptr".to_string()),
lexeme: "ptr".to_string(),
line: self.line,
+ span: None,
},
"array" => Token {
token_type: TokenType::Identifier("array".to_string()),
lexeme: "array".to_string(),
line: self.line,
+ span: None,
},
"isz" => Token {
token_type: TokenType::TokenTypeInt(IntegerType::ISZ),
lexeme: "isz".to_string(),
line: self.line,
+ span: None,
},
"i8" => Token {
token_type: TokenType::TokenTypeInt(IntegerType::I8),
lexeme: "i8".to_string(),
line: self.line,
+ span: None,
},
"i16" => Token {
token_type: TokenType::TokenTypeInt(IntegerType::I16),
lexeme: "i16".to_string(),
line: self.line,
+ span: None,
},
"i32" => Token {
token_type: TokenType::TokenTypeInt(IntegerType::I32),
lexeme: "i32".to_string(),
line: self.line,
+ span: None,
},
"i64" => Token {
token_type: TokenType::TokenTypeInt(IntegerType::I64),
lexeme: "i64".to_string(),
line: self.line,
+ span: None,
},
"i128" => Token {
token_type: TokenType::TokenTypeInt(IntegerType::I128),
lexeme: "i128".to_string(),
line: self.line,
+ span: None,
},
"i256" => Token {
token_type: TokenType::TokenTypeInt(IntegerType::I256),
lexeme: "i256".to_string(),
line: self.line,
+ span: None,
},
"i512" => Token {
token_type: TokenType::TokenTypeInt(IntegerType::I512),
lexeme: "i512".to_string(),
line: self.line,
+ span: None,
},
"i1024" => Token {
token_type: TokenType::TokenTypeInt(IntegerType::I1024),
lexeme: "i1024".to_string(),
line: self.line,
+ span: None,
},
"usz" => Token {
token_type: TokenType::TokenTypeUint(UnsignedIntegerType::USZ),
lexeme: "usz".to_string(),
line: self.line,
+ span: None,
},
"u8" => Token {
token_type: TokenType::TokenTypeUint(UnsignedIntegerType::U8),
lexeme: "u8".to_string(),
line: self.line,
+ span: None,
},
"u16" => Token {
token_type: TokenType::TokenTypeUint(UnsignedIntegerType::U16),
lexeme: "u16".to_string(),
line: self.line,
+ span: None,
},
"u32" => Token {
token_type: TokenType::TokenTypeUint(UnsignedIntegerType::U32),
lexeme: "u32".to_string(),
line: self.line,
+ span: None,
},
"u64" => Token {
token_type: TokenType::TokenTypeUint(UnsignedIntegerType::U64),
lexeme: "u64".to_string(),
line: self.line,
+ span: None,
},
"u128" => Token {
token_type: TokenType::TokenTypeUint(UnsignedIntegerType::U128),
lexeme: "u128".to_string(),
line: self.line,
+ span: None,
},
"u256" => Token {
token_type: TokenType::TokenTypeUint(UnsignedIntegerType::U256),
lexeme: "u256".to_string(),
line: self.line,
+ span: None,
},
"u512" => Token {
token_type: TokenType::TokenTypeUint(UnsignedIntegerType::U512),
lexeme: "u512".to_string(),
line: self.line,
+ span: None,
},
"u1024" => Token {
token_type: TokenType::TokenTypeUint(UnsignedIntegerType::U1024),
lexeme: "u1024".to_string(),
line: self.line,
+ span: None,
},
"f32" => Token {
token_type: TokenType::TokenTypeFloat(FloatType::F32),
lexeme: "f32".to_string(),
line: self.line,
+ span: None,
},
"f64" => Token {
token_type: TokenType::TokenTypeFloat(FloatType::F64),
lexeme: "f64".to_string(),
line: self.line,
+ span: None,
},
"str" => Token {
token_type: TokenType::TypeString,
lexeme: "str".to_string(),
line: self.line,
+ span: None,
},
"break" => Token {
token_type: TokenType::Break,
lexeme: "break".to_string(),
line: self.line,
+ span: None,
},
"true" => Token {
token_type: TokenType::BoolLiteral(true),
lexeme: "true".to_string(),
line: self.line,
+ span: None,
},
"false" => Token {
token_type: TokenType::BoolLiteral(false),
lexeme: "false".to_string(),
line: self.line,
+ span: None,
},
"null" => Token {
token_type: TokenType::Null,
lexeme: "null".to_string(),
line: self.line,
+ span: None,
},
_ => Token {
token_type: TokenType::Identifier(ident.clone()),
lexeme: ident,
line: self.line,
+ span: None,
},
}
}
diff --git a/front/lexer/src/lib.rs b/front/lexer/src/lib.rs
index bdf96868..20eac296 100644
--- a/front/lexer/src/lib.rs
+++ b/front/lexer/src/lib.rs
@@ -23,8 +23,28 @@ pub mod core;
pub mod cursor;
pub mod ident;
pub mod literals;
+pub mod number;
pub mod scan;
pub mod token;
pub mod trivia;
pub use crate::core::{Lexer, Token};
+
+/// Range of exactly the tokens consumed between two parser cursors.
+pub fn consumed_span<'a, T>(
+ before: std::iter::Peekable,
+ after: &mut std::iter::Peekable,
+) -> Option
+where
+ T: Iterator- + Clone,
+{
+ let stop = after.peek().copied();
+ let mut consumed = before.take_while(|t| stop.is_none_or(|stop| !std::ptr::eq(*t, stop)));
+ let first = consumed.next()?.span.as_ref()?.clone();
+ Some(
+ consumed
+ .last()
+ .and_then(|t| t.span.as_ref())
+ .map_or(first.clone(), |last| first.through(last)),
+ )
+}
diff --git a/front/lexer/src/literals.rs b/front/lexer/src/literals.rs
index d190dec9..c59f065d 100644
--- a/front/lexer/src/literals.rs
+++ b/front/lexer/src/literals.rs
@@ -273,7 +273,17 @@ impl<'a> Lexer<'a> {
.with_label("char literal must contain exactly one character")
.with_help("close with `'` and ensure exactly one character value"));
}
- self.advance(); // closing '
+ self.advance(); // closing quote
+ if u32::from(c) > 255 {
+ return Err(self
+ .make_error(
+ WaveErrorKind::InvalidString("character is outside the byte range".into()),
+ "char literals must fit the unsigned 8-bit char type",
+ start_line,
+ start_col,
+ )
+ .with_code("E1005"));
+ }
Ok(c)
}
}
diff --git a/front/lexer/src/number.rs b/front/lexer/src/number.rs
new file mode 100644
index 00000000..29babafd
--- /dev/null
+++ b/front/lexer/src/number.rs
@@ -0,0 +1,131 @@
+//! Canonical Alpha numeric literal grammar, independent of target and backend.
+//!
+//! Integers use decimal, 0b, 0o or 0x digits. A single underscore may separate
+//! digits. Decimal floats require a fractional part or exponent; suffixes and
+//! hexadecimal floats are unsupported. Signs belong to unary expressions.
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct IntegerLiteral {
+ pub negative: bool,
+ pub radix: u32,
+ /// Validated digits with separators removed (at least one digit).
+ pub digits: String,
+}
+
+fn digits(raw: &str, radix: u32) -> Option {
+ let chars: Vec = raw.chars().collect();
+ if chars.is_empty() {
+ return None;
+ }
+ for (i, ch) in chars.iter().enumerate() {
+ if *ch == '_' {
+ if i == 0
+ || i + 1 == chars.len()
+ || !chars[i - 1].is_ascii()
+ || !chars[i - 1].is_digit(radix)
+ || !chars[i + 1].is_ascii()
+ || !chars[i + 1].is_digit(radix)
+ {
+ return None;
+ }
+ } else if !ch.is_ascii() || !ch.is_digit(radix) {
+ return None;
+ }
+ }
+ Some(raw.replace('_', ""))
+}
+
+impl IntegerLiteral {
+ pub fn parse(raw: &str) -> Option {
+ let (negative, unsigned) = if let Some(rest) = raw.strip_prefix('-') {
+ (true, rest)
+ } else {
+ (false, raw.strip_prefix('+').unwrap_or(raw))
+ };
+ let (radix, raw_digits) = match unsigned.as_bytes().get(..2) {
+ Some(b"0x" | b"0X") => (16, &unsigned[2..]),
+ Some(b"0o" | b"0O") => (8, &unsigned[2..]),
+ Some(b"0b" | b"0B") => (2, &unsigned[2..]),
+ _ => (10, unsigned),
+ };
+ Some(Self {
+ negative,
+ radix,
+ digits: digits(raw_digits, radix)?,
+ })
+ }
+
+ pub fn is_zero(&self) -> bool {
+ self.digits.bytes().all(|ch| ch == b'0')
+ }
+
+ pub fn to_i128(&self) -> Option {
+ let magnitude = u128::from_str_radix(&self.digits, self.radix).ok()?;
+ if self.negative && magnitude == (1u128 << 127) {
+ return Some(i128::MIN);
+ }
+ let value = i128::try_from(magnitude).ok()?;
+ if self.negative {
+ value.checked_neg()
+ } else {
+ Some(value)
+ }
+ }
+
+ pub fn to_f64(&self) -> Option {
+ // Accumulate exactly in decimal, then let Rust perform one correctly
+ // rounded conversion. Repeated floating-point multiplication rounds
+ // several times and changes values such as i64::MIN.
+ let mut decimal = vec![0u8];
+ for ch in self.digits.chars() {
+ let mut carry = ch.to_digit(self.radix)?;
+ for digit in &mut decimal {
+ let value = u32::from(*digit) * self.radix + carry;
+ *digit = (value % 10) as u8;
+ carry = value / 10;
+ }
+ while carry != 0 {
+ decimal.push((carry % 10) as u8);
+ carry /= 10;
+ }
+ }
+ let text: String = decimal.iter().rev().map(|d| char::from(b'0' + d)).collect();
+ let value: f64 = text.parse().ok()?;
+ value
+ .is_finite()
+ .then_some(if self.negative { -value } else { value })
+ }
+}
+
+/// Parse a finite decimal float, validating separators before normalization.
+pub fn parse_float(raw: &str) -> Option {
+ let unsigned = raw
+ .strip_prefix('-')
+ .or_else(|| raw.strip_prefix('+'))
+ .unwrap_or(raw);
+ let mut exponent_parts = unsigned.split(['e', 'E']);
+ let mantissa = exponent_parts.next()?;
+ let exponent = exponent_parts.next();
+ if exponent_parts.next().is_some() {
+ return None;
+ }
+ if let Some(exp) = exponent {
+ digits(
+ exp.strip_prefix('-')
+ .or_else(|| exp.strip_prefix('+'))
+ .unwrap_or(exp),
+ 10,
+ )?;
+ }
+ let mut fraction_parts = mantissa.split('.');
+ digits(fraction_parts.next()?, 10)?;
+ let fraction = fraction_parts.next();
+ if let Some(frac) = fraction {
+ digits(frac, 10)?;
+ }
+ if fraction_parts.next().is_some() || (fraction.is_none() && exponent.is_none()) {
+ return None;
+ }
+ let value: f64 = raw.replace('_', "").parse().ok()?;
+ value.is_finite().then_some(value)
+}
diff --git a/front/lexer/src/scan.rs b/front/lexer/src/scan.rs
index 930b3e1f..68a0e51a 100644
--- a/front/lexer/src/scan.rs
+++ b/front/lexer/src/scan.rs
@@ -24,6 +24,27 @@ impl<'a> Lexer<'a> {
#[allow(clippy::never_loop)]
/// Scans the next non-trivia token while preserving its source line.
pub fn next_token(&mut self) -> Result {
+ self.skip_trivia()?;
+ let (start, line, column) = (self.current, self.line, self.current_column());
+ let mut token = self.scan_token()?;
+ token.line = line;
+ token.lexeme = self.source[start..self.current].to_string();
+ token.span = Some(error::SourceSpan {
+ file: self.file.clone(),
+ start,
+ end: self.current,
+ line,
+ column,
+ end_line: self.line,
+ end_column: self.current_column(),
+ expansion: Vec::new(),
+ focus: None,
+ });
+ Ok(token)
+ }
+
+ #[allow(clippy::never_loop)]
+ fn scan_token(&mut self) -> Result {
loop {
self.skip_trivia()?;
@@ -32,6 +53,7 @@ impl<'a> Lexer<'a> {
token_type: TokenType::Eof,
lexeme: String::new(),
line: self.line,
+ span: None,
});
}
@@ -44,18 +66,21 @@ impl<'a> Lexer<'a> {
token_type: TokenType::Increment,
lexeme: "++".to_string(),
line: self.line,
+ span: None,
});
} else if self.match_next('=') {
return Ok(Token {
token_type: TokenType::PlusEq,
lexeme: "+=".to_string(),
line: self.line,
+ span: None,
});
} else {
return Ok(Token {
token_type: TokenType::Plus,
lexeme: "+".to_string(),
line: self.line,
+ span: None,
});
}
}
@@ -65,24 +90,28 @@ impl<'a> Lexer<'a> {
token_type: TokenType::Decrement,
lexeme: "--".to_string(),
line: self.line,
+ span: None,
});
} else if self.match_next('>') {
return Ok(Token {
token_type: TokenType::Arrow,
lexeme: "->".to_string(),
line: self.line,
+ span: None,
});
} else if self.match_next('=') {
return Ok(Token {
token_type: TokenType::MinusEq,
lexeme: "-=".to_string(),
line: self.line,
+ span: None,
});
} else {
return Ok(Token {
token_type: TokenType::Minus,
lexeme: "-".to_string(),
line: self.line,
+ span: None,
});
}
}
@@ -92,12 +121,14 @@ impl<'a> Lexer<'a> {
token_type: TokenType::StarEq,
lexeme: "*=".to_string(),
line: self.line,
+ span: None,
});
} else {
return Ok(Token {
token_type: TokenType::Star,
lexeme: "*".to_string(),
line: self.line,
+ span: None,
});
}
}
@@ -106,6 +137,7 @@ impl<'a> Lexer<'a> {
token_type: TokenType::Dot,
lexeme: ".".to_string(),
line: self.line,
+ span: None,
})
}
'/' => {
@@ -114,12 +146,14 @@ impl<'a> Lexer<'a> {
token_type: TokenType::DivEq,
lexeme: "/=".to_string(),
line: self.line,
+ span: None,
});
} else {
return Ok(Token {
token_type: TokenType::Div,
lexeme: "/".to_string(),
line: self.line,
+ span: None,
});
}
}
@@ -129,12 +163,14 @@ impl<'a> Lexer<'a> {
token_type: TokenType::RemainderEq,
lexeme: "%=".to_string(),
line: self.line,
+ span: None,
});
} else {
return Ok(Token {
token_type: TokenType::Remainder,
lexeme: "%".to_string(),
line: self.line,
+ span: None,
});
}
}
@@ -143,6 +179,7 @@ impl<'a> Lexer<'a> {
token_type: TokenType::SemiColon,
lexeme: ";".to_string(),
line: self.line,
+ span: None,
})
}
':' => {
@@ -151,12 +188,14 @@ impl<'a> Lexer<'a> {
token_type: TokenType::DoubleColon,
lexeme: "::".to_string(),
line: self.line,
+ span: None,
});
}
return Ok(Token {
token_type: TokenType::Colon,
lexeme: ":".to_string(),
line: self.line,
+ span: None,
});
}
'<' => {
@@ -165,18 +204,21 @@ impl<'a> Lexer<'a> {
token_type: TokenType::Rol,
lexeme: "<<".to_string(),
line: self.line,
+ span: None,
});
} else if self.match_next('=') {
return Ok(Token {
token_type: TokenType::LchevrEq,
lexeme: "<=".to_string(),
line: self.line,
+ span: None,
});
} else {
return Ok(Token {
token_type: TokenType::Lchevr,
lexeme: "<".to_string(),
line: self.line,
+ span: None,
});
}
}
@@ -186,18 +228,21 @@ impl<'a> Lexer<'a> {
token_type: TokenType::Ror,
lexeme: ">>".to_string(),
line: self.line,
+ span: None,
});
} else if self.match_next('=') {
return Ok(Token {
token_type: TokenType::RchevrEq,
lexeme: ">=".to_string(),
line: self.line,
+ span: None,
});
} else {
return Ok(Token {
token_type: TokenType::Rchevr,
lexeme: ">".to_string(),
line: self.line,
+ span: None,
});
}
}
@@ -206,6 +251,7 @@ impl<'a> Lexer<'a> {
token_type: TokenType::Lparen,
lexeme: "(".to_string(),
line: self.line,
+ span: None,
})
}
')' => {
@@ -213,6 +259,7 @@ impl<'a> Lexer<'a> {
token_type: TokenType::Rparen,
lexeme: ")".to_string(),
line: self.line,
+ span: None,
})
}
'{' => {
@@ -220,6 +267,7 @@ impl<'a> Lexer<'a> {
token_type: TokenType::Lbrace,
lexeme: "{".to_string(),
line: self.line,
+ span: None,
})
}
'}' => {
@@ -227,6 +275,7 @@ impl<'a> Lexer<'a> {
token_type: TokenType::Rbrace,
lexeme: "}".to_string(),
line: self.line,
+ span: None,
})
}
'[' => {
@@ -234,6 +283,7 @@ impl<'a> Lexer<'a> {
token_type: TokenType::Lbrack,
lexeme: "[".to_string(),
line: self.line,
+ span: None,
})
}
']' => {
@@ -241,6 +291,7 @@ impl<'a> Lexer<'a> {
token_type: TokenType::Rbrack,
lexeme: "]".to_string(),
line: self.line,
+ span: None,
})
}
'=' => {
@@ -249,12 +300,14 @@ impl<'a> Lexer<'a> {
token_type: TokenType::EqualTwo,
lexeme: "==".to_string(),
line: self.line,
+ span: None,
});
} else {
return Ok(Token {
token_type: TokenType::Equal,
lexeme: "=".to_string(),
line: self.line,
+ span: None,
});
}
}
@@ -264,12 +317,14 @@ impl<'a> Lexer<'a> {
token_type: TokenType::LogicalAnd,
lexeme: "&&".to_string(),
line: self.line,
+ span: None,
});
} else {
return Ok(Token {
token_type: TokenType::AddressOf,
lexeme: "&".to_string(),
line: self.line,
+ span: None,
});
}
}
@@ -279,12 +334,14 @@ impl<'a> Lexer<'a> {
token_type: TokenType::LogicalOr,
lexeme: "||".to_string(),
line: self.line,
+ span: None,
});
} else {
return Ok(Token {
token_type: TokenType::BitwiseOr,
lexeme: "|".to_string(),
line: self.line,
+ span: None,
});
}
}
@@ -294,24 +351,28 @@ impl<'a> Lexer<'a> {
token_type: TokenType::NotEqual,
lexeme: "!=".to_string(),
line: self.line,
+ span: None,
});
} else if self.match_next('&') {
return Ok(Token {
token_type: TokenType::Nand,
lexeme: "!&".to_string(),
line: self.line,
+ span: None,
});
} else if self.match_next('|') {
return Ok(Token {
token_type: TokenType::Nor,
lexeme: "!|".to_string(),
line: self.line,
+ span: None,
});
} else {
return Ok(Token {
token_type: TokenType::Not,
lexeme: "!".to_string(),
line: self.line,
+ span: None,
});
}
}
@@ -320,6 +381,7 @@ impl<'a> Lexer<'a> {
token_type: TokenType::Xor,
lexeme: "^".to_string(),
line: self.line,
+ span: None,
})
}
'~' => {
@@ -328,12 +390,14 @@ impl<'a> Lexer<'a> {
token_type: TokenType::Xnor,
lexeme: "~^".to_string(),
line: self.line,
+ span: None,
});
} else {
return Ok(Token {
token_type: TokenType::BitwiseNot,
lexeme: "~".to_string(),
line: self.line,
+ span: None,
});
}
}
@@ -343,12 +407,14 @@ impl<'a> Lexer<'a> {
token_type: TokenType::NullCoalesce,
lexeme: "??".to_string(),
line: self.line,
+ span: None,
});
} else {
return Ok(Token {
token_type: TokenType::Condition,
lexeme: "?".to_string(),
line: self.line,
+ span: None,
});
}
}
@@ -357,6 +423,7 @@ impl<'a> Lexer<'a> {
token_type: TokenType::Comma,
lexeme: ",".to_string(),
line: self.line,
+ span: None,
})
}
'\'' => {
@@ -365,6 +432,7 @@ impl<'a> Lexer<'a> {
token_type: TokenType::CharLiteral(value),
lexeme: format!("'{}'", value),
line: self.line,
+ span: None,
});
}
'"' => {
@@ -373,108 +441,40 @@ impl<'a> Lexer<'a> {
token_type: TokenType::String(string_value.clone()),
lexeme: format!("\"{}\"", string_value),
line: self.line,
+ span: None,
});
}
- 'a'..='z' | 'A'..='Z' | '_' => {
- let ident = self.identifier();
+ ch if ch.is_alphabetic() || ch == '_' => {
+ let ident = self.identifier(c);
return Ok(self.keyword_or_ident_token(ident));
}
'0'..='9' => {
- if c == '0' && (self.peek() == 'b' || self.peek() == 'B') {
- self.advance(); // consume 'b' or 'B'
-
- let mut bin_str = String::new();
- while self.peek() == '0' || self.peek() == '1' {
- bin_str.push(self.advance());
- }
-
- if bin_str.is_empty() {
- return Err(
- self.make_error_here(
- WaveErrorKind::InvalidNumber("0b".to_string()),
- "invalid binary literal: expected at least one binary digit after `0b`",
- )
- .with_code("E1006")
- .with_label("missing binary digits")
- .with_help("example: `0b1011`"),
- );
- }
-
- return Ok(Token {
- token_type: TokenType::IntLiteral(format!("0b{}", bin_str)),
- lexeme: format!("0b{}", bin_str),
- line: self.line,
- });
- }
-
- if c == '0' && (self.peek() == 'x' || self.peek() == 'X') {
- self.advance(); // consume 'x' or 'X'
-
- let mut hex_str = String::new();
- while self.peek().is_ascii_hexdigit() {
- hex_str.push(self.advance());
- }
-
- if hex_str.is_empty() {
- return Err(
- self.make_error_here(
- WaveErrorKind::InvalidNumber("0x".to_string()),
- "invalid hexadecimal literal: expected at least one hex digit after `0x`",
- )
- .with_code("E1006")
- .with_label("missing hexadecimal digits")
- .with_help("example: `0x1FF`"),
- );
- }
-
- return Ok(Token {
- token_type: TokenType::IntLiteral(format!("0x{}", hex_str)),
- lexeme: format!("0x{}", hex_str),
- line: self.line,
- });
- }
-
- let mut num_str = c.to_string();
- while self.peek().is_ascii_digit() {
- num_str.push(self.advance());
- }
-
- let is_float = if self.peek() == '.' {
- num_str.push('.');
+ let start = self.current - 1;
+ let radix =
+ c == '0' && matches!(self.peek(), 'x' | 'X' | 'o' | 'O' | 'b' | 'B');
+ while self.peek().is_ascii_alphanumeric()
+ || self.peek() == '_'
+ || (!radix && self.peek() == '.')
+ || (!radix
+ && matches!(self.peek(), '+' | '-')
+ && self.source[..self.current].ends_with(['e', 'E']))
+ {
self.advance();
- while self.peek().is_ascii_digit() {
- num_str.push(self.advance());
- }
- true
- } else {
- false
- };
-
- let token_type = if is_float {
- match num_str.parse::() {
- Ok(v) => TokenType::Float(v),
- Err(_) => {
- return Err(self
- .make_error_here(
- WaveErrorKind::InvalidNumber(num_str.clone()),
- format!("invalid floating-point literal `{}`", num_str),
- )
- .with_code("E1006")
- .with_label("cannot parse float literal")
- .with_help("check decimal point placement and digits"));
- }
- }
+ }
+ let raw = &self.source[start..self.current];
+ let token_type = if crate::number::IntegerLiteral::parse(raw).is_some() {
+ Some(TokenType::IntLiteral(raw.replace('_', "")))
} else {
- TokenType::IntLiteral(num_str.clone())
+ crate::number::parse_float(raw).map(TokenType::Float)
};
-
- return Ok(Token {
- token_type,
- lexeme: num_str,
- line: self.line,
- });
+ return token_type.map(|token_type| Token {
+ token_type, lexeme: raw.to_string(), line: self.line, span: None,
+ }).ok_or_else(|| self.make_error(
+ WaveErrorKind::InvalidNumber(raw.to_string()),
+ format!("invalid numeric literal `{raw}`"), self.line, self.column_at(start),
+ ).with_code("E1006").with_help("use binary, octal, decimal or hexadecimal digits; separate digits with a single underscore; floats use decimal fractions or exponents, without suffixes"));
}
_ => {
diff --git a/front/lexer/src/token.rs b/front/lexer/src/token.rs
index 8b20e7ce..8489eb8a 100644
--- a/front/lexer/src/token.rs
+++ b/front/lexer/src/token.rs
@@ -196,3 +196,22 @@ pub enum TokenType {
Null,
Clobber,
}
+
+impl TokenType {
+ /// Reserved spellings have no executable Alpha grammar production.
+ pub fn reserved_spelling(&self) -> Option<&'static str> {
+ match self {
+ Self::Module => Some("module"),
+ Self::Class => Some("class"),
+ Self::Is => Some("is"),
+ Self::Xnand => Some("xnand"),
+ Self::Xnor => Some("~^"),
+ Self::Nand => Some("!&"),
+ Self::Nor => Some("!|"),
+ Self::Condition => Some("?"),
+ Self::NullCoalesce => Some("??"),
+ Self::Conditional => Some("?:"),
+ _ => None,
+ }
+ }
+}
diff --git a/front/lexer/tests/numeric_contract.rs b/front/lexer/tests/numeric_contract.rs
new file mode 100644
index 00000000..790748ca
--- /dev/null
+++ b/front/lexer/tests/numeric_contract.rs
@@ -0,0 +1,58 @@
+//! Shared numeric grammar and conversions.
+use lexer::number::{parse_float, IntegerLiteral};
+use lexer::token::TokenType;
+use lexer::Lexer;
+
+#[test]
+fn numeric_tokens_and_shared_values_agree() {
+ for (raw, value) in [
+ ("16", 16),
+ ("0x10", 16),
+ ("0Xf_F", 255),
+ ("0o20", 16),
+ ("0O2_0", 16),
+ ("0b1_0000", 16),
+ ("1_024", 1024),
+ ] {
+ let number = IntegerLiteral::parse(raw).unwrap();
+ assert_eq!(number.to_i128(), Some(value));
+ let tokens = Lexer::new(raw).tokenize().unwrap();
+ let TokenType::IntLiteral(normalized) = &tokens[0].token_type else {
+ panic!()
+ };
+ assert_eq!(IntegerLiteral::parse(normalized), Some(number));
+ }
+ assert_eq!(
+ IntegerLiteral::parse("-170141183460469231731687303715884105728")
+ .unwrap()
+ .to_i128(),
+ Some(i128::MIN)
+ );
+ assert_eq!(
+ IntegerLiteral::parse("-9223372036854775808")
+ .unwrap()
+ .to_f64(),
+ Some(i64::MIN as f64)
+ );
+ for (raw, value) in [
+ ("1.0", 1.0),
+ ("1e3", 1000.0),
+ ("1E-3", 0.001),
+ ("1_000.2_5e+2", 100025.0),
+ ] {
+ assert_eq!(parse_float(raw), Some(value));
+ assert!(
+ matches!(Lexer::new(raw).tokenize().unwrap()[0].token_type,TokenType::Float(n) if n == value)
+ );
+ }
+}
+
+#[test]
+fn malformed_numbers_are_one_lexical_error() {
+ for raw in [
+ "0x", "0b2", "0b102", "0o8", "0xg", "0x_1", "1_", "1__2", "1_.2", "1._2", "1e", "1e+",
+ "1e_2", "1e+-2", "1.2.3", "1.", "1u32", "1.0f64", "1e309", "0x1p2", "0b11name",
+ ] {
+ assert!(Lexer::new(raw).tokenize().is_err(), "{raw}");
+ }
+}
diff --git a/front/parser/src/ast.rs b/front/parser/src/ast.rs
index 2b48ba49..2ba4c309 100644
--- a/front/parser/src/ast.rs
+++ b/front/parser/src/ast.rs
@@ -19,15 +19,11 @@
use std::collections::HashMap;
-#[derive(Debug, Clone)]
-pub enum Value {
- Int(i64),
- Float(f64),
- Text(String),
-}
-
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WaveType {
+ /// Target-sized integers remain symbolic until the target-resolution pass.
+ Isz,
+ Usz,
Int(u16),
Uint(u16),
Float(u16),
@@ -38,12 +34,18 @@ pub enum WaveType {
Pointer(Box),
Array(Box, u32),
Void,
+ /// A function that cannot return to its caller (return position only).
+ Never,
Struct(String),
Variant(String),
}
#[derive(Debug, Clone)]
pub enum ASTNode {
+ Located {
+ value: Box,
+ span: error::SourceSpan,
+ },
Function(FunctionNode),
ExternFunction(ExternFunctionNode),
Program(ParameterNode),
@@ -81,6 +83,7 @@ pub struct EnumNode {
#[derive(Debug, Clone)]
pub struct EnumVariantNode {
+ pub span: Option,
pub name: String,
pub explicit_value: Option,
}
@@ -95,16 +98,19 @@ pub struct VariantNode {
#[derive(Debug, Clone)]
pub struct VariantCaseNode {
+ pub span: Option,
pub name: String,
pub payload_types: Vec,
}
#[derive(Debug, Clone)]
pub struct FunctionNode {
+ pub span: Option,
pub name: String,
pub generic_params: Vec,
pub parameters: Vec,
pub return_type: Option,
+ pub return_type_span: Option,
pub body: Vec,
pub export: Option,
pub visibility: Visibility,
@@ -121,6 +127,7 @@ pub struct StructNode {
pub name: String,
pub generic_params: Vec,
pub fields: Vec<(String, WaveType)>,
+ pub field_spans: Vec