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>, pub methods: Vec, pub visibility: Visibility, } @@ -140,9 +147,10 @@ pub struct FunctionSignature { #[derive(Debug, Clone)] pub struct ParameterNode { + pub span: Option, pub name: String, pub param_type: WaveType, - pub initial_value: Option, + pub initial_value: Option, } #[derive(Debug, Clone)] @@ -171,6 +179,10 @@ pub enum IncDecKind { #[derive(Debug, Clone)] pub enum Expression { + Located { + value: Box, + span: error::SourceSpan, + }, StructLiteral { name: String, fields: Vec<(String, Expression)>, @@ -283,6 +295,10 @@ pub enum AssignOperator { #[derive(Debug, Clone)] pub enum MatchPattern { + Located { + value: Box, + span: error::SourceSpan, + }, Int(String), Ident(String), Binding(String), @@ -296,6 +312,7 @@ pub enum MatchPattern { #[derive(Debug, Clone)] pub struct MatchArm { + pub span: Option, pub pattern: MatchPattern, pub body: Vec, } @@ -387,7 +404,7 @@ pub struct VariableInfo { impl Expression { pub fn as_identifier(&self) -> Option<&str> { - match self { + match self.unspanned() { Expression::Variable(name) => Some(name.as_str()), Expression::AddressOf(inner) => { if let Expression::Variable(name) = &**inner { @@ -401,7 +418,7 @@ impl Expression { } pub fn get_wave_type(&self, variables: &HashMap) -> WaveType { - match self { + match self.unspanned() { Expression::Variable(name) => variables .get(name) .unwrap_or_else(|| panic!("Variable '{}' not found", name)) @@ -432,3 +449,117 @@ impl Expression { } } } + +impl ASTNode { + pub fn unspanned(&self) -> &Self { + match self { + Self::Located { value, .. } => value.unspanned(), + other => other, + } + } + pub fn into_unspanned(self) -> Self { + match self { + Self::Located { value, .. } => value.into_unspanned(), + other => other, + } + } + pub fn span(&self) -> Option<&error::SourceSpan> { + match self { + Self::Located { span, .. } => Some(span), + _ => None, + } + } + pub fn with_span(self, span: Option) -> Self { + if self.span() == span.as_ref() { + return self; + } + match span { + Some(span) => Self::Located { + value: Box::new(self), + span, + }, + None => self, + } + } +} + +impl Expression { + pub fn unspanned(&self) -> &Self { + match self { + Self::Located { value, .. } => value.unspanned(), + other => other, + } + } + pub fn into_unspanned(self) -> Self { + match self { + Self::Located { value, .. } => value.into_unspanned(), + other => other, + } + } + pub fn span(&self) -> Option<&error::SourceSpan> { + match self { + Self::Located { span, .. } => Some(span), + _ => None, + } + } + pub fn with_span(self, span: Option) -> Self { + if self.span() == span.as_ref() { + return self; + } + match span { + Some(span) => Self::Located { + value: Box::new(self), + span, + }, + None => self, + } + } +} + +impl MatchPattern { + pub fn unspanned(&self) -> &Self { + match self { + Self::Located { value, .. } => value.unspanned(), + other => other, + } + } + pub fn into_unspanned(self) -> Self { + match self { + Self::Located { value, .. } => value.into_unspanned(), + other => other, + } + } + pub fn span(&self) -> Option<&error::SourceSpan> { + match self { + Self::Located { span, .. } => Some(span), + _ => None, + } + } + pub fn with_span(self, span: Option) -> Self { + if self.span() == span.as_ref() { + return self; + } + match span { + Some(span) => Self::Located { + value: Box::new(self), + span, + }, + None => self, + } + } +} + +impl Expression { + pub fn binary(left: Expression, operator: Operator, right: Expression) -> Self { + let span = left + .span() + .zip(right.span()) + .map(|(first, last)| first.through(last)); + Self::BinaryExpression { + left: Box::new(left), + operator, + right: Box::new(right), + } + .with_span(span) + } +} diff --git a/front/parser/src/expr/assign.rs b/front/parser/src/expr/assign.rs index 8af46f47..5fc097ad 100644 --- a/front/parser/src/expr/assign.rs +++ b/front/parser/src/expr/assign.rs @@ -31,28 +31,32 @@ pub fn parse_assignment_expression<'a, T>(tokens: &mut std::iter::Peekable) - where T: Iterator + Clone, { - let left = parse_logical_or_expression(tokens)?; - - if let Some(token) = tokens.peek() { - let op = match token.token_type { - TokenType::Equal => AssignOperator::Assign, - TokenType::PlusEq => AssignOperator::AddAssign, - TokenType::MinusEq => AssignOperator::SubAssign, - TokenType::StarEq => AssignOperator::MulAssign, - TokenType::DivEq => AssignOperator::DivAssign, - TokenType::RemainderEq => AssignOperator::RemAssign, - _ => return Some(left), - }; - - tokens.next(); // consume op - - let right = parse_assignment_expression(tokens)?; - return Some(Expression::AssignOperation { - target: Box::new(left), - operator: op, - value: Box::new(right), - }); - } - - Some(left) + let before = tokens.clone(); + let result = (|| { + let left = parse_logical_or_expression(tokens)?; + + if let Some(token) = tokens.peek() { + let op = match token.token_type { + TokenType::Equal => AssignOperator::Assign, + TokenType::PlusEq => AssignOperator::AddAssign, + TokenType::MinusEq => AssignOperator::SubAssign, + TokenType::StarEq => AssignOperator::MulAssign, + TokenType::DivEq => AssignOperator::DivAssign, + TokenType::RemainderEq => AssignOperator::RemAssign, + _ => return Some(left), + }; + + tokens.next(); // consume op + + let right = parse_assignment_expression(tokens)?; + return Some(Expression::AssignOperation { + target: Box::new(left), + operator: op, + value: Box::new(right), + }); + } + + Some(left) + })(); + result.map(|value: Expression| value.with_span(lexer::consumed_span(before, tokens))) } diff --git a/front/parser/src/expr/binary.rs b/front/parser/src/expr/binary.rs index 3d25b460..4062fca3 100644 --- a/front/parser/src/expr/binary.rs +++ b/front/parser/src/expr/binary.rs @@ -35,11 +35,7 @@ where ) { tokens.next(); let right = parse_logical_and_expression(tokens)?; - left = Expression::BinaryExpression { - left: Box::new(left), - operator: Operator::LogicalOr, - right: Box::new(right), - }; + left = Expression::binary(left, Operator::LogicalOr, right); } Some(left) @@ -59,11 +55,7 @@ where ) { tokens.next(); let right = parse_bitwise_or_expression(tokens)?; - left = Expression::BinaryExpression { - left: Box::new(left), - operator: Operator::LogicalAnd, - right: Box::new(right), - }; + left = Expression::binary(left, Operator::LogicalAnd, right); } Some(left) @@ -81,11 +73,7 @@ where ) { tokens.next(); let right = parse_bitwise_xor_expression(tokens)?; - left = Expression::BinaryExpression { - left: Box::new(left), - operator: Operator::BitwiseOr, - right: Box::new(right), - }; + left = Expression::binary(left, Operator::BitwiseOr, right); } Some(left) @@ -102,11 +90,7 @@ where while matches!(tokens.peek().map(|t| &t.token_type), Some(TokenType::Xor)) { tokens.next(); let right = parse_bitwise_and_expression(tokens)?; - left = Expression::BinaryExpression { - left: Box::new(left), - operator: Operator::BitwiseXor, - right: Box::new(right), - }; + left = Expression::binary(left, Operator::BitwiseXor, right); } Some(left) @@ -126,11 +110,7 @@ where ) { tokens.next(); let right = parse_equality_expression(tokens)?; - left = Expression::BinaryExpression { - left: Box::new(left), - operator: Operator::BitwiseAnd, - right: Box::new(right), - }; + left = Expression::binary(left, Operator::BitwiseAnd, right); } Some(left) @@ -150,11 +130,7 @@ where }; tokens.next(); let right = parse_relational_expression(tokens)?; - left = Expression::BinaryExpression { - left: Box::new(left), - operator: op, - right: Box::new(right), - }; + left = Expression::binary(left, op, right); } Some(left) @@ -176,11 +152,7 @@ where }; tokens.next(); let right = parse_shift_expression(tokens)?; - left = Expression::BinaryExpression { - left: Box::new(left), - operator: op, - right: Box::new(right), - }; + left = Expression::binary(left, op, right); } Some(left) @@ -201,11 +173,7 @@ where tokens.next(); let right = parse_additive_expression(tokens)?; - left = Expression::BinaryExpression { - left: Box::new(left), - operator: op, - right: Box::new(right), - }; + left = Expression::binary(left, op, right); } Some(left) @@ -225,11 +193,7 @@ where }; tokens.next(); let right = parse_multiplicative_expression(tokens)?; - left = Expression::BinaryExpression { - left: Box::new(left), - operator: op, - right: Box::new(right), - }; + left = Expression::binary(left, op, right); } Some(left) @@ -252,11 +216,7 @@ where }; tokens.next(); let right = parse_cast_expression(tokens)?; - left = Expression::BinaryExpression { - left: Box::new(left), - operator: op, - right: Box::new(right), - }; + left = Expression::binary(left, op, right); } Some(left) @@ -269,12 +229,20 @@ where let mut expr = parse_unary_expression(tokens)?; while matches!(tokens.peek().map(|t| &t.token_type), Some(TokenType::As)) { + let before = tokens.clone(); + let first = expr.span().cloned(); tokens.next(); // consume `as` let target_type = parse_type_from_stream(tokens)?; expr = Expression::Cast { expr: Box::new(expr), target_type, - }; + } + .with_span( + first + .as_ref() + .zip(lexer::consumed_span(before, tokens)) + .map(|(first, last)| first.through(&last)), + ); } Some(expr) diff --git a/front/parser/src/expr/helpers.rs b/front/parser/src/expr/helpers.rs index 9b4f6347..50680d33 100644 --- a/front/parser/src/expr/helpers.rs +++ b/front/parser/src/expr/helpers.rs @@ -25,7 +25,7 @@ use std::iter::Peekable; use std::slice::Iter; pub fn is_assignable(expr: &Expression) -> bool { - match expr { + match expr.unspanned() { Expression::Variable(_) => true, Expression::Deref(_) => true, Expression::FieldAccess { .. } => true, diff --git a/front/parser/src/expr/postfix.rs b/front/parser/src/expr/postfix.rs index 5d0fcf77..47941f14 100644 --- a/front/parser/src/expr/postfix.rs +++ b/front/parser/src/expr/postfix.rs @@ -31,6 +31,8 @@ pub fn parse_postfix_expression<'a, T>( where T: Iterator + Clone, { + let first = expr.span().cloned(); + let before = tokens.clone(); loop { match tokens.peek().map(|t| &t.token_type) { Some(TokenType::Dot) => { @@ -165,6 +167,11 @@ where _ => break, } + let span = first + .as_ref() + .zip(lexer::consumed_span(before.clone(), tokens)) + .map(|(first, last)| first.through(&last)); + expr = expr.with_span(span); } Some(expr) diff --git a/front/parser/src/expr/primary.rs b/front/parser/src/expr/primary.rs index 1f9b9b50..0bbfbb32 100644 --- a/front/parser/src/expr/primary.rs +++ b/front/parser/src/expr/primary.rs @@ -133,333 +133,350 @@ pub fn parse_primary_expression<'a, T>(tokens: &mut Peekable) -> Option + Clone, { - let token = (*tokens.peek()?).clone(); + let before = tokens.clone(); + let result = (|| { + let token = (*tokens.peek()?).clone(); - let expr = match &token.token_type { - TokenType::IntLiteral(s) => { - tokens.next(); - Some(Expression::Literal(Literal::Int(s.clone()))) - } - TokenType::Float(value) => { - tokens.next(); - Some(Expression::Literal(Literal::Float(*value))) - } - TokenType::CharLiteral(c) => { - tokens.next(); - Some(Expression::Literal(Literal::Char(*c))) - } - TokenType::BoolLiteral(b) => { - tokens.next(); - Some(Expression::Literal(Literal::Bool(*b))) - } - TokenType::Null => { - tokens.next(); - Some(Expression::Null) - } - TokenType::Identifier(name) => { - let mut name = name.clone(); - tokens.next(); - - while matches!( - tokens.peek().map(|token| &token.token_type), - Some(TokenType::DoubleColon) - ) { + let expr = match &token.token_type { + TokenType::IntLiteral(s) => { tokens.next(); - let segment = match tokens.next() { - Some(Token { - token_type: TokenType::Identifier(segment), - .. - }) => segment, - _ => { - println!("Error: Expected identifier after '::'"); - return None; - } - }; - name.push_str("::"); - name.push_str(segment); + Some(Expression::Literal(Literal::Int(s.clone()))) } + TokenType::Float(value) => { + tokens.next(); + Some(Expression::Literal(Literal::Float(*value))) + } + TokenType::CharLiteral(c) => { + tokens.next(); + Some(Expression::Literal(Literal::Char(*c))) + } + TokenType::BoolLiteral(b) => { + tokens.next(); + Some(Expression::Literal(Literal::Bool(*b))) + } + TokenType::Null => { + tokens.next(); + Some(Expression::Null) + } + TokenType::Identifier(name) => { + let mut name = name.clone(); + tokens.next(); - let expr = if let Some(peeked_token) = tokens.peek() { - match &peeked_token.token_type { - TokenType::Lchevr if peek_is_generic_call(tokens) => { - tokens.next(); // consume '<' - let inner = collect_generic_inner(tokens)?; - let arg_strs = split_top_level_generic_args(&inner)?; - - let mut type_args = Vec::with_capacity(arg_strs.len()); - for arg in arg_strs { - let tt = parse_type(&arg)?; - let wt = token_type_to_wave_type(&tt)?; - type_args.push(wt); - } - - skip_ws(tokens); - if tokens - .peek() - .map_or(true, |t| t.token_type != TokenType::Lparen) - { - println!("Error: Expected '(' after generic function type arguments"); + while matches!( + tokens.peek().map(|token| &token.token_type), + Some(TokenType::DoubleColon) + ) { + tokens.next(); + let segment = match tokens.next() { + Some(Token { + token_type: TokenType::Identifier(segment), + .. + }) => segment, + _ => { + println!("Error: Expected identifier after '::'"); return None; } - tokens.next(); // consume '(' + }; + name.push_str("::"); + name.push_str(segment); + } - let mut args = vec![]; - if tokens - .peek() - .map_or(false, |t| t.token_type != TokenType::Rparen) - { - loop { - let arg = parse_expression(tokens)?; - args.push(arg); - - if let Some(Token { - token_type: TokenType::Comma, - .. - }) = tokens.peek() - { - tokens.next(); - } else { - break; + let expr = if let Some(peeked_token) = tokens.peek() { + match &peeked_token.token_type { + TokenType::Lchevr if peek_is_generic_call(tokens) => { + tokens.next(); // consume '<' + let inner = collect_generic_inner(tokens)?; + let arg_strs = split_top_level_generic_args(&inner)?; + + let mut type_args = Vec::with_capacity(arg_strs.len()); + for arg in arg_strs { + let tt = parse_type(&arg)?; + let wt = token_type_to_wave_type(&tt)?; + type_args.push(wt); + } + + skip_ws(tokens); + if tokens + .peek() + .map_or(true, |t| t.token_type != TokenType::Lparen) + { + println!( + "Error: Expected '(' after generic function type arguments" + ); + return None; + } + tokens.next(); // consume '(' + + let mut args = vec![]; + if tokens + .peek() + .map_or(false, |t| t.token_type != TokenType::Rparen) + { + loop { + let arg = parse_expression(tokens)?; + args.push(arg); + + if let Some(Token { + token_type: TokenType::Comma, + .. + }) = tokens.peek() + { + tokens.next(); + } else { + break; + } } } - } - if tokens - .peek() - .map_or(true, |t| t.token_type != TokenType::Rparen) - { - println!("Error: Expected ')' after function call arguments"); - return None; - } - tokens.next(); + if tokens + .peek() + .map_or(true, |t| t.token_type != TokenType::Rparen) + { + println!("Error: Expected ')' after function call arguments"); + return None; + } + tokens.next(); - Expression::FunctionCall { - name, - type_args, - args, - } - } - TokenType::Lchevr if peek_is_generic_struct_literal(tokens) => { - tokens.next(); // consume '<' - let inner = collect_generic_inner(tokens)?; - let arg_strs = split_top_level_generic_args(&inner)?; - - // Validate the application here so malformed type arguments - // fail as syntax instead of becoming an opaque struct name. - for arg in &arg_strs { - let token_type = parse_type(arg)?; - token_type_to_wave_type(&token_type)?; + Expression::FunctionCall { + name, + type_args, + args, + } } + TokenType::Lchevr if peek_is_generic_struct_literal(tokens) => { + tokens.next(); // consume '<' + let inner = collect_generic_inner(tokens)?; + let arg_strs = split_top_level_generic_args(&inner)?; + + // Validate the application here so malformed type arguments + // fail as syntax instead of becoming an opaque struct name. + for arg in &arg_strs { + let token_type = parse_type(arg)?; + token_type_to_wave_type(&token_type)?; + } - skip_ws(tokens); - if tokens - .peek() - .is_none_or(|token| token.token_type != TokenType::Lbrace) - { - println!("Error: Expected '{{' after generic struct type arguments"); - return None; - } - tokens.next(); // consume '{' + skip_ws(tokens); + if tokens + .peek() + .is_none_or(|token| token.token_type != TokenType::Lbrace) + { + println!( + "Error: Expected '{{' after generic struct type arguments" + ); + return None; + } + tokens.next(); // consume '{' - name.push('<'); - name.push_str(&arg_strs.join(",")); - name.push('>'); + name.push('<'); + name.push_str(&arg_strs.join(",")); + name.push('>'); - Expression::StructLiteral { - name, - fields: parse_struct_literal_fields(tokens)?, + Expression::StructLiteral { + name, + fields: parse_struct_literal_fields(tokens)?, + } } - } - TokenType::Lparen => { - tokens.next(); - - let mut args = vec![]; - if tokens - .peek() - .map_or(false, |t| t.token_type != TokenType::Rparen) - { - loop { - let arg = parse_expression(tokens)?; - args.push(arg); - - if let Some(Token { - token_type: TokenType::Comma, - .. - }) = tokens.peek() - { - tokens.next(); - } else { - break; + TokenType::Lparen => { + tokens.next(); + + let mut args = vec![]; + if tokens + .peek() + .map_or(false, |t| t.token_type != TokenType::Rparen) + { + loop { + let arg = parse_expression(tokens)?; + args.push(arg); + + if let Some(Token { + token_type: TokenType::Comma, + .. + }) = tokens.peek() + { + tokens.next(); + } else { + break; + } } } - } - if tokens - .peek() - .map_or(true, |t| t.token_type != TokenType::Rparen) - { - println!("Error: Expected ')' after function call arguments"); - return None; - } - tokens.next(); + if tokens + .peek() + .map_or(true, |t| t.token_type != TokenType::Rparen) + { + println!("Error: Expected ')' after function call arguments"); + return None; + } + tokens.next(); - Expression::FunctionCall { - name, - type_args: Vec::new(), - args, + Expression::FunctionCall { + name, + type_args: Vec::new(), + args, + } } - } - TokenType::Lbrace => { - tokens.next(); - Expression::StructLiteral { - name, - fields: parse_struct_literal_fields(tokens)?, + TokenType::Lbrace => { + tokens.next(); + Expression::StructLiteral { + name, + fields: parse_struct_literal_fields(tokens)?, + } } + _ => Expression::Variable(name), } - _ => Expression::Variable(name), - } - } else { - Expression::Variable(name) - }; + } else { + Expression::Variable(name) + }; - Some(expr) - } - TokenType::Lparen => { - tokens.next(); - let inner_expr = parse_expression(tokens)?; - if tokens - .peek() - .map_or(true, |t| t.token_type != TokenType::Rparen) - { - println!("Error: Expected ')' to close grouped expression"); - return None; + Some(expr) } - tokens.next(); - Some(Expression::Grouped(Box::new(inner_expr))) - } - TokenType::String(value) => { - tokens.next(); - Some(Expression::Literal(Literal::String(value.clone()))) - } - TokenType::Lbrack => { - tokens.next(); - let mut elements = vec![]; - if tokens - .peek() - .map_or(false, |t| t.token_type != TokenType::Rbrack) - { - loop { - elements.push(parse_expression(tokens)?); - if let Some(Token { - token_type: TokenType::Comma, - .. - }) = tokens.peek() - { - tokens.next(); - } else { - break; - } + TokenType::Lparen => { + tokens.next(); + let inner_expr = parse_expression(tokens)?; + if tokens + .peek() + .map_or(true, |t| t.token_type != TokenType::Rparen) + { + println!("Error: Expected ')' to close grouped expression"); + return None; } + tokens.next(); + Some(Expression::Grouped(Box::new(inner_expr))) } - if tokens - .peek() - .map_or(true, |t| t.token_type != TokenType::Rbrack) - { - println!("Error: Expected ']' to close array literal"); - return None; + TokenType::String(value) => { + tokens.next(); + Some(Expression::Literal(Literal::String(value.clone()))) } - tokens.next(); - Some(Expression::ArrayLiteral(elements)) - } - TokenType::Asm => { - tokens.next(); - if tokens.peek()?.token_type != TokenType::Lbrace { - println!("Expected '{{' after 'asm'"); - return None; + TokenType::Lbrack => { + tokens.next(); + let mut elements = vec![]; + if tokens + .peek() + .map_or(false, |t| t.token_type != TokenType::Rbrack) + { + loop { + elements.push(parse_expression(tokens)?); + if let Some(Token { + token_type: TokenType::Comma, + .. + }) = tokens.peek() + { + tokens.next(); + } else { + break; + } + } + } + if tokens + .peek() + .map_or(true, |t| t.token_type != TokenType::Rbrack) + { + println!("Error: Expected ']' to close array literal"); + return None; + } + tokens.next(); + Some(Expression::ArrayLiteral(elements)) } - tokens.next(); - - let mut instructions: Vec = vec![]; - let mut inputs: Vec<(String, Expression)> = vec![]; - let mut outputs: Vec<(String, Expression)> = vec![]; - let mut clobbers: Vec = vec![]; + TokenType::Asm => { + tokens.next(); + if tokens.peek()?.token_type != TokenType::Lbrace { + println!("Expected '{{' after 'asm'"); + return None; + } + tokens.next(); - while let Some(token) = tokens.peek() { - match &token.token_type { - TokenType::Rbrace => { - tokens.next(); - break; - } + let mut instructions: Vec = vec![]; + let mut inputs: Vec<(String, Expression)> = vec![]; + let mut outputs: Vec<(String, Expression)> = vec![]; + let mut clobbers: Vec = vec![]; + + let mut closed = false; + while let Some(token) = tokens.peek() { + match &token.token_type { + TokenType::Rbrace => { + tokens.next(); + closed = true; + break; + } - TokenType::In => { - tokens.next(); - parse_asm_inout_clause(tokens, true, &mut inputs, &mut outputs)?; - } + TokenType::In => { + tokens.next(); + parse_asm_inout_clause(tokens, true, &mut inputs, &mut outputs)?; + } - TokenType::Out => { - tokens.next(); - parse_asm_inout_clause(tokens, false, &mut inputs, &mut outputs)?; - } + TokenType::Out => { + tokens.next(); + parse_asm_inout_clause(tokens, false, &mut inputs, &mut outputs)?; + } - TokenType::Clobber => { - tokens.next(); - parse_asm_clobber_clause(tokens, &mut clobbers)?; - } + TokenType::Clobber => { + tokens.next(); + parse_asm_clobber_clause(tokens, &mut clobbers)?; + } - TokenType::Identifier(s) if s == "in" => { - tokens.next(); - parse_asm_inout_clause(tokens, true, &mut inputs, &mut outputs)?; - } + TokenType::Identifier(s) if s == "in" => { + tokens.next(); + parse_asm_inout_clause(tokens, true, &mut inputs, &mut outputs)?; + } - TokenType::Identifier(s) if s == "out" => { - tokens.next(); - parse_asm_inout_clause(tokens, false, &mut inputs, &mut outputs)?; - } + TokenType::Identifier(s) if s == "out" => { + tokens.next(); + parse_asm_inout_clause(tokens, false, &mut inputs, &mut outputs)?; + } - TokenType::Identifier(s) if s == "clobber" => { - tokens.next(); - parse_asm_clobber_clause(tokens, &mut clobbers)?; - } + TokenType::Identifier(s) if s == "clobber" => { + tokens.next(); + parse_asm_clobber_clause(tokens, &mut clobbers)?; + } - TokenType::String(s) => { - instructions.push(s.clone()); - tokens.next(); - } + TokenType::String(s) => { + instructions.push(s.clone()); + tokens.next(); + } - other => { - println!("Unexpected token in asm expression: {:?}", other); - tokens.next(); + TokenType::SemiColon | TokenType::Comma => { + tokens.next(); + } + other => { + println!("Unexpected token in asm expression: {:?}", other); + return None; + } } } - } - Some(Expression::AsmBlock { - instructions, - inputs, - outputs, - clobbers, - }) - } - _ => match token.token_type { - TokenType::Continue | TokenType::Break | TokenType::Return | TokenType::SemiColon => { - None - } - _ => { - println!( - "Error: Expected primary expression, found {:?}", - token.token_type - ); - println!( - "Error: Expected primary expression, found {:?}", - token.lexeme - ); - println!("Error: Expected primary expression, found {:?}", token.line); - None + if !closed { + return None; + } + Some(Expression::AsmBlock { + instructions, + inputs, + outputs, + clobbers, + }) } - }, - }; + _ => match token.token_type { + TokenType::Continue + | TokenType::Break + | TokenType::Return + | TokenType::SemiColon => None, + _ => { + println!( + "Error: Expected primary expression, found {:?}", + token.token_type + ); + println!( + "Error: Expected primary expression, found {:?}", + token.lexeme + ); + println!("Error: Expected primary expression, found {:?}", token.line); + None + } + }, + }; - let base = expr?; + let base = expr?.with_span(lexer::consumed_span(before.clone(), tokens)); - parse_postfix_expression(tokens, base) + parse_postfix_expression(tokens, base) + })(); + result.map(|value: Expression| value.with_span(lexer::consumed_span(before, tokens))) } diff --git a/front/parser/src/expr/unary.rs b/front/parser/src/expr/unary.rs index 860becb3..a1bbdc27 100644 --- a/front/parser/src/expr/unary.rs +++ b/front/parser/src/expr/unary.rs @@ -25,87 +25,95 @@ pub fn parse_unary_expression<'a, T>(tokens: &mut std::iter::Peekable) -> Opt where T: Iterator + Clone, { - if let Some(token) = tokens.peek() { - match token.token_type { - TokenType::Not => { - tokens.next(); - let inner = parse_unary_expression(tokens)?; - return Some(Expression::Unary { - operator: Operator::Not, - expr: Box::new(inner), - }); - } - TokenType::BitwiseNot => { - tokens.next(); - let inner = parse_unary_expression(tokens)?; - return Some(Expression::Unary { - operator: Operator::BitwiseNot, - expr: Box::new(inner), - }); - } - TokenType::AddressOf => { - tokens.next(); - let inner = parse_unary_expression(tokens)?; - return Some(Expression::AddressOf(Box::new(inner))); - } - TokenType::Deref => { - tokens.next(); - let inner = parse_unary_expression(tokens)?; - return Some(Expression::Deref(Box::new(inner))); - } - TokenType::Increment => { - let tok = tokens.next()?; // '++' - let inner = parse_unary_expression(tokens)?; - if !is_assignable(&inner) { - println!("Error: ++ target must be assignable (line {})", tok.line); - return None; + let before = tokens.clone(); + let result = (|| { + if let Some(token) = tokens.peek() { + match token.token_type { + TokenType::Not => { + tokens.next(); + let inner = parse_unary_expression(tokens)?; + return Some(Expression::Unary { + operator: Operator::Not, + expr: Box::new(inner), + }); } - return Some(Expression::IncDec { - kind: IncDecKind::PreInc, - target: Box::new(inner), - }); - } - TokenType::Decrement => { - let tok = tokens.next()?; // '--' - let inner = parse_unary_expression(tokens)?; - if !is_assignable(&inner) { - println!("Error: -- target must be assignable (line {})", tok.line); - return None; + TokenType::BitwiseNot => { + tokens.next(); + let inner = parse_unary_expression(tokens)?; + return Some(Expression::Unary { + operator: Operator::BitwiseNot, + expr: Box::new(inner), + }); } - return Some(Expression::IncDec { - kind: IncDecKind::PreDec, - target: Box::new(inner), - }); - } - TokenType::Minus => { - let _tok = tokens.next()?; // '-' - let inner = parse_unary_expression(tokens)?; - - match inner { - Expression::Literal(Literal::Int(s)) => { - return Some(Expression::Literal(Literal::Int(format!("-{}", s)))); + TokenType::AddressOf => { + tokens.next(); + let inner = parse_unary_expression(tokens)?; + return Some(Expression::AddressOf(Box::new(inner))); + } + TokenType::Deref => { + tokens.next(); + let inner = parse_unary_expression(tokens)?; + return Some(Expression::Deref(Box::new(inner))); + } + TokenType::Increment => { + let tok = tokens.next()?; // '++' + let inner = parse_unary_expression(tokens)?; + if !is_assignable(&inner) { + println!("Error: ++ target must be assignable (line {})", tok.line); + return None; } - Expression::Literal(Literal::Float(f)) => { - return Some(Expression::Literal(Literal::Float(-f))); + return Some(Expression::IncDec { + kind: IncDecKind::PreInc, + target: Box::new(inner), + }); + } + TokenType::Decrement => { + let tok = tokens.next()?; // '--' + let inner = parse_unary_expression(tokens)?; + if !is_assignable(&inner) { + println!("Error: -- target must be assignable (line {})", tok.line); + return None; } + return Some(Expression::IncDec { + kind: IncDecKind::PreDec, + target: Box::new(inner), + }); + } + TokenType::Minus => { + let _tok = tokens.next()?; // '-' + let inner = parse_unary_expression(tokens)?; - other => { - return Some(Expression::Unary { - operator: Operator::Neg, - expr: Box::new(other), - }) + match inner.into_unspanned() { + Expression::Literal(Literal::Int(s)) => { + return Some(Expression::Literal(Literal::Int( + s.strip_prefix('-') + .map(str::to_string) + .unwrap_or_else(|| format!("-{s}")), + ))); + } + Expression::Literal(Literal::Float(f)) => { + return Some(Expression::Literal(Literal::Float(-f))); + } + + other => { + return Some(Expression::Unary { + operator: Operator::Neg, + expr: Box::new(other), + }) + } } } - } - TokenType::Plus => { - tokens.next(); // consume '+' - let inner = parse_unary_expression(tokens)?; - return Some(inner); + TokenType::Plus => { + tokens.next(); // consume '+' + let inner = parse_unary_expression(tokens)?; + return Some(inner); + } + _ => {} } - _ => {} } - } - parse_primary_expression(tokens) + parse_primary_expression(tokens) + })(); + result.map(|value: Expression| value.with_span(lexer::consumed_span(before, tokens))) } diff --git a/front/parser/src/generics.rs b/front/parser/src/generics.rs index 3b7e70ce..5cd41195 100644 --- a/front/parser/src/generics.rs +++ b/front/parser/src/generics.rs @@ -19,15 +19,16 @@ //! so repeated references resolve to one generated definition. use crate::ast::{ - ASTNode, EnumNode, Expression, ExternFunctionNode, FunctionNode, Literal, MatchArm, - MatchPattern, ParameterNode, ProtoImplNode, StatementNode, StructNode, TypeAliasNode, Value, - VariableNode, VariantNode, WaveType, + ASTNode, EnumNode, Expression, ExternFunctionNode, FunctionNode, MatchArm, MatchPattern, + ParameterNode, ProtoImplNode, StatementNode, StructNode, TypeAliasNode, VariableNode, + VariantNode, WaveType, }; use crate::types::{parse_type, split_top_level_generic_args, token_type_to_wave_type}; use std::collections::{BTreeMap, HashMap, HashSet}; #[derive(Default)] struct GenericEnv { + origin_spans: HashMap, // Templates are source definitions; instances are fully substituted nodes // that may be emitted. BTreeMap keeps generated output deterministic. function_templates: HashMap, @@ -57,7 +58,18 @@ pub fn monomorphize_generics(ast: Vec) -> Result, String> // Pass one records every callable signature and generic template before any // body is rewritten, allowing forward references between declarations. for node in &ast { - match node { + if let Some(span) = node.span() { + let name = match node.unspanned() { + ASTNode::Function(f) => Some(&f.name), + ASTNode::Struct(s) => Some(&s.name), + ASTNode::Variant(v) => Some(&v.name), + _ => None, + }; + if let Some(name) = name { + env.origin_spans.insert(name.clone(), span.clone()); + } + } + match node.unspanned() { ASTNode::Function(f) => { if env .function_parameters @@ -107,7 +119,10 @@ pub fn monomorphize_generics(ast: Vec) -> Result, String> // Pass two rewrites concrete roots. Referenced generic instances are added // to the environment recursively and appended after source declarations. for node in ast { - match node { + let span = node.span().cloned(); + let first_output = out.len(); + match node.into_unspanned() { + ASTNode::Located { .. } => unreachable!("unspanned root"), ASTNode::Function(f) => { if f.generic_params.is_empty() { out.push(ASTNode::Function(rewrite_function( @@ -194,16 +209,23 @@ pub fn monomorphize_generics(ast: Vec) -> Result, String> } ASTNode::Program(p) => out.push(ASTNode::Program(p)), } + for node in &mut out[first_output..] { + let value = std::mem::replace(node, ASTNode::Expression(Expression::Null)); + *node = value.with_span(span.clone()); + } } for (_, variant) in env.variant_instances { - out.push(ASTNode::Variant(variant)); + let span = env.origin_spans.get(&variant.name).cloned(); + out.push(ASTNode::Variant(variant).with_span(span)); } for (_, s) in env.struct_instances { - out.push(ASTNode::Struct(s)); + let span = env.origin_spans.get(&s.name).cloned(); + out.push(ASTNode::Struct(s).with_span(span)); } for (_, f) in env.function_instances { - out.push(ASTNode::Function(f)); + let span = env.origin_spans.get(&f.name).cloned(); + out.push(ASTNode::Function(f).with_span(span)); } Ok(out) @@ -215,6 +237,7 @@ fn rewrite_parameter( env: &mut GenericEnv, ) -> Result { Ok(ParameterNode { + span: param.span, name: param.name, param_type: rewrite_wave_type(¶m.param_type, subst, env)?, initial_value: param.initial_value, @@ -359,6 +382,9 @@ fn rewrite_node( env: &mut GenericEnv, ) -> Result { match node { + ASTNode::Located { value, span } => { + Ok(rewrite_node(*value, subst, env)?.with_span(Some(span))) + } ASTNode::Variable(v) => Ok(ASTNode::Variable(rewrite_variable(v, subst, env)?)), ASTNode::Statement(s) => Ok(ASTNode::Statement(rewrite_statement(s, subst, env)?)), ASTNode::Expression(e) => Ok(ASTNode::Expression(rewrite_expression(e, subst, env)?)), @@ -456,6 +482,7 @@ fn rewrite_statement( .into_iter() .map(|arm| { Ok(MatchArm { + span: arm.span, pattern: rewrite_pattern(arm.pattern), body: rewrite_node_list(arm.body, subst, env)?, }) @@ -530,6 +557,14 @@ fn rewrite_expression( env: &mut GenericEnv, ) -> Result { match expr { + Expression::Located { value, span } => { + let span = if subst.is_empty() { + span + } else { + span.generated("generic specialization") + }; + Ok(rewrite_expression(*value, subst, env)?.with_span(Some(span))) + } Expression::FunctionCall { name, type_args, @@ -685,20 +720,16 @@ fn append_default_arguments( .initial_value .as_ref() .ok_or_else(|| format!("function '{}' requires argument '{}'", name, parameter.name))?; - args.push(value_to_expression(default)); + let span = default + .span() + .cloned() + .map(|span| span.generated(format!("default argument for {name}"))); + args.push(default.clone().with_span(span)); } Ok(()) } -fn value_to_expression(value: &Value) -> Expression { - match value { - Value::Int(value) => Expression::Literal(Literal::Int(value.to_string())), - Value::Float(value) => Expression::Literal(Literal::Float(*value)), - Value::Text(value) => Expression::Literal(Literal::String(value.clone())), - } -} - fn rewrite_wave_type( ty: &WaveType, subst: &HashMap, @@ -802,6 +833,12 @@ fn ensure_variant_instance( .collect::>() .join(",") ); + if let Some(span) = env.origin_spans.get(base).cloned() { + env.origin_spans.insert( + instance_name.clone(), + span.generated(format!("specialization of {base}")), + ); + } if env.variant_instances.contains_key(&instance_name) || env.variant_in_progress.contains(&instance_name) { @@ -860,6 +897,12 @@ fn ensure_struct_instance( } let inst_name = mangle_instance_name(base, args); + if let Some(span) = env.origin_spans.get(base).cloned() { + env.origin_spans.insert( + inst_name.clone(), + span.generated(format!("specialization of {base}")), + ); + } if env.struct_instances.contains_key(&inst_name) { return Ok(inst_name); } @@ -912,6 +955,12 @@ fn ensure_function_instance( } let inst_name = mangle_instance_name(base, args); + if let Some(span) = env.origin_spans.get(base).cloned() { + env.origin_spans.insert( + inst_name.clone(), + span.generated(format!("specialization of {base}")), + ); + } if env.function_instances.contains_key(&inst_name) { return Ok(inst_name); } @@ -989,6 +1038,8 @@ fn mangle_instance_name(base: &str, args: &[WaveType]) -> String { fn mangle_type(ty: &WaveType) -> String { match ty { + WaveType::Isz => "isz".to_string(), + WaveType::Usz => "usz".to_string(), WaveType::Int(n) => format!("i{}", n), WaveType::Uint(n) => format!("u{}", n), WaveType::Float(n) => format!("f{}", n), @@ -997,6 +1048,7 @@ fn mangle_type(ty: &WaveType) -> String { WaveType::Byte => "byte".to_string(), WaveType::String => "str".to_string(), WaveType::Void => "void".to_string(), + WaveType::Never => "!".to_string(), WaveType::Pointer(inner) => format!("p_{}", mangle_type(inner)), WaveType::Array(inner, n) => format!("a{}_{}", n, mangle_type(inner)), WaveType::Struct(name) => sanitize_ident(name), @@ -1006,6 +1058,8 @@ fn mangle_type(ty: &WaveType) -> String { fn display_type_for_application(ty: &WaveType) -> String { match ty { + WaveType::Isz => "isz".to_string(), + WaveType::Usz => "usz".to_string(), WaveType::Int(bits) => format!("i{}", bits), WaveType::Uint(bits) => format!("u{}", bits), WaveType::Float(bits) => format!("f{}", bits), @@ -1018,6 +1072,7 @@ fn display_type_for_application(ty: &WaveType) -> String { format!("array<{},{}>", display_type_for_application(inner), size) } WaveType::Void => "void".to_string(), + WaveType::Never => "!".to_string(), WaveType::Struct(name) | WaveType::Variant(name) => name.clone(), } } diff --git a/front/parser/src/hir.rs b/front/parser/src/hir.rs index 953fa118..fb270703 100644 --- a/front/parser/src/hir.rs +++ b/front/parser/src/hir.rs @@ -24,6 +24,15 @@ use crate::verification::{analyze_hir_expression_types, SemanticDiagnostic}; use std::collections::{HashMap, HashSet}; use std::fmt; +/// Stable identity of an AST declaration or statement in one typed program. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct NodeId(usize); +impl NodeId { + pub fn index(self) -> usize { + self.0 + } +} + /// Stable identity of an expression within one [`TypedProgram`]. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct ExpressionId(usize); @@ -86,11 +95,15 @@ pub struct HirVariantPattern { #[derive(Debug)] pub struct TypedProgram { syntax: Box<[ASTNode]>, + node_ids: HashMap, + node_spans: Vec>, expression_ids: HashMap, expression_types: Vec, + expression_spans: Vec>, variant_constructions: Vec>, pattern_ids: HashMap, variant_patterns: Vec>, + pattern_spans: Vec>, } /// Semantic lowering failure that retains the syntax used for source mapping. @@ -130,8 +143,9 @@ impl TypedProgram { /// Validates a final AST and builds its stable typed frontend representation. pub fn lower(syntax: Vec) -> Result { let mut syntax = syntax.into_boxed_slice(); + let source_map = crate::source::SourceMap::detach(&mut syntax); let (analyzed_types, analyzed_variants, analyzed_patterns) = - match analyze_hir_expression_types(&syntax) { + match analyze_hir_expression_types(&syntax, &source_map) { Ok(analysis) => analysis, Err(diagnostic) => return Err(HirLoweringError { syntax, diagnostic }), }; @@ -140,6 +154,7 @@ impl TypedProgram { // concrete without invalidating the expression addresses used while // stable HIR identities are assigned below. canonicalize_syntax_types(&mut syntax); + let mut expression_spans = Vec::new(); let mut expression_ids = HashMap::with_capacity(analyzed_types.len()); let mut expression_types = Vec::with_capacity(analyzed_types.len()); let mut variant_constructions = Vec::with_capacity(analyzed_variants.len()); @@ -148,6 +163,7 @@ impl TypedProgram { let address = expression as *const Expression as usize; let id = ExpressionId(expression_types.len()); expression_ids.insert(address, id); + expression_spans.push(source_map.expressions.get(&address).cloned()); expression_types.push( analyzed_types .get(&address) @@ -157,25 +173,57 @@ impl TypedProgram { variant_constructions.push(analyzed_variants.get(&address).cloned()); }); + let mut pattern_spans = Vec::new(); let mut pattern_ids = HashMap::with_capacity(analyzed_patterns.len()); let mut variant_patterns = Vec::with_capacity(analyzed_patterns.len()); walk_patterns_in_nodes(&syntax, &mut |pattern| { let address = pattern as *const MatchPattern as usize; let id = PatternId(variant_patterns.len()); pattern_ids.insert(address, id); + pattern_spans.push(source_map.patterns.get(&address).cloned()); variant_patterns.push(analyzed_patterns.get(&address).cloned()); }); + let node_ids = source_map + .node_order + .iter() + .enumerate() + .map(|(id, address)| (*address, NodeId(id))) + .collect(); + let node_spans = source_map + .node_order + .iter() + .map(|address| source_map.nodes.get(address).cloned()) + .collect(); Ok(Self { syntax, + node_ids, + node_spans, expression_ids, expression_types, + expression_spans, variant_constructions, pattern_ids, variant_patterns, + pattern_spans, }) } + pub fn node_id(&self, node: &ASTNode) -> Option { + self.node_ids.get(&(node as *const _ as usize)).copied() + } + pub fn node_span(&self, id: NodeId) -> Option<&error::SourceSpan> { + self.node_spans.get(id.index())?.as_ref() + } + + pub fn expression_span(&self, id: ExpressionId) -> Option<&error::SourceSpan> { + self.expression_spans.get(id.index())?.as_ref() + } + + pub fn pattern_span(&self, id: PatternId) -> Option<&error::SourceSpan> { + self.pattern_spans.get(id.index())?.as_ref() + } + pub fn syntax(&self) -> &[ASTNode] { &self.syntax } @@ -241,6 +289,22 @@ impl TypedProgram { } } +/// Resolve target-sized integers before semantic analysis and monomorphization. +/// This pass uses only the selected pointer width, never LLVM or the host width. +pub fn resolve_target_types(nodes: &mut [ASTNode], pointer_bits: u16) -> Result<(), String> { + if !matches!(pointer_bits, 32 | 64) { + return Err(format!("unsupported target pointer width {pointer_bits}")); + } + let named = HashMap::from([ + ("isz".to_string(), WaveType::Int(pointer_bits)), + ("usz".to_string(), WaveType::Uint(pointer_bits)), + ]); + for node in nodes { + canonicalize_node_types(node, &named); + } + Ok(()) +} + fn canonicalize_syntax_types(nodes: &mut [ASTNode]) { let named = collect_named_types(nodes); for node in nodes { @@ -251,7 +315,7 @@ fn canonicalize_syntax_types(nodes: &mut [ASTNode]) { fn collect_named_types(nodes: &[ASTNode]) -> HashMap { let mut named = HashMap::new(); for node in nodes { - match node { + match node.unspanned() { ASTNode::TypeAlias(alias) => { named.insert(alias.name.clone(), alias.target.clone()); } @@ -276,6 +340,8 @@ fn canonical_type( visiting: &mut HashSet, ) -> WaveType { match ty { + WaveType::Isz => named.get("isz").cloned().unwrap_or(WaveType::Isz), + WaveType::Usz => named.get("usz").cloned().unwrap_or(WaveType::Usz), WaveType::Pointer(inner) => { WaveType::Pointer(Box::new(canonical_type(inner, named, visiting))) } @@ -313,16 +379,18 @@ fn canonical_variant_application( visiting: &mut HashSet, ) -> Option { let (base, arguments) = split_named_application(name)?; - if !matches!(named.get(base), Some(WaveType::Variant(_))) { - return None; - } let arguments = arguments .into_iter() .map(|argument| canonical_type(&argument, named, visiting)) .map(|argument| display_wave_type(&argument)) .collect::>() .join(","); - Some(WaveType::Variant(format!("{base}<{arguments}>"))) + let name = format!("{base}<{arguments}>"); + Some(if matches!(named.get(base), Some(WaveType::Variant(_))) { + WaveType::Variant(name) + } else { + WaveType::Struct(name) + }) } fn split_named_application(name: &str) -> Option<(&str, Vec)> { @@ -337,6 +405,8 @@ fn split_named_application(name: &str) -> Option<(&str, Vec)> { fn display_wave_type(ty: &WaveType) -> String { match ty { + WaveType::Isz => "isz".to_string(), + WaveType::Usz => "usz".to_string(), WaveType::Int(bits) => format!("i{bits}"), WaveType::Uint(bits) => format!("u{bits}"), WaveType::Float(bits) => format!("f{bits}"), @@ -349,6 +419,7 @@ fn display_wave_type(ty: &WaveType) -> String { format!("array<{},{}>", display_wave_type(inner), length) } WaveType::Void => "void".to_string(), + WaveType::Never => "!".to_string(), WaveType::Struct(name) | WaveType::Variant(name) => name.clone(), } } @@ -363,6 +434,9 @@ fn canonicalize_function_types( ) { for parameter in &mut function.parameters { canonicalize_type(&mut parameter.param_type, named); + if let Some(default) = &mut parameter.initial_value { + canonicalize_expression_types(default, named); + } } if let Some(return_type) = &mut function.return_type { canonicalize_type(return_type, named); @@ -374,6 +448,7 @@ fn canonicalize_function_types( fn canonicalize_node_types(node: &mut ASTNode, named: &HashMap) { match node { + ASTNode::Located { value, .. } => canonicalize_node_types(value, named), ASTNode::Function(function) => canonicalize_function_types(function, named), ASTNode::ExternFunction(function) => { for (_, parameter_type) in &mut function.params { @@ -498,7 +573,11 @@ fn canonicalize_statement_types(statement: &mut StatementNode, named: &HashMap) { match expression { - Expression::StructLiteral { fields, .. } => { + Expression::Located { value, .. } => canonicalize_expression_types(value, named), + Expression::StructLiteral { name, fields } => { + if let Some(ty) = canonical_variant_application(name, named, &mut HashSet::new()) { + *name = display_wave_type(&ty); + } for (_, value) in fields { canonicalize_expression_types(value, named); } @@ -572,7 +651,15 @@ fn walk_nodes(nodes: &[ASTNode], visit: &mut impl FnMut(&Expression)) { fn walk_node(node: &ASTNode, visit: &mut impl FnMut(&Expression)) { match node { - ASTNode::Function(function) => walk_nodes(&function.body, visit), + ASTNode::Located { value, .. } => walk_node(value, visit), + ASTNode::Function(function) => { + for parameter in &function.parameters { + if let Some(default) = ¶meter.initial_value { + walk_expression(default, visit); + } + } + walk_nodes(&function.body, visit); + } ASTNode::Struct(structure) => { for method in &structure.methods { walk_nodes(&method.body, visit); @@ -670,6 +757,7 @@ fn walk_statement(statement: &StatementNode, visit: &mut impl FnMut(&Expression) fn walk_expression(expression: &Expression, visit: &mut impl FnMut(&Expression)) { visit(expression); match expression { + Expression::Located { value, .. } => walk_expression(value, visit), Expression::StructLiteral { fields, .. } => { for (_, value) in fields { walk_expression(value, visit); @@ -729,6 +817,9 @@ fn walk_expression(expression: &Expression, visit: &mut impl FnMut(&Expression)) fn walk_patterns_in_nodes(nodes: &[ASTNode], visit: &mut impl FnMut(&MatchPattern)) { for node in nodes { match node { + ASTNode::Located { value, .. } => { + walk_patterns_in_nodes(std::slice::from_ref(value), visit) + } ASTNode::Function(function) => walk_patterns_in_nodes(&function.body, visit), ASTNode::Struct(structure) => { for method in &structure.methods { diff --git a/front/parser/src/import.rs b/front/parser/src/import.rs index ab868f01..f0bf70ed 100644 --- a/front/parser/src/import.rs +++ b/front/parser/src/import.rs @@ -20,7 +20,7 @@ use crate::arch; use crate::ast::ASTNode; use crate::os; -use crate::{parse_syntax_only, ParseError}; +use crate::{parse_syntax_with_spans, ParseError}; use error::error::{WaveError, WaveErrorKind}; use lexer::Lexer; use std::collections::{HashMap, HashSet}; @@ -137,8 +137,8 @@ fn is_supported_target_item_start(line: &str) -> bool { let trimmed = line.trim_start(); for kw in [ - "import", "extern", "export", "pub", "fun", "struct", "enum", "const", "static", "type", - "proto", + "import", "extern", "export", "pub", "fun", "struct", "enum", "variant", "const", "static", + "type", "proto", ] { if let Some(rest) = trimmed.strip_prefix(kw) { if has_ident_boundary(rest) { @@ -248,7 +248,7 @@ fn consume_target_item(lines: &[&str], mut idx: usize, keep: bool, out: &mut Vec if keep { out.push(line.to_string()); } else { - out.push(String::new()); + out.push(" ".repeat(line.len())); } let mut saw_semicolon = false; @@ -275,7 +275,7 @@ fn consume_target_item(lines: &[&str], mut idx: usize, keep: bool, out: &mut Vec } pub fn preprocess_target_attrs(source: &str, target: &TargetConditionContext) -> String { - let lines: Vec<&str> = source.lines().collect(); + let lines: Vec<&str> = source.split('\n').collect(); let mut out: Vec = Vec::with_capacity(lines.len()); let mut idx: usize = 0; @@ -284,7 +284,7 @@ pub fn preprocess_target_attrs(source: &str, target: &TargetConditionContext) -> if let Some(target_attr) = parse_target_attr(line) { // Attribute line is removed for parser compatibility, // but we keep its line slot to preserve diagnostics. - out.push(String::new()); + out.push(" ".repeat(line.len())); idx += 1; let keep_item = target_attr.matches(target); @@ -304,7 +304,7 @@ pub fn preprocess_target_attrs(source: &str, target: &TargetConditionContext) -> if keep_item { out.push(item_line.to_string()); } else { - out.push(String::new()); + out.push(" ".repeat(item_line.len())); } idx += 1; continue; @@ -316,7 +316,7 @@ pub fn preprocess_target_attrs(source: &str, target: &TargetConditionContext) -> out.push(item_line.to_string()); idx += 1; } else { - out.push(String::new()); + out.push(" ".repeat(item_line.len())); idx += 1; } break; @@ -328,11 +328,7 @@ pub fn preprocess_target_attrs(source: &str, target: &TargetConditionContext) -> idx += 1; } - let mut processed = out.join("\n"); - if source.ends_with('\n') { - processed.push('\n'); - } - processed + out.join("\n") } pub struct ImportedUnit { @@ -844,7 +840,7 @@ fn parse_wave_file( let mut lexer = Lexer::new_with_file(&content, abs_path.display().to_string()); let tokens = lexer.tokenize()?; - let ast = parse_syntax_only(&tokens).map_err(|e| { + let ast = parse_syntax_with_spans(&tokens).map_err(|e| { let (kind, phase, code) = match &e { ParseError::Syntax(_) => ( WaveErrorKind::SyntaxError(e.message().to_string()), @@ -873,6 +869,7 @@ fn parse_wave_file( .with_code(code) .with_source_code(content.clone()); + we = we.with_span(e.span()); if let Some(ctx) = e.context() { we = we.with_context(ctx.to_string()); } diff --git a/front/parser/src/lib.rs b/front/parser/src/lib.rs index c79fd0f1..26eed97a 100644 --- a/front/parser/src/lib.rs +++ b/front/parser/src/lib.rs @@ -53,3 +53,5 @@ pub mod stdlib; pub mod verification; pub use parser::*; + +pub mod source; diff --git a/front/parser/src/parser/asm.rs b/front/parser/src/parser/asm.rs index 7c92807e..773879b4 100644 --- a/front/parser/src/parser/asm.rs +++ b/front/parser/src/parser/asm.rs @@ -16,7 +16,7 @@ //! register names and stack contracts are intentionally deferred to the //! architecture-aware backend planner. -use crate::ast::{ASTNode, Expression, Literal, StatementNode}; +use crate::ast::{ASTNode, Expression, StatementNode}; use crate::expr::is_assignable; use lexer::token::TokenType; use lexer::Token; @@ -84,7 +84,7 @@ pub fn parse_asm_block(tokens: &mut Peekable>) -> Option { println!("Unexpected token in asm block: {:?}", other); - tokens.next(); + return None; } } } @@ -108,7 +108,7 @@ pub fn parse_asm_clobber_clause<'a, T>( clobbers: &mut Vec, ) -> Option<()> where - T: Iterator, + T: Iterator + Clone, { // expect '(' if tokens.peek().map(|t| &t.token_type) != Some(&TokenType::Lparen) { @@ -174,7 +174,7 @@ pub fn parse_asm_inout_clause<'a, T>( outputs: &mut Vec<(String, Expression)>, ) -> Option<()> where - T: Iterator, + T: Iterator + Clone, { if tokens.peek().map(|t| &t.token_type) != Some(&TokenType::Lparen) { println!("Expected '(' after in/out"); @@ -227,62 +227,7 @@ where pub(crate) fn parse_asm_operand<'a, T>(tokens: &mut Peekable) -> Option where - T: Iterator, + T: Iterator + Clone, { - let tok = tokens.next()?; - match &tok.token_type { - TokenType::Identifier(s) => Some(Expression::Variable(s.clone())), - TokenType::IntLiteral(n) => Some(Expression::Literal(Literal::Int(n.clone()))), - TokenType::String(s) => Some(Expression::Literal(Literal::String(s.clone()))), - - TokenType::AddressOf => { - // &x - let next = tokens.next()?; - match &next.token_type { - TokenType::Identifier(s) => Some(Expression::AddressOf(Box::new( - Expression::Variable(s.clone()), - ))), - _ => { - println!("Expected identifier after '&' in in/out(...)"); - None - } - } - } - - TokenType::Deref => { - let next = tokens.next()?; - match &next.token_type { - TokenType::Identifier(s) => { - Some(Expression::Deref(Box::new(Expression::Variable(s.clone())))) - } - _ => { - println!("Expected identifier after 'deref' in in/out(...)"); - None - } - } - } - - TokenType::Minus => match tokens.next()? { - Token { - token_type: TokenType::IntLiteral(n), - .. - } => Some(Expression::Literal(Literal::Int(format!("-{}", n)))), - Token { - token_type: TokenType::Float(f), - .. - } => Some(Expression::Literal(Literal::Float(-*f))), - other => { - println!( - "Expected int/float after '-' in asm operand, got {:?}", - other.token_type - ); - None - } - }, - - other => { - println!("Expected asm operand, got {:?}", other); - None - } - } + crate::expr::parse_expression(tokens) } diff --git a/front/parser/src/parser/control.rs b/front/parser/src/parser/control.rs index 0802c029..cda6992b 100644 --- a/front/parser/src/parser/control.rs +++ b/front/parser/src/parser/control.rs @@ -66,102 +66,106 @@ fn parse_match_pattern( tokens: &mut Peekable>, payload_position: bool, ) -> Option { - skip_ws_and_newlines(tokens); - - match tokens.next()? { - Token { - token_type: TokenType::IntLiteral(v), - .. - } => Some(MatchPattern::Int(v.clone())), - Token { - token_type: TokenType::Identifier(name), - .. - } => { - if name == "_" { - return Some(MatchPattern::Wildcard); - } + let before = tokens.clone(); + let result = (|| { + skip_ws_and_newlines(tokens); - let mut segments = vec![name.clone()]; - loop { - skip_ws_and_newlines(tokens); - if !matches!( - tokens.peek().map(|token| &token.token_type), - Some(TokenType::DoubleColon) - ) { - break; + match tokens.next()? { + Token { + token_type: TokenType::IntLiteral(v), + .. + } => Some(MatchPattern::Int(v.clone())), + Token { + token_type: TokenType::Identifier(name), + .. + } => { + if name == "_" { + return Some(MatchPattern::Wildcard); } - tokens.next(); - skip_ws_and_newlines(tokens); - match tokens.next() { - Some(Token { - token_type: TokenType::Identifier(segment), - .. - }) => segments.push(segment.clone()), - _ => { - println!("Error: Expected case name after '::' in match pattern"); - return None; - } - } - } - - if segments.len() == 1 { - return if payload_position { - Some(MatchPattern::Binding(name.clone())) - } else { - Some(MatchPattern::Ident(name.clone())) - }; - } - let case_name = segments.pop().unwrap(); - let variant_type = segments.join("::"); - let mut payloads = Vec::new(); - skip_ws_and_newlines(tokens); - if matches!( - tokens.peek().map(|token| &token.token_type), - Some(TokenType::Lparen) - ) { - tokens.next(); + let mut segments = vec![name.clone()]; loop { skip_ws_and_newlines(tokens); - if matches!( + if !matches!( tokens.peek().map(|token| &token.token_type), - Some(TokenType::Rparen) + Some(TokenType::DoubleColon) ) { - tokens.next(); break; } - payloads.push(parse_match_pattern(tokens, true)?); + tokens.next(); skip_ws_and_newlines(tokens); - match tokens.peek().map(|token| &token.token_type) { - Some(TokenType::Comma) => { - tokens.next(); + match tokens.next() { + Some(Token { + token_type: TokenType::Identifier(segment), + .. + }) => segments.push(segment.clone()), + _ => { + println!("Error: Expected case name after '::' in match pattern"); + return None; } - Some(TokenType::Rparen) => { + } + } + + if segments.len() == 1 { + return if payload_position { + Some(MatchPattern::Binding(name.clone())) + } else { + Some(MatchPattern::Ident(name.clone())) + }; + } + + let case_name = segments.pop().unwrap(); + let variant_type = segments.join("::"); + let mut payloads = Vec::new(); + skip_ws_and_newlines(tokens); + if matches!( + tokens.peek().map(|token| &token.token_type), + Some(TokenType::Lparen) + ) { + tokens.next(); + loop { + skip_ws_and_newlines(tokens); + if matches!( + tokens.peek().map(|token| &token.token_type), + Some(TokenType::Rparen) + ) { tokens.next(); break; } - _ => { - println!("Error: Expected ',' or ')' in variant pattern"); - return None; + payloads.push(parse_match_pattern(tokens, true)?); + skip_ws_and_newlines(tokens); + match tokens.peek().map(|token| &token.token_type) { + Some(TokenType::Comma) => { + tokens.next(); + } + Some(TokenType::Rparen) => { + tokens.next(); + break; + } + _ => { + println!("Error: Expected ',' or ')' in variant pattern"); + return None; + } } } } - } - Some(MatchPattern::Variant { - variant_type, - case_name, - payloads, - }) - } - other => { - println!( + Some(MatchPattern::Variant { + variant_type, + case_name, + payloads, + }) + } + other => { + println!( "Error: Invalid match pattern {:?} (expected integer literal, enum variant, or `_`)", other.token_type ); - None + None + } } - } + })(); + result.map(|value: MatchPattern| value.with_span(lexer::consumed_span(before, tokens))) } pub fn parse_if(tokens: &mut Peekable>) -> Option { @@ -304,27 +308,34 @@ fn parse_typed_for_initializer( } fn parse_for_initializer(tokens: &mut Peekable>) -> Option { - match tokens.peek().map(|t| &t.token_type) { - Some(TokenType::Var) => { - tokens.next(); // consume `var` - parse_typed_for_initializer(tokens, Mutability::Var) - } - Some(TokenType::Const) => { - println!("Error: `const` is not allowed in local for-loop initializer"); - None - } - Some(TokenType::Static) => { - println!("Error: `static` is not allowed in local for-loop initializer"); - None - } - _ if is_typed_for_initializer(tokens) => { - parse_typed_for_initializer(tokens, Mutability::Var) - } - _ => { - let expr = parse_expression(tokens)?; - Some(ASTNode::Statement(StatementNode::Expression(expr))) + let before = tokens.clone(); + let result = (|| { + match tokens.peek().map(|t| &t.token_type) { + Some(TokenType::Var) => { + tokens.next(); // consume `var` + parse_typed_for_initializer(tokens, Mutability::Var) + } + Some(TokenType::Const) => { + println!("Error: `const` is not allowed in local for-loop initializer"); + None + } + Some(TokenType::Static) => { + println!("Error: `static` is not allowed in local for-loop initializer"); + None + } + _ if is_typed_for_initializer(tokens) => { + parse_typed_for_initializer(tokens, Mutability::Var) + } + _ => { + let expr = parse_expression(tokens)?; + Some(ASTNode::Statement(StatementNode::Expression(expr))) + } } - } + })(); + result.map(|value: ASTNode| { + let span = crate::source::node_span(before, tokens, &value); + value.with_span(span) + }) } // FOR parsing @@ -464,8 +475,9 @@ pub fn parse_match(tokens: &mut Peekable>) -> Option { break; } + let before = tokens.clone(); let pattern = parse_match_pattern(tokens, false)?; - if matches!(pattern, MatchPattern::Wildcard) { + if matches!(pattern.unspanned(), MatchPattern::Wildcard) { if saw_wildcard { println!("Error: Duplicate wildcard arm `_` in match"); return None; @@ -485,7 +497,11 @@ pub fn parse_match(tokens: &mut Peekable>) -> Option { tokens.next(); // consume '{' let body = parse_block(tokens)?; - arms.push(MatchArm { pattern, body }); + arms.push(MatchArm { + pattern, + body, + span: lexer::consumed_span(before, tokens), + }); skip_ws_and_newlines(tokens); if matches!( diff --git a/front/parser/src/parser/decl.rs b/front/parser/src/parser/decl.rs index 9c42b0cb..18d2c25f 100644 --- a/front/parser/src/parser/decl.rs +++ b/front/parser/src/parser/decl.rs @@ -17,8 +17,8 @@ //! depth so inner commas and closing chevrons cannot terminate the outer type. use crate::ast::{ - ASTNode, EnumNode, EnumVariantNode, Expression, ExternFunctionNode, Mutability, TypeAliasNode, - VariableNode, VariantCaseNode, VariantNode, Visibility, WaveType, + ASTNode, EnumNode, EnumVariantNode, Expression, ExternFunctionNode, Literal, Mutability, + TypeAliasNode, VariableNode, VariantCaseNode, VariantNode, Visibility, WaveType, }; use crate::expr::parse_expression; use crate::parser::functions::parse_generic_param_names; @@ -130,9 +130,10 @@ pub fn parse_const_decl(tokens: &mut Peekable>) -> Option>) -> Option { } tokens.next(); - if let (WaveType::Array(_, expected_len), Some(Expression::ArrayLiteral(elements))) = - (&wave_type, &initial_value) - { + if let (WaveType::Array(_, expected_len), Some(Expression::ArrayLiteral(elements))) = ( + &wave_type, + &initial_value.as_ref().map(Expression::unspanned), + ) { if *expected_len != elements.len() as u32 { println!( "❌ Error: Array length mismatch. Expected {}, but got {} elements", @@ -685,16 +687,6 @@ pub fn parse_type_alias(tokens: &mut Peekable>) -> Option Option { - if !tok.lexeme.is_empty() { - return Some(tok.lexeme.clone()); - } - if let TokenType::Identifier(s) = &tok.token_type { - return Some(s.clone()); - } - None -} - pub fn parse_enum(tokens: &mut Peekable>) -> Option { // enum -> { (=)? (, ...)* } let name = match tokens.next() { @@ -755,6 +747,7 @@ pub fn parse_enum(tokens: &mut Peekable>) -> Option { break; } TokenType::Identifier(_) => { + let before = tokens.clone(); // variant name let vname = match tokens.next() { Some(Token { @@ -769,29 +762,17 @@ pub fn parse_enum(tokens: &mut Peekable>) -> Option { if matches!(tokens.peek().map(|t| &t.token_type), Some(TokenType::Equal)) { tokens.next(); // consume '=' - let val_tok = match tokens.next() { - Some(t) => t, - None => { - println!( - "Error: Expected integer literal after '=' in enum '{}'", - name - ); - return None; - } - }; - - let raw = match token_text(val_tok) { - Some(s) => s, - None => { - println!("Error: Expected integer literal after '=' in enum '{}', found {:?}", name, val_tok); - return None; - } + let value = parse_expression(tokens)?; + let Expression::Literal(Literal::Int(raw)) = value.unspanned() else { + return None; }; + let raw = raw.clone(); explicit_value = Some(raw); } variants.push(EnumVariantNode { + span: lexer::consumed_span(before, tokens), name: vname, explicit_value, }); @@ -867,6 +848,7 @@ pub fn parse_variant(tokens: &mut Peekable>) -> Option break; } + let before = tokens.clone(); let case_name = match tokens.next() { Some(Token { token_type: TokenType::Identifier(case_name), @@ -916,6 +898,7 @@ pub fn parse_variant(tokens: &mut Peekable>) -> Option } cases.push(VariantCaseNode { + span: lexer::consumed_span(before, tokens), name: case_name, payload_types, }); diff --git a/front/parser/src/parser/functions.rs b/front/parser/src/parser/functions.rs index 8c65bdf4..0e501107 100644 --- a/front/parser/src/parser/functions.rs +++ b/front/parser/src/parser/functions.rs @@ -16,15 +16,8 @@ //! within generic parameter lists are rejected here; program-wide symbol and //! body type checks remain the semantic verifier's responsibility. -use crate::ast::{ - ASTNode, ExportAttribute, FunctionNode, ParameterNode, StatementNode, Value, Visibility, -}; -use crate::expr::parse_expression; -use crate::parser::asm::*; -use crate::parser::control::*; -use crate::parser::decl::*; -use crate::parser::io::*; -use crate::parser::stmt::parse_assignment; +use crate::ast::{ASTNode, ExportAttribute, Expression, FunctionNode, ParameterNode, Visibility}; +use crate::parser::decl::parse_ffi_header; use crate::parser::types::parse_type_from_stream; use lexer::token::TokenType; use lexer::Token; @@ -61,6 +54,9 @@ pub fn parse_generic_param_names(tokens: &mut Peekable>) -> Option' break; } @@ -101,7 +97,7 @@ pub fn parse_generic_param_names(tokens: &mut Peekable>) -> Option>) -> Vec { +pub fn parse_parameters(tokens: &mut Peekable>) -> Option> { let mut params = vec![]; loop { skip_ws(tokens); @@ -113,6 +109,7 @@ pub fn parse_parameters(tokens: &mut Peekable>) -> Vec>) -> Vec>) -> Vec>) -> Vec pt, None => { println!("Error: Failed to parse type for parameter '{}'", name); - break; + return None; } }; let initial_value = if tokens .peek() - .map_or(false, |t| t.token_type == TokenType::Equal) + .is_some_and(|t| t.token_type == TokenType::Equal) { - tokens.next(); // consume '=' - match tokens.next() { - Some(Token { - token_type: TokenType::IntLiteral(n), - .. - }) => Some(Value::Int((*n).parse().unwrap())), - Some(Token { - token_type: TokenType::Float(f), - .. - }) => Some(Value::Float(*f)), - Some(Token { - token_type: TokenType::String(s), - .. - }) => Some(Value::Text(s.clone())), - _ => { - println!("Error: Unsupported initializer for parameter '{}'", name); - None - } + tokens.next(); + let value = crate::expr::parse_expression(tokens)?; + if !matches!(value.unspanned(), Expression::Literal(_) | Expression::Null) { + return None; } + Some(value) } else { None }; params.push(ParameterNode { + span: lexer::consumed_span(before, tokens), name, param_type, initial_value, @@ -182,14 +167,14 @@ pub fn parse_parameters(tokens: &mut Peekable>) -> Vec { println!("Error: use `,` instead of `;` to separate parameters"); - break; + return None; } Some(TokenType::Rparen) => { // loop end } _ => { println!("Error: Expected ',' or ')' after parameter"); - break; + return None; } } } @@ -199,11 +184,12 @@ pub fn parse_parameters(tokens: &mut Peekable>) -> Vec>) -> Option { @@ -214,6 +200,7 @@ pub fn parse_function_with_export( tokens: &mut Peekable>, export: Option, ) -> Option { + let before = tokens.clone(); tokens.next(); skip_ws(tokens); @@ -234,7 +221,7 @@ pub fn parse_function_with_export( } tokens.next(); // consume '(' - let parameters = parse_parameters(tokens); + let parameters = parse_parameters(tokens)?; let mut param_names = HashSet::new(); for param in ¶meters { @@ -248,13 +235,17 @@ pub fn parse_function_with_export( } skip_ws(tokens); + let mut return_type_span = None; let return_type = if let Some(Token { token_type: TokenType::Arrow, .. }) = tokens.peek() { tokens.next(); // consume '->' - parse_type_from_stream(tokens) + let before_type = tokens.clone(); + let ty = parse_type_from_stream(tokens)?; + return_type_span = lexer::consumed_span(before_type, tokens); + Some(ty) } else { None }; @@ -262,11 +253,13 @@ pub fn parse_function_with_export( skip_ws(tokens); let body = extract_body(tokens)?; Some(ASTNode::Function(FunctionNode { + span: lexer::consumed_span(before, tokens), name, generic_params, parameters, body, return_type, + return_type_span, export, visibility: Visibility::Private, })) @@ -340,194 +333,9 @@ pub fn parse_export(tokens: &mut Peekable>) -> Option> } pub fn extract_body(tokens: &mut Peekable>) -> Option> { - let mut body = vec![]; - if tokens.peek()?.token_type != TokenType::Lbrace { - println!("❌ Expected '{{' at the beginning of function body"); return None; } - tokens.next(); // consume '{' - - while let Some(token) = tokens.peek() { - match &token.token_type { - TokenType::Whitespace => { - tokens.next(); // ignore - } - TokenType::Rbrace => { - tokens.next(); - break; - } - TokenType::Eof => { - println!("❌ Unexpected EOF inside function body"); - return None; - } - TokenType::Asm => { - tokens.next(); - body.push(parse_asm_block(tokens)?); - } - TokenType::Var => { - tokens.next(); // consume 'var' - body.push(parse_var(tokens)?); - } - TokenType::Let | TokenType::Mut => { - println!("Error: `let` and `let mut` declarations were removed; use `var`"); - return None; - } - TokenType::Const => { - println!("Error: `const` is only allowed at top level"); - return None; - } - TokenType::Static => { - println!("Error: `static` is only allowed at top level"); - return None; - } - TokenType::Println => { - tokens.next(); // consume 'println' - let node = parse_println(tokens)?; - // Added semicolon handling - if let Some(Token { - token_type: TokenType::SemiColon, - .. - }) = tokens.peek() - { - tokens.next(); - } - body.push(node); - } - TokenType::Print => { - tokens.next(); // consume 'print' - let node = parse_print(tokens)?; - // Added semicolon handling - if let Some(Token { - token_type: TokenType::SemiColon, - .. - }) = tokens.peek() - { - tokens.next(); - } - body.push(node); - } - TokenType::Input => { - tokens.next(); // consume 'input' - let node = parse_input(tokens)?; - // Added semicolon handling - if let Some(Token { - token_type: TokenType::SemiColon, - .. - }) = tokens.peek() - { - tokens.next(); - } - body.push(node); - } - TokenType::If => { - tokens.next(); - body.push(parse_if(tokens)?); - } - TokenType::For => { - tokens.next(); - body.push(parse_for(tokens)?); - } - TokenType::While => { - tokens.next(); - body.push(parse_while(tokens)?); - } - TokenType::Match => { - tokens.next(); - body.push(parse_match(tokens)?); - } - TokenType::Identifier(_) => { - if let Some(expr) = parse_expression(tokens) { - if let Some(Token { - token_type: TokenType::SemiColon, - .. - }) = tokens.peek() - { - tokens.next(); // consume ';' - } - body.push(ASTNode::Statement(StatementNode::Expression(expr))); - } else { - println!("❌ Failed to parse expression starting with identifier"); - return None; - } - } - TokenType::Break => { - tokens.next(); // consume 'break' - if let Some(Token { - token_type: TokenType::SemiColon, - .. - }) = tokens.peek() - { - tokens.next(); // consume ; - } - body.push(ASTNode::Statement(StatementNode::Break)); - } - TokenType::Continue => { - tokens.next(); // consume 'continue' - if let Some(Token { - token_type: TokenType::SemiColon, - .. - }) = tokens.peek() - { - tokens.next(); // consume ; - } - body.push(ASTNode::Statement(StatementNode::Continue)); - } - TokenType::Return => { - tokens.next(); // consume 'return' - - let expr = if let Some(Token { - token_type: TokenType::SemiColon, - .. - }) = tokens.peek() - { - tokens.next(); // return; - None - } else { - let value = match parse_expression(tokens) { - Some(v) => v, - None => { - println!("Error: Expected valid expression after 'return'"); - return None; - } - }; - - if let Some(Token { - token_type: TokenType::SemiColon, - .. - }) = tokens.peek() - { - tokens.next(); - } else { - println!("Error: Missing semicolon after return expression"); - return None; - } - Some(value) - }; - - body.push(ASTNode::Statement(StatementNode::Return(expr))); - } - TokenType::Deref => { - let token = (*token).clone(); - tokens.next(); - body.push(parse_assignment(tokens, &token)?); - } - _ => { - if let Some(expr) = parse_expression(tokens) { - if let Some(Token { - token_type: TokenType::SemiColon, - .. - }) = tokens.peek() - { - tokens.next(); // consume ; - } - body.push(ASTNode::Statement(StatementNode::Expression(expr))); - } else { - tokens.next(); // fallback skip - } - } - } - } - - Some(body) + tokens.next(); + crate::parser::stmt::parse_block(tokens) } diff --git a/front/parser/src/parser/items.rs b/front/parser/src/parser/items.rs index f01635a6..be84e3e8 100644 --- a/front/parser/src/parser/items.rs +++ b/front/parser/src/parser/items.rs @@ -246,6 +246,7 @@ pub fn parse_struct(tokens: &mut Peekable>) -> Option { tokens.next(); let mut fields = Vec::new(); + let mut field_spans = Vec::new(); let mut methods = Vec::new(); loop { @@ -301,6 +302,7 @@ pub fn parse_struct(tokens: &mut Peekable>) -> Option { lookahead.peek().map(|t| &t.token_type), Some(TokenType::Colon) ) { + let before = tokens.clone(); let field_name = if let Some(Token { token_type: TokenType::Identifier(n), .. @@ -353,6 +355,7 @@ pub fn parse_struct(tokens: &mut Peekable>) -> Option { } tokens.next(); // consume ';' + field_spans.push(lexer::consumed_span(before, tokens)); fields.push((field_name, wave_type)); } else { let id_str = @@ -383,6 +386,7 @@ pub fn parse_struct(tokens: &mut Peekable>) -> Option { name, generic_params, fields, + field_spans, methods, visibility: Visibility::Private, })) diff --git a/front/parser/src/parser/mod.rs b/front/parser/src/parser/mod.rs index 23831ea2..0dd5b690 100644 --- a/front/parser/src/parser/mod.rs +++ b/front/parser/src/parser/mod.rs @@ -26,4 +26,4 @@ mod parse; pub mod stmt; pub mod types; -pub use parse::{parse, parse_syntax_only, ParseError}; +pub use parse::{parse, parse_syntax_only, parse_syntax_with_spans, ParseError}; diff --git a/front/parser/src/parser/parse.rs b/front/parser/src/parser/parse.rs index 584abbf5..7493087e 100644 --- a/front/parser/src/parser/parse.rs +++ b/front/parser/src/parser/parse.rs @@ -29,6 +29,7 @@ pub struct ParseDiagnostic { pub message: String, pub line: usize, pub column: usize, + pub span: Option, pub expected: Vec, pub found: Option, pub context: Option, @@ -48,6 +49,7 @@ impl ParseError { message: message.into(), line: 0, column: 0, + span: None, expected: Vec::new(), found: None, context: None, @@ -59,7 +61,8 @@ impl ParseError { pub fn syntax_at(token: Option<&Token>, message: impl Into) -> Self { let mut err = Self::syntax(message); if let Some(tok) = token { - err = err.with_line_col(tok.line, 1); + err = err.with_line_col(tok.line, tok.span.as_ref().map_or(0, |s| s.column)); + err.diag_mut().span = tok.span.clone(); } err } @@ -69,6 +72,7 @@ impl ParseError { message: message.into(), line: 0, column: 0, + span: None, expected: Vec::new(), found: None, context: None, @@ -120,13 +124,13 @@ impl ParseError { pub fn with_found_token(mut self, token: Option<&Token>) -> Self { if let Some(tok) = token { let d = self.diag_mut(); - if d.line == 0 { - d.line = tok.line; - } - if d.column == 0 { - d.column = 1; - } + d.line = tok.line; + d.column = tok.span.as_ref().map_or(0, |s| s.column); + d.span = tok.span.clone(); d.found = Some(Self::token_desc(tok)); + if let Some(spelling) = tok.token_type.reserved_spelling() { + d.message = format!("reserved syntax `{spelling}` is not implemented in Alpha"); + } } self } @@ -152,6 +156,12 @@ impl ParseError { } } + pub fn span(&self) -> Option<&error::SourceSpan> { + match self { + Self::Syntax(d) | Self::Semantic(d) => d.span.as_ref(), + } + } + pub fn line(&self) -> usize { match self { ParseError::Syntax(d) | ParseError::Semantic(d) => d.line, @@ -195,13 +205,25 @@ impl ParseError { } } +/// Compatibility entry point for consumers that do not retain source provenance. pub fn parse_syntax_only(tokens: &[Token]) -> Result, ParseError> { + let mut tokens = tokens.to_vec(); + for token in &mut tokens { + token.span = None; + } + parse_syntax_with_spans(&tokens) +} + +/// Parse physical syntax with byte ranges preserved through frontend rewrites. +pub fn parse_syntax_with_spans(tokens: &[Token]) -> Result, ParseError> { validate_explicit_variable_types(tokens)?; let mut iter = tokens.iter().peekable(); let mut nodes = vec![]; - while let Some(token) = iter.peek() { + while let Some(token) = iter.peek().copied() { + let before = iter.clone(); + let first_node = nodes.len(); match token.token_type { TokenType::Whitespace | TokenType::Newline => { iter.next(); @@ -515,6 +537,11 @@ pub fn parse_syntax_only(tokens: &[Token]) -> Result, ParseError> { ); } } + for node in &mut nodes[first_node..] { + let value = std::mem::replace(node, ASTNode::Expression(crate::ast::Expression::Null)); + let span = crate::source::node_span(before.clone(), &mut iter, &value); + *node = value.with_span(span); + } } Ok(nodes) diff --git a/front/parser/src/parser/stmt.rs b/front/parser/src/parser/stmt.rs index c5c8cd10..b89fcf77 100644 --- a/front/parser/src/parser/stmt.rs +++ b/front/parser/src/parser/stmt.rs @@ -16,8 +16,8 @@ //! ownership local prevents a failed statement from shifting the token stream //! seen by the following declaration. -use crate::ast::{ASTNode, AssignOperator, Expression, StatementNode}; -use crate::expr::{is_assignable, parse_expression, parse_expression_from_token}; +use crate::ast::{ASTNode, StatementNode}; +use crate::expr::parse_expression; use crate::parser::control::{parse_for, parse_if, parse_match, parse_while}; use crate::parser::decl::parse_var; use crate::parser::io::*; @@ -27,95 +27,12 @@ use lexer::Token; use std::iter::Peekable; use std::slice::Iter; -pub fn parse_assignment( - tokens: &mut Peekable>, - first_token: &Token, -) -> Option { - let left_expr = match parse_expression_from_token(first_token, tokens) { - Some(expr) => expr, - None => { - println!( - "Error: Failed to parse left-hand side of assignment. Token: {:?}", - first_token.token_type - ); - return None; - } - }; - - let assign_op = match tokens.peek()?.token_type { - TokenType::PlusEq => { - tokens.next(); - Some(AssignOperator::AddAssign) - } - TokenType::MinusEq => { - tokens.next(); - Some(AssignOperator::SubAssign) - } - TokenType::StarEq => { - tokens.next(); - Some(AssignOperator::MulAssign) - } - TokenType::DivEq => { - tokens.next(); - Some(AssignOperator::DivAssign) - } - TokenType::RemainderEq => { - tokens.next(); - Some(AssignOperator::RemAssign) - } - TokenType::Equal => { - tokens.next(); - None - } - _ => return None, - }; - - let right_expr = parse_expression(tokens)?; - - if let Some(Token { - token_type: TokenType::SemiColon, - .. - }) = tokens.peek() - { - tokens.next(); - } - - match assign_op { - Some(op) => { - if !is_assignable(&left_expr) { - println!( - "Error: Unsupported assignment target for '{:?}': {:?}", - op, left_expr - ); - return None; - } - - Some(ASTNode::Statement(StatementNode::Expression( - Expression::AssignOperation { - target: Box::new(left_expr), - operator: op, - value: Box::new(right_expr), - }, - ))) - } - - None => { - if !is_assignable(&left_expr) { - println!( - "Error: Unsupported assignment left expression: {:?}", - left_expr - ); - return None; - } - - Some(ASTNode::Statement(StatementNode::Expression( - Expression::Assignment { - target: Box::new(left_expr), - value: Box::new(right_expr), - }, - ))) - } +fn semicolon(tokens: &mut Peekable>) -> Option<()> { + if tokens.peek()?.token_type != TokenType::SemiColon { + return None; } + tokens.next(); + Some(()) } pub fn parse_block(tokens: &mut Peekable>) -> Option> { @@ -151,144 +68,115 @@ pub fn parse_block(tokens: &mut Peekable>) -> Option> { } pub fn parse_statement(tokens: &mut Peekable>) -> Option { - let token = match tokens.peek() { - Some(t) => (*t).clone(), - None => return None, - }; - - if matches!( - token.token_type, - TokenType::Identifier(_) | TokenType::Deref - ) { - let first = token.clone(); - - let mut look = tokens.clone(); - look.next(); - - if let Some(node) = parse_assignment(&mut look, &first) { - *tokens = look; - return Some(node); - } - } - - let node = match token.token_type { - TokenType::Var => { - tokens.next(); - parse_var(tokens) - } - TokenType::Let | TokenType::Mut => { - println!("Error: `let` and `let mut` declarations were removed; use `var`"); - None - } - TokenType::Const => { - println!("Error: `const` is only allowed at top level"); - None - } - TokenType::Static => { - println!("Error: `static` is only allowed at top level"); - None - } - TokenType::Println => { - tokens.next(); - parse_println(tokens) - } - TokenType::Print => { - tokens.next(); - parse_print(tokens) - } - TokenType::Input => { - tokens.next(); - parse_input(tokens) - } - TokenType::If => { - tokens.next(); - parse_if(tokens) - } - TokenType::For => { - tokens.next(); - parse_for(tokens) - } - TokenType::While => { - tokens.next(); - parse_while(tokens) - } - TokenType::Match => { - tokens.next(); - parse_match(tokens) - } - TokenType::Continue => { - tokens.next(); - if let Some(Token { - token_type: TokenType::SemiColon, - .. - }) = tokens.peek() - { + let before = tokens.clone(); + let result = (|| { + let token = match tokens.peek() { + Some(t) => (*t).clone(), + None => return None, + }; + + let node = match token.token_type { + TokenType::Var => { tokens.next(); + parse_var(tokens) } - Some(ASTNode::Statement(StatementNode::Continue)) - } - TokenType::Break => { - tokens.next(); - if let Some(Token { - token_type: TokenType::SemiColon, - .. - }) = tokens.peek() - { - tokens.next(); + TokenType::Let | TokenType::Mut => { + println!("Error: `let` and `let mut` declarations were removed; use `var`"); + None } - Some(ASTNode::Statement(StatementNode::Break)) - } - TokenType::Return => { - tokens.next(); - let expr = if let Some(Token { - token_type: TokenType::SemiColon, - .. - }) = tokens.peek() - { - tokens.next(); + TokenType::Const => { + println!("Error: `const` is only allowed at top level"); None - } else if tokens.peek().is_none() { + } + TokenType::Static => { + println!("Error: `static` is only allowed at top level"); None - } else { - let value = parse_expression(tokens)?; - if let Some(Token { - token_type: TokenType::SemiColon, - .. - }) = tokens.peek() + } + TokenType::Println => { + tokens.next(); + parse_println(tokens) + } + TokenType::Print => { + tokens.next(); + parse_print(tokens) + } + TokenType::Input => { + tokens.next(); + parse_input(tokens) + } + TokenType::If => { + tokens.next(); + parse_if(tokens) + } + TokenType::For => { + tokens.next(); + parse_for(tokens) + } + TokenType::While => { + tokens.next(); + parse_while(tokens) + } + TokenType::Match => { + tokens.next(); + parse_match(tokens) + } + TokenType::Continue | TokenType::Break => { + tokens.next(); + semicolon(tokens)?; + Some(ASTNode::Statement( + if token.token_type == TokenType::Continue { + StatementNode::Continue + } else { + StatementNode::Break + }, + )) + } + TokenType::Return => { + tokens.next(); + let expr = if tokens.peek()?.token_type == TokenType::SemiColon { + None + } else { + Some(parse_expression(tokens)?) + }; + semicolon(tokens)?; + Some(ASTNode::Statement(StatementNode::Return(expr))) + } + TokenType::Asm => { + tokens.next(); + let node = crate::parser::asm::parse_asm_block(tokens)?; + if tokens + .peek() + .is_some_and(|t| t.token_type == TokenType::SemiColon) { tokens.next(); } - Some(value) - }; - Some(ASTNode::Statement(StatementNode::Return(expr))) - } - TokenType::Rbrace => None, - - _ => { - if is_expression_start(&token.token_type) { - if let Some(expr) = parse_expression(tokens) { - if let Some(Token { - token_type: TokenType::SemiColon, - .. - }) = tokens.peek() - { - tokens.next(); + Some(node) + } + TokenType::Rbrace => None, + + _ => { + if is_expression_start(&token.token_type) { + if let Some(expr) = parse_expression(tokens) { + semicolon(tokens)?; + Some(ASTNode::Statement(StatementNode::Expression(expr))) + } else { + println!("Error: Failed to parse expression statement."); + None } - Some(ASTNode::Statement(StatementNode::Expression(expr))) } else { - println!("Error: Failed to parse expression statement."); + println!( + "Error: Unexpected token, cannot start a statement with: {:?}", + token.token_type + ); None } - } else { - println!( - "Error: Unexpected token, cannot start a statement with: {:?}", - token.token_type - ); - tokens.next(); - None } - } - }; - - node + }; + + node + })(); + result.map(|value: ASTNode| { + let span = crate::source::node_span(before, tokens, &value); + value.with_span(span) + }) } diff --git a/front/parser/src/parser/types.rs b/front/parser/src/parser/types.rs index 4a18eb5f..5cfa5d16 100644 --- a/front/parser/src/parser/types.rs +++ b/front/parser/src/parser/types.rs @@ -63,6 +63,7 @@ pub fn split_top_level_generic_args(inner: &str) -> Option> { pub fn token_type_to_wave_type(token_type: &TokenType) -> Option { match token_type { TokenType::TypeVoid => Some(WaveType::Void), + TokenType::Not => Some(WaveType::Never), TokenType::TypeInt(bits) => Some(WaveType::Int(*bits)), TokenType::TokenTypeInt(int_type) => match int_type { IntegerType::I8 => Some(WaveType::Int(8)), @@ -73,7 +74,7 @@ pub fn token_type_to_wave_type(token_type: &TokenType) -> Option { IntegerType::I256 => Some(WaveType::Int(256)), IntegerType::I512 => Some(WaveType::Int(512)), IntegerType::I1024 => Some(WaveType::Int(1024)), - _ => panic!("Unhandled integer type: {:?}", int_type), + IntegerType::ISZ => Some(WaveType::Isz), }, TokenType::TypeUint(bits) => Some(WaveType::Uint(*bits)), TokenType::TokenTypeUint(uint_type) => match uint_type { @@ -85,7 +86,7 @@ pub fn token_type_to_wave_type(token_type: &TokenType) -> Option { UnsignedIntegerType::U256 => Some(WaveType::Uint(256)), UnsignedIntegerType::U512 => Some(WaveType::Uint(512)), UnsignedIntegerType::U1024 => Some(WaveType::Uint(1024)), - _ => panic!("Unhandled uint type: {:?}", uint_type), + UnsignedIntegerType::USZ => Some(WaveType::Usz), }, TokenType::TokenTypeFloat(float_type) => match float_type { FloatType::F32 => Some(WaveType::Float(32)), @@ -120,12 +121,23 @@ pub fn is_expression_start(token_type: &TokenType) -> bool { | TokenType::Deref | TokenType::Null | TokenType::CharLiteral(_) + | TokenType::BoolLiteral(_) + | TokenType::Plus + | TokenType::Minus + | TokenType::Not + | TokenType::BitwiseNot + | TokenType::AddressOf + | TokenType::Increment + | TokenType::Decrement ) } pub fn parse_type(type_str: &str) -> Option { let type_str = type_str.trim(); + if type_str == "!" { + return Some(TokenType::Not); + } if type_str == "void" { return Some(TokenType::TypeVoid); } @@ -150,7 +162,8 @@ pub fn parse_type(type_str: &str) -> Option { let size_str = args[1].trim(); let elem_type = parse_type(elem_type_str)?; - let size = size_str.parse::().ok()?; + let size = + u32::try_from(lexer::number::IntegerLiteral::parse(size_str)?.to_i128()?).ok()?; return Some(TokenType::TypeArray(Box::new(elem_type), size)); } @@ -167,23 +180,33 @@ pub fn parse_type(type_str: &str) -> Option { return Some(TokenType::TypeCustom(type_str.to_string())); } - if type_str.starts_with('i') { - let bits = type_str[1..].parse::().ok()?; - return Some(TokenType::TypeInt(bits)); - } else if type_str.starts_with('u') { - let bits = type_str[1..].parse::().ok()?; - return Some(TokenType::TypeUint(bits)); - } else if type_str.starts_with('f') { - let bits = type_str[1..].parse::().ok()?; - return Some(TokenType::TypeFloat(bits)); - } else if type_str == "bool" { - return Some(TokenType::TypeBool); - } else if type_str == "char" { - return Some(TokenType::TypeChar); - } else if type_str == "byte" { - return Some(TokenType::TypeByte); - } else if type_str == "str" { - return Some(TokenType::TypeString); + match type_str { + "isz" => return Some(TokenType::TokenTypeInt(IntegerType::ISZ)), + "usz" => return Some(TokenType::TokenTypeUint(UnsignedIntegerType::USZ)), + "bool" => return Some(TokenType::TypeBool), + "char" => return Some(TokenType::TypeChar), + "byte" => return Some(TokenType::TypeByte), + "str" => return Some(TokenType::TypeString), + _ => {} + } + if let Some(prefix @ ('i' | 'u' | 'f')) = type_str.chars().next() { + let suffix = &type_str[1..]; + if !suffix.is_empty() && suffix.bytes().all(|ch| ch.is_ascii_digit()) { + let bits = suffix.parse::().ok()?; + if suffix != bits.to_string() { + return None; + } + return match prefix { + 'i' if matches!(bits, 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024) => { + Some(TokenType::TypeInt(bits)) + } + 'u' if matches!(bits, 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024) => { + Some(TokenType::TypeUint(bits)) + } + 'f' if matches!(bits, 32 | 64) => Some(TokenType::TypeFloat(bits)), + _ => None, + }; + } } if type_str.split("::").all(|segment| { @@ -236,29 +259,7 @@ pub fn parse_type_from_token(token_opt: Option<&&Token>) -> Option { | ty @ TokenType::TokenTypeUint(_) | ty @ TokenType::TokenTypeFloat(_) => token_type_to_wave_type(ty), - TokenType::Identifier(name) => match name.as_str() { - "i8" => Some(WaveType::Int(8)), - "i16" => Some(WaveType::Int(16)), - "i32" => Some(WaveType::Int(32)), - "i64" => Some(WaveType::Int(64)), - "u8" => Some(WaveType::Uint(8)), - "u16" => Some(WaveType::Uint(16)), - "u32" => Some(WaveType::Uint(32)), - "u64" => Some(WaveType::Uint(64)), - "f32" => Some(WaveType::Float(32)), - "f64" => Some(WaveType::Float(64)), - "bool" => Some(WaveType::Bool), - "char" => Some(WaveType::Char), - "byte" => Some(WaveType::Byte), - "str" => Some(WaveType::String), - _ => { - if let Some(tt) = parse_type(name) { - token_type_to_wave_type(&tt) - } else { - Some(WaveType::Struct(name.clone())) - } - } - }, + TokenType::Identifier(name) => token_type_to_wave_type(&parse_type(name)?), _ => None, } diff --git a/front/parser/src/source.rs b/front/parser/src/source.rs new file mode 100644 index 00000000..148bd0a9 --- /dev/null +++ b/front/parser/src/source.rs @@ -0,0 +1,266 @@ +//! Source provenance retained independently of the backend and node allocation. +use crate::ast::*; +use error::SourceSpan; +use std::collections::HashMap; + +#[derive(Debug, Default)] +pub struct SourceMap { + pub nodes: HashMap, + pub node_order: Vec, + pub expressions: HashMap, + pub patterns: HashMap, +} + +impl SourceMap { + /// Remove syntax wrappers only after the owning allocation is stable. + /// Child boxes/vectors keep their allocations; maps refer to the resulting nodes. + pub fn detach(nodes: &mut [ASTNode]) -> Self { + let mut map = Self::default(); + for node in nodes { + map.node(node); + } + map + } + + fn function(&mut self, function: &mut FunctionNode) { + for parameter in &mut function.parameters { + if let Some(default) = &mut parameter.initial_value { + self.expression(default); + } + } + for node in &mut function.body { + self.node(node); + } + } + + fn node(&mut self, node: &mut ASTNode) { + let span = node.span().cloned(); + while matches!(node, ASTNode::Located { .. }) { + let ASTNode::Located { value, .. } = + std::mem::replace(node, ASTNode::Expression(Expression::Null)) + else { + unreachable!() + }; + *node = *value; + } + self.node_order.push(node as *const _ as usize); + if let Some(span) = span { + self.nodes.insert(node as *const _ as usize, span); + } + match node { + ASTNode::Function(f) => self.function(f), + ASTNode::Struct(s) => { + for f in &mut s.methods { + self.function(f); + } + } + ASTNode::ProtoImpl(p) => { + for f in &mut p.methods { + self.function(f); + } + } + ASTNode::Program(p) => { + if let Some(e) = &mut p.initial_value { + self.expression(e); + } + } + ASTNode::Variable(v) => { + if let Some(e) = &mut v.initial_value { + self.expression(e); + } + } + ASTNode::Expression(e) => self.expression(e), + ASTNode::Statement(s) => self.statement(s), + _ => {} + } + } + + fn statement(&mut self, statement: &mut StatementNode) { + match statement { + StatementNode::PrintFormat { args, .. } + | StatementNode::PrintlnFormat { args, .. } + | StatementNode::Input { args, .. } => { + for e in args { + self.expression(e); + } + } + StatementNode::If { + condition, + body, + else_if_blocks, + else_block, + } => { + self.expression(condition); + for node in body { + self.node(node); + } + if let Some(blocks) = else_if_blocks { + for (condition, body) in blocks.iter_mut() { + self.expression(condition); + for node in body { + self.node(node); + } + } + } + if let Some(body) = else_block { + for node in body.iter_mut() { + self.node(node); + } + } + } + StatementNode::For { + initialization, + condition, + increment, + body, + } => { + self.node(initialization); + self.expression(condition); + self.expression(increment); + for node in body { + self.node(node); + } + } + StatementNode::While { condition, body } => { + self.expression(condition); + for node in body { + self.node(node); + } + } + StatementNode::Match { value, arms } => { + self.expression(value); + for arm in arms { + self.pattern(&mut arm.pattern); + for node in &mut arm.body { + self.node(node); + } + } + } + StatementNode::Assign { value, .. } + | StatementNode::Return(Some(value)) + | StatementNode::Expression(value) => self.expression(value), + StatementNode::AsmBlock { + inputs, outputs, .. + } => { + for (_, e) in inputs.iter_mut().chain(outputs.iter_mut()) { + self.expression(e); + } + } + _ => {} + } + } + + fn expression(&mut self, expression: &mut Expression) { + let span = expression.span().cloned(); + while matches!(expression, Expression::Located { .. }) { + let Expression::Located { value, .. } = std::mem::replace(expression, Expression::Null) + else { + unreachable!() + }; + *expression = *value; + } + if let Some(span) = span { + self.expressions + .insert(expression as *const _ as usize, span); + } + match expression { + Expression::StructLiteral { fields, .. } => { + for (_, e) in fields { + self.expression(e); + } + } + Expression::FunctionCall { args, .. } | Expression::ArrayLiteral(args) => { + for e in args { + self.expression(e); + } + } + Expression::MethodCall { object, args, .. } => { + self.expression(object); + for e in args { + self.expression(e); + } + } + Expression::Deref(e) + | Expression::AddressOf(e) + | Expression::Grouped(e) + | Expression::Unary { expr: e, .. } + | Expression::Cast { expr: e, .. } + | Expression::FieldAccess { object: e, .. } + | Expression::IncDec { target: e, .. } => self.expression(e), + Expression::BinaryExpression { left, right, .. } + | Expression::IndexAccess { + target: left, + index: right, + } + | Expression::AssignOperation { + target: left, + value: right, + .. + } + | Expression::Assignment { + target: left, + value: right, + } => { + self.expression(left); + self.expression(right); + } + Expression::AsmBlock { + inputs, outputs, .. + } => { + for (_, e) in inputs.iter_mut().chain(outputs.iter_mut()) { + self.expression(e); + } + } + _ => {} + } + } + + fn pattern(&mut self, pattern: &mut MatchPattern) { + let span = pattern.span().cloned(); + while matches!(pattern, MatchPattern::Located { .. }) { + let MatchPattern::Located { value, .. } = + std::mem::replace(pattern, MatchPattern::Wildcard) + else { + unreachable!() + }; + *pattern = *value; + } + if let Some(span) = span { + self.patterns.insert(pattern as *const _ as usize, span); + } + if let MatchPattern::Variant { payloads, .. } = pattern { + for p in payloads { + self.pattern(p); + } + } + } +} + +/// Choose a declaration's name from its consumed token stream, never source text. +pub fn node_span<'a, T>( + before: std::iter::Peekable, + after: &mut std::iter::Peekable, + node: &ASTNode, +) -> Option +where + T: Iterator + Clone, +{ + let mut span = lexer::consumed_span(before.clone(), after)?; + let name = match node.unspanned() { + ASTNode::Function(f) => Some(&f.name), + ASTNode::ExternFunction(f) => Some(&f.name), + ASTNode::Variable(v) => Some(&v.name), + ASTNode::Struct(s) => Some(&s.name), + ASTNode::Variant(v) => Some(&v.name), + ASTNode::Enum(e) => Some(&e.name), + ASTNode::TypeAlias(t) => Some(&t.name), + ASTNode::ProtoImpl(p) => Some(&p.target), + _ => None, + }; + if let Some(name) = name { + span.focus = before.take_while(|token| token.span.as_ref().is_some_and(|s| s.start < span.end)) + .find(|token| matches!(&token.token_type, lexer::token::TokenType::Identifier(value) if value == name)) + .and_then(|token| token.span.clone()).map(Box::new); + } + Some(span) +} diff --git a/front/parser/src/verification.rs b/front/parser/src/verification.rs index 8ebebe0a..70600c15 100644 --- a/front/parser/src/verification.rs +++ b/front/parser/src/verification.rs @@ -14,8 +14,8 @@ //! //! The verifier runs after imports and generics have been expanded. It first //! collects declarations into a program-wide type environment, then validates -//! bodies with lexical scopes and expected types. It reports source-oriented -//! hints instead of retaining parser token positions in the AST. +//! bodies with lexical scopes and expected types. Physical diagnostics use the +//! detached AST source map; legacy unlocated callers retain descriptive hints. use crate::ast::{ ASTNode, AssignOperator, Expression, FunctionNode, IncDecKind, Literal, MatchPattern, @@ -46,6 +46,7 @@ pub struct SemanticDiagnostic { pub message: String, pub top_level_index: usize, pub primary: Option, + pub span: Option, pub label: String, pub note: Option, pub help: String, @@ -464,6 +465,8 @@ impl ProgramTypes { context: &str, ) -> Result<(), String> { match ty { + WaveType::Isz | WaveType::Usz => Err(format!("{context}: target-sized integer requires target resolution before semantic analysis")), + WaveType::Never if !allow_void => Err(format!("{context} cannot use the return-only `!` type")), WaveType::Void if !allow_void => Err(format!("{} cannot use the `void` type", context)), WaveType::Pointer(inner) | WaveType::Array(inner, _) => { self.validate_type(inner, generic_params, false, context) @@ -587,36 +590,7 @@ fn insert_unique_method( } fn parse_integer_value(raw: &str) -> Option { - let raw = raw.trim().replace('_', ""); - let (negative, unsigned) = if let Some(value) = raw.strip_prefix('-') { - (true, value) - } else { - (false, raw.strip_prefix('+').unwrap_or(&raw)) - }; - let (radix, digits) = if let Some(value) = unsigned - .strip_prefix("0x") - .or_else(|| unsigned.strip_prefix("0X")) - { - (16, value) - } else if let Some(value) = unsigned - .strip_prefix("0b") - .or_else(|| unsigned.strip_prefix("0B")) - { - (2, value) - } else if let Some(value) = unsigned - .strip_prefix("0o") - .or_else(|| unsigned.strip_prefix("0O")) - { - (8, value) - } else { - (10, unsigned) - }; - let value = i128::from_str_radix(digits, radix).ok()?; - if negative { - value.checked_neg() - } else { - Some(value) - } + lexer::number::IntegerLiteral::parse(raw)?.to_i128() } fn function_type(function: &FunctionNode) -> FunctionType { @@ -795,6 +769,8 @@ struct Validator<'a> { top_level_index: usize, span_counts: HashMap<(SemanticSpanKind, String), usize>, primary_span: Option, + source_map: &'a crate::source::SourceMap, + source_span: Option, diagnostic_help: Option, expression_types: HashMap, hir_expression_types: HashMap, @@ -803,7 +779,7 @@ struct Validator<'a> { } impl<'a> Validator<'a> { - fn new(program: &'a ProgramTypes) -> Self { + fn new(program: &'a ProgramTypes, source_map: &'a crate::source::SourceMap) -> Self { Self { program, scopes: vec![HashMap::new()], @@ -814,6 +790,8 @@ impl<'a> Validator<'a> { top_level_index: 0, span_counts: HashMap::new(), primary_span: None, + source_map, + source_span: None, diagnostic_help: None, expression_types: HashMap::new(), hir_expression_types: HashMap::new(), @@ -850,6 +828,7 @@ impl<'a> Validator<'a> { message, top_level_index: self.top_level_index, primary: self.primary_span.clone(), + span: self.source_span.clone(), note: None, help: self.diagnostic_help.clone().unwrap_or_else(|| { "fix type, mutability, scope, and control-flow errors".to_string() @@ -903,6 +882,9 @@ impl<'a> Validator<'a> { .extend(function.generic_params.iter().cloned()); for parameter in &function.parameters { + if let Some(span) = ¶meter.span { + self.source_span = Some(span.clone()); + } self.program.validate_type( ¶meter.param_type, &self.current_type_params, @@ -913,6 +895,27 @@ impl<'a> Validator<'a> { ), )?; } + let mut saw_default = false; + for parameter in &function.parameters { + if let Some(value) = ¶meter.initial_value { + saw_default = true; + let actual = self.validate_expr_expected(value, Some(¶meter.param_type))?; + self.require_assignable( + &actual, + ¶meter.param_type, + &format!("default for `{}`", parameter.name), + )?; + } else if saw_default { + return Err(format!( + "required parameter `{}` follows a default parameter", + parameter.name + )); + } + } + self.source_span = function + .return_type_span + .clone() + .or_else(|| function.span.clone()); let return_type = function.return_type.clone().unwrap_or(WaveType::Void); self.program.validate_type( &return_type, @@ -936,6 +939,7 @@ impl<'a> Validator<'a> { let falls_through = validator.validate_block(&function.body)?; let return_type = function.return_type.clone().unwrap_or(WaveType::Void); if return_type != WaveType::Void && falls_through { + validator.source_span = function.span.clone(); return Err(format!( "non-void function `{}` may exit without returning `{}`", display_name, @@ -986,6 +990,9 @@ impl<'a> Validator<'a> { } fn validate_node(&mut self, node: &ASTNode) -> Result { + if let Some(span) = self.source_map.nodes.get(&(node as *const _ as usize)) { + self.source_span = Some(span.clone()); + } match node { ASTNode::Variable(variable) => { self.mark_span(SemanticSpanKind::Declaration, variable.name.clone()); @@ -1038,8 +1045,8 @@ impl<'a> Validator<'a> { } ASTNode::Statement(statement) => self.validate_statement(statement), ASTNode::Expression(expression) => { - self.validate_expr(expression)?; - Ok(true) + let ty = self.validate_expr(expression)?; + Ok(!matches!(ty, ExpressionType::Known(WaveType::Never))) } _ => Ok(true), } @@ -1048,8 +1055,8 @@ impl<'a> Validator<'a> { fn validate_statement(&mut self, statement: &StatementNode) -> Result { match statement { StatementNode::Expression(expression) => { - self.validate_expr(expression)?; - Ok(true) + let ty = self.validate_expr(expression)?; + Ok(!matches!(ty, ExpressionType::Known(WaveType::Never))) } StatementNode::Assign { variable, value } => { self.mark_span(SemanticSpanKind::Identifier, variable.clone()); @@ -1198,12 +1205,15 @@ impl<'a> Validator<'a> { Ok(false) } StatementNode::AsmBlock { - inputs, outputs, .. + inputs, + outputs, + clobbers, + .. } => { for (_, expression) in inputs.iter().chain(outputs.iter()) { self.validate_expr(expression)?; } - Ok(true) + Ok(!clobbers.iter().any(|clobber| clobber == "noreturn")) } _ => Ok(true), } @@ -1215,7 +1225,17 @@ impl<'a> Validator<'a> { let mut all_arms_terminate = !arms.is_empty(); for arm in arms { + if let Some(span) = self + .source_map + .patterns + .get(&(&arm.pattern as *const _ as usize)) + { + self.source_span = Some(span.clone()); + } let key = match &arm.pattern { + MatchPattern::Located { .. } => { + unreachable!("source wrappers detached before analysis") + } MatchPattern::Int(raw) => { self.mark_span(SemanticSpanKind::Keyword, raw.clone()); let value = parse_integer_value(raw) @@ -1278,6 +1298,13 @@ impl<'a> Validator<'a> { let mut all_arms_terminate = !arms.is_empty(); for arm in arms { + if let Some(span) = self + .source_map + .patterns + .get(&(&arm.pattern as *const _ as usize)) + { + self.source_span = Some(span.clone()); + } if self.variant_patterns_cover(&covered_patterns, &expected_type) { return Err( if covered_patterns @@ -1293,6 +1320,9 @@ impl<'a> Validator<'a> { } let mut bindings = HashMap::new(); match &arm.pattern { + MatchPattern::Located { .. } => { + unreachable!("source wrappers detached before analysis") + } MatchPattern::Wildcard => {} MatchPattern::Variant { variant_type, @@ -1383,6 +1413,9 @@ impl<'a> Validator<'a> { bindings: &mut HashMap, ) -> Result<(), String> { match pattern { + MatchPattern::Located { .. } => { + unreachable!("source wrappers detached before analysis") + } MatchPattern::Binding(name) => { if bindings.insert(name.clone(), expected.clone()).is_some() { return Err(format!("duplicate pattern binding `{}`", name)); @@ -1451,6 +1484,9 @@ impl<'a> Validator<'a> { fn variant_pattern_is_irrefutable(&self, pattern: &MatchPattern, expected: &WaveType) -> bool { match pattern { + MatchPattern::Located { .. } => { + unreachable!("source wrappers detached before analysis") + } MatchPattern::Binding(_) | MatchPattern::Wildcard => true, MatchPattern::Variant { variant_type, @@ -1550,6 +1586,9 @@ impl<'a> Validator<'a> { let expected = self.current_return_type.clone().unwrap_or(WaveType::Void); match (expected, value) { + (WaveType::Never, _) => Err(format!( + "never-returning function `{function}` cannot contain a return statement" + )), (WaveType::Void, None) => Ok(()), (WaveType::Void, Some(_)) => Err(format!( "void function `{}` cannot return a value", @@ -1664,6 +1703,14 @@ impl<'a> Validator<'a> { expression: &Expression, expected: Option<&WaveType>, ) -> Result { + if let Some(span) = self + .source_map + .expressions + .get(&(expression as *const _ as usize)) + { + self.source_span = Some(span.clone()); + } + let result = self.validate_expr_inner(expression, expected); if let Ok(expression_type) = &result { self.hir_expression_types.insert( @@ -1684,9 +1731,40 @@ impl<'a> Validator<'a> { expected: Option<&WaveType>, ) -> Result { match expression { + Expression::Located { .. } => unreachable!("source wrappers detached before analysis"), Expression::Literal(literal) => Ok(match literal { - Literal::Int(raw) => ExpressionType::IntLiteral(raw.clone()), - Literal::Float(_) => ExpressionType::FloatLiteral, + Literal::Int(raw) => { + if let Some(WaveType::Float(bits)) = + expected.map(|ty| self.program.canonical_type(ty)) + { + let value = lexer::number::IntegerLiteral::parse(raw) + .and_then(|value| value.to_f64()) + .ok_or_else(|| { + "integer literal is out of range for floating-point conversion" + .to_string() + })?; + if bits == 32 && !(value as f32).is_finite() { + return Err( + "integer literal is out of range for f32 conversion".to_string() + ); + } + } + ExpressionType::IntLiteral(raw.clone()) + } + Literal::Float(value) => { + if !value.is_finite() + || (matches!( + expected.map(|ty| self.program.canonical_type(ty)), + Some(WaveType::Float(32)) + ) && !(*value as f32).is_finite()) + { + return Err( + "floating-point literal is out of range for its expected type" + .to_string(), + ); + } + ExpressionType::FloatLiteral + } Literal::String(_) => ExpressionType::Known(WaveType::String), Literal::Bool(_) => ExpressionType::Known(WaveType::Bool), Literal::Char(_) => ExpressionType::Known(WaveType::Char), @@ -1704,7 +1782,7 @@ impl<'a> Validator<'a> { Err(format!("use of undeclared identifier `{}`", name)) } } - Expression::Grouped(inner) => self.validate_expr(inner), + Expression::Grouped(inner) => self.validate_expr_expected(inner, expected), Expression::Cast { expr, target_type } => { self.mark_span(SemanticSpanKind::Keyword, "as"); self.program.validate_type( @@ -1713,7 +1791,7 @@ impl<'a> Validator<'a> { false, "cast target", )?; - let source = self.validate_expr(expr)?; + let source = self.validate_expr_expected(expr, Some(target_type))?; if !self.is_valid_cast(&source, target_type) { return Err(format!( "invalid cast from `{}` to `{}`", @@ -1851,6 +1929,13 @@ impl<'a> Validator<'a> { Expression::FieldAccess { object, field } => { self.mark_span(SemanticSpanKind::Identifier, field.clone()); let object_type = self.validate_expr(object)?; + if let Some(span) = self + .source_map + .expressions + .get(&(expression as *const _ as usize)) + { + self.source_span = Some(span.clone()); + } let structure = match &object_type { ExpressionType::Known(WaveType::Struct(name)) => Some(name.clone()), ExpressionType::Known(WaveType::Pointer(inner)) => match inner.as_ref() { @@ -2895,6 +2980,7 @@ fn find_base_var(target: &Expression, saw_deref: bool) -> Option<(String, bool)> fn is_lvalue_expression(expression: &Expression) -> bool { match expression { + Expression::Located { .. } => unreachable!("source wrappers detached before analysis"), Expression::Variable(_) | Expression::FieldAccess { .. } | Expression::IndexAccess { .. } @@ -2955,6 +3041,7 @@ impl ConditionMutation { fn condition_mutation(expression: &Expression) -> Option { match expression { + Expression::Located { value, .. } => condition_mutation(value), Expression::Assignment { .. } => Some(ConditionMutation::Assignment("=")), Expression::AssignOperation { operator, .. } => { let symbol = assign_operator_source_symbol(operator); @@ -3039,18 +3126,7 @@ fn node_breaks_current_loop(node: &ASTNode) -> bool { } fn int_literal_is_zero(raw: &str) -> bool { - let raw = raw.trim().replace('_', ""); - let raw = raw.strip_prefix('+').unwrap_or(&raw); - if let Some(hex) = raw.strip_prefix("0x").or_else(|| raw.strip_prefix("0X")) { - return u128::from_str_radix(hex, 16).ok() == Some(0); - } - if let Some(binary) = raw.strip_prefix("0b").or_else(|| raw.strip_prefix("0B")) { - return u128::from_str_radix(binary, 2).ok() == Some(0); - } - if let Some(octal) = raw.strip_prefix("0o").or_else(|| raw.strip_prefix("0O")) { - return u128::from_str_radix(octal, 8).ok() == Some(0); - } - raw.parse::().ok() == Some(0) + lexer::number::IntegerLiteral::parse(raw).is_some_and(|n| n.is_zero()) } fn integer_literal_fits(raw: &str, ty: &WaveType) -> bool { @@ -3083,34 +3159,12 @@ fn integer_literal_fits(raw: &str, ty: &WaveType) -> bool { } fn integer_literal_parts(raw: &str) -> Option<(bool, u32, String)> { - let raw = raw.trim().replace('_', ""); - let (negative, unsigned) = if let Some(value) = raw.strip_prefix('-') { - (true, value) - } else { - (false, raw.strip_prefix('+').unwrap_or(&raw)) - }; - let (radix, digits) = if let Some(value) = unsigned - .strip_prefix("0x") - .or_else(|| unsigned.strip_prefix("0X")) - { - (16, value) - } else if let Some(value) = unsigned - .strip_prefix("0b") - .or_else(|| unsigned.strip_prefix("0B")) - { - (2, value) - } else if let Some(value) = unsigned - .strip_prefix("0o") - .or_else(|| unsigned.strip_prefix("0O")) - { - (8, value) - } else { - (10, unsigned) - }; - if digits.is_empty() || !digits.chars().all(|ch| ch.is_digit(radix)) { - return None; - } - Some((negative, radix, digits.trim_start_matches('0').to_string())) + let n = lexer::number::IntegerLiteral::parse(raw)?; + Some(( + n.negative, + n.radix, + n.digits.trim_start_matches('0').to_string(), + )) } fn unsigned_literal_bit_len(radix: u32, digits: &str) -> Option { @@ -3231,6 +3285,8 @@ fn display_expression_type(ty: &ExpressionType) -> String { fn display_wave_type(ty: &WaveType) -> String { match ty { + WaveType::Isz => "isz".to_string(), + WaveType::Usz => "usz".to_string(), WaveType::Int(bits) => format!("i{}", bits), WaveType::Uint(bits) => format!("u{}", bits), WaveType::Float(bits) => format!("f{}", bits), @@ -3241,6 +3297,7 @@ fn display_wave_type(ty: &WaveType) -> String { WaveType::Pointer(inner) => format!("ptr<{}>", display_wave_type(inner)), WaveType::Array(inner, size) => format!("array<{}, {}>", display_wave_type(inner), size), WaveType::Void => "void".to_string(), + WaveType::Never => "!".to_string(), WaveType::Struct(name) => name.clone(), WaveType::Variant(name) => name.clone(), } @@ -3248,6 +3305,7 @@ fn display_wave_type(ty: &WaveType) -> String { fn variant_pattern_key(pattern: &MatchPattern) -> String { match pattern { + MatchPattern::Located { .. } => unreachable!("source wrappers detached before analysis"), MatchPattern::Int(raw) => format!("int:{}", raw), MatchPattern::Ident(name) => format!("ident:{}", name), MatchPattern::Binding(_) | MatchPattern::Wildcard => "*".to_string(), @@ -3273,17 +3331,21 @@ pub fn validate_program(nodes: &Vec) -> Result<(), String> { } pub fn validate_program_detailed(nodes: &[ASTNode]) -> Result<(), SemanticDiagnostic> { - analyze_expression_types(nodes).map(|_| ()) + let mut syntax = nodes.to_vec().into_boxed_slice(); + let sources = crate::source::SourceMap::detach(&mut syntax); + analyze_program_types(&syntax, &sources).map(|_| ()) } pub fn analyze_expression_types( nodes: &[ASTNode], ) -> Result, SemanticDiagnostic> { - analyze_program_types(nodes).map(|analysis| analysis.expression_types) + analyze_program_types(nodes, &crate::source::SourceMap::default()) + .map(|analysis| analysis.expression_types) } pub(crate) fn analyze_hir_expression_types( nodes: &[ASTNode], + sources: &crate::source::SourceMap, ) -> Result< ( HashMap, @@ -3292,7 +3354,7 @@ pub(crate) fn analyze_hir_expression_types( ), SemanticDiagnostic, > { - analyze_program_types(nodes).map(|analysis| { + analyze_program_types(nodes, sources).map(|analysis| { ( analysis.hir_expression_types, analysis.hir_variant_constructions, @@ -3312,15 +3374,30 @@ fn is_supported_foreign_abi(abi: &str) -> bool { abi.eq_ignore_ascii_case("c") || abi.eq_ignore_ascii_case("system") } -fn analyze_program_types(nodes: &[ASTNode]) -> Result { +fn analyze_program_types( + nodes: &[ASTNode], + sources: &crate::source::SourceMap, +) -> Result { let program = ProgramTypes::collect(nodes).map_err(|(index, message, primary)| { - semantic_diagnostic_for_top_level(nodes, index, message, primary) + let mut diagnostic = semantic_diagnostic_for_top_level(nodes, index, message, primary); + diagnostic.span = nodes + .get(index) + .and_then(|node| sources.nodes.get(&(node as *const _ as usize))) + .cloned(); + diagnostic + })?; + validate_declaration_types(nodes, &program).map_err(|mut diagnostic| { + diagnostic.span = nodes + .get(diagnostic.top_level_index) + .and_then(|node| sources.nodes.get(&(node as *const _ as usize))) + .cloned(); + diagnostic })?; - validate_declaration_types(nodes, &program)?; - let mut validator = Validator::new(&program); + let mut validator = Validator::new(&program, sources); for (index, node) in nodes.iter().enumerate() { validator.begin_top_level(index, top_level_span_hint(node)); + validator.source_span = sources.nodes.get(&(node as *const _ as usize)).cloned(); let result = match node { ASTNode::Function(function) => { if let Some(export) = &function.export { @@ -3398,6 +3475,7 @@ fn analyze_program_types(nodes: &[ASTNode]) -> Result SemanticSpanHint { let (kind, text) = match node { + ASTNode::Located { value, .. } => return top_level_span_hint(value), ASTNode::Function(function) => (SemanticSpanKind::Declaration, function.name.clone()), ASTNode::ExternFunction(function) => (SemanticSpanKind::Declaration, function.name.clone()), ASTNode::Struct(structure) => (SemanticSpanKind::Declaration, structure.name.clone()), @@ -3432,6 +3510,7 @@ fn semantic_diagnostic_for_top_level( message, top_level_index: index, primary, + span: nodes.get(index).and_then(ASTNode::span).cloned(), note: None, help: "fix type, mutability, scope, and control-flow errors".to_string(), } diff --git a/front/parser/tests/alpha_frontend.rs b/front/parser/tests/alpha_frontend.rs new file mode 100644 index 00000000..56a5ed1e --- /dev/null +++ b/front/parser/tests/alpha_frontend.rs @@ -0,0 +1,234 @@ +//! Alpha grammar regressions: malformed source must never be silently accepted. +use lexer::Lexer; +use parser::ast::{ASTNode, Expression, Literal}; +use parser::generics::monomorphize_generics; +use parser::hir::TypedProgram; +use parser::import::{preprocess_target_attrs, TargetConditionContext}; +use parser::parse_syntax_only; + +fn syntax(src: &str) -> Result, String> { + let tokens = Lexer::new(src).tokenize().map_err(|e| format!("{e:?}"))?; + parse_syntax_only(&tokens).map_err(|e| format!("{e:?}")) +} + +#[test] +fn rejects_unterminated_and_unsupported_statements_at_every_depth() { + for body in [ + "1 ? 2;", + "ping() ping();", + "return", + "return 1", + "break", + "continue", + "asm { ? }", + "var x: i32 = asm { ? };", + ] { + for nested in [false, true] { + let body = if nested { + format!("if (true) {{ {body} }}") + } else { + body.into() + }; + assert!( + syntax(&format!("fun main() {{ {body} }}")).is_err(), + "{body}" + ); + } + } + assert!(syntax("fun main() { asm { \"nop\"").is_err()); + assert!(syntax("fun main() { var x: i32 = asm { \"nop\"").is_err()); +} + +#[test] +fn expression_statements_have_the_same_grammar_in_all_blocks() { + for expression in [ + "true", "false", "!true", "~1", "-1", "+1", "&x", "deref p", "++x", "--x", "x++", "x = 2", + "x += 2", "(1)", "[1, 2]", "null", "'a'", "\"text\"", "call()", + ] { + for depth in 0..=3 { + let mut body = format!("{expression};"); + for _ in 0..depth { + body = format!("if (true) {{ {body} }}"); + } + syntax(&format!("fun main() {{ {body} }}")).unwrap_or_else(|e| panic!("{body}: {e}")); + } + } +} + +#[test] +fn numeric_defaults_preserve_radix_and_large_values() { + for value in [ + "0x10", + "0b10000", + "0o20", + "16", + "1_024", + "-0x10", + "18446744073709551616", + ] { + let nodes = syntax(&format!( + "fun value(x: i128 = {value}) -> i128 {{ return x; }} fun main() {{ value(); }}" + )) + .unwrap(); + let ASTNode::Function(f) = &nodes[0] else { + panic!() + }; + assert!(matches!( + &f.parameters[0].initial_value, + Some(Expression::Literal(Literal::Int(_))) + )); + TypedProgram::lower(nodes.clone()).expect("defaults validate before expansion"); + TypedProgram::lower(monomorphize_generics(nodes).unwrap()) + .expect("defaults validate after expansion"); + } + for declaration in [ + "x: i8 = 128", + "x: u8 = -1", + "x: i64 = 9999999999999999999999999999999999999999999999999999999999999", + "x: i8 = 1, y: i8", + ] { + assert!( + TypedProgram::lower(syntax(&format!("fun f({declaration}) {{}} ")).unwrap()).is_err(), + "{declaration}" + ); + } + for declaration in [ + "x: i32 = 0x", + "x: i32 = 0b102", + "x: i32 = 12u8", + "x: i32 = 1__2", + "x: i32 =", + "x i32", + "x: i32 y: i32", + "x: i32 = unknown", + ] { + assert!( + syntax(&format!("fun f({declaration}) {{}} ")).is_err(), + "{declaration}" + ); + } +} + +#[test] +fn target_filter_removes_complete_multiline_declarations() { + let target = TargetConditionContext { + arch: Some("amd64".into()), + os: Some("linux".into()), + ..Default::default() + }; + for declaration in [ + "variant Choice {\n Empty,\n Value(i32),\n}", + "pub variant Choice {\n Empty,\n Value(i32),\n}", + "struct Pair {\n x: i32;\n}", + "enum Mode {\n A,\n B,\n}", + "fun f() {\n return;\n}", + "type Item = i32;", + "const ITEM: i32 = 1;", + "static item: i32 = 1;", + "import(\"absent\");", + "extern(\"C\") fun f();", + "export(\"C\") fun f() {\n return;\n}", + "proto Pair {\n fun f(self: ptr) {}\n}", + ] { + let filtered = preprocess_target_attrs( + &format!("#[target(arch=\"arm64\")]\n{declaration}\nfun main() {{}}"), + &target, + ); + let nodes = syntax(&filtered).unwrap_or_else(|e| panic!("{declaration}: {e}")); + assert_eq!(nodes.len(), 1, "{declaration}: {filtered}"); + assert!(matches!(&nodes[0], ASTNode::Function(f) if f.name == "main")); + } +} + +#[test] +fn target_sized_types_resolve_recursively_without_host_assumptions() { + use parser::ast::WaveType; + use parser::hir::resolve_target_types; + use parser::types::{parse_type, token_type_to_wave_type}; + assert_eq!( + token_type_to_wave_type(&parse_type("isz").unwrap()), + Some(WaveType::Isz) + ); + assert_eq!( + token_type_to_wave_type(&parse_type("usz").unwrap()), + Some(WaveType::Usz) + ); + for invalid in [ + "i0", + "i1", + "i24", + "u7", + "u2048", + "i9999999999999", + "f16", + "f128", + "i032", + ] { + assert!(parse_type(invalid).is_none(), "{invalid}"); + assert!( + syntax(&format!("fun f(x: {invalid}) {{}} ")).is_err(), + "{invalid}" + ); + } + for name in ["item", "user", "file", "이름", "pkg::item"] { + assert!(parse_type(name).is_some()); + } + let source = "struct Box { value: T; } fun id(x: T) -> T { return x; } fun f(x: ptr>, y: Box) -> isz { var z: isz = id(16); return z; }"; + for bits in [32, 64] { + let mut nodes = syntax(source).unwrap(); + resolve_target_types(&mut nodes, bits).unwrap(); + let ASTNode::Function(f) = &nodes[2] else { + panic!() + }; + assert_eq!(f.return_type, Some(WaveType::Int(bits))); + assert_eq!( + f.parameters[0].param_type, + WaveType::Pointer(Box::new(WaveType::Array(Box::new(WaveType::Int(bits)), 2))) + ); + assert_eq!( + f.parameters[1].param_type, + WaveType::Struct(format!("Box")) + ); + TypedProgram::lower(monomorphize_generics(nodes).unwrap()).unwrap(); + } +} + +#[test] +fn never_returning_functions_must_terminate_without_returning() { + for source in [ + "fun stop() -> ! { while (true) {} } fun value() -> i32 { stop(); }", + "fun stop() -> ! { while (true) {} } fun forward() -> ! { stop(); }", + ] { + TypedProgram::lower(syntax(source).unwrap()).unwrap(); + } + for source in [ + "fun stop() -> ! {}", + "fun stop() -> ! { return; }", + "fun stop() -> ! { return 1; }", + "fun stop() -> ! { while (true) { break; } }", + "fun bad(x: !) {}", + "fun bad() { var x: !; }", + "type Bad = !;", + ] { + assert!( + TypedProgram::lower(syntax(source).unwrap()).is_err(), + "{source}" + ); + } +} + +#[test] +fn asm_operands_keep_casts_and_projections_instead_of_skipping_tokens() { + let nodes = syntax("fun f() { asm { in(\"r\") p as i64 out(\"r\") value.field } }").unwrap(); + let ASTNode::Function(f) = &nodes[0] else { + panic!() + }; + let ASTNode::Statement(parser::ast::StatementNode::AsmBlock { + inputs, outputs, .. + }) = &f.body[0] + else { + panic!() + }; + assert!(matches!(inputs[0].1, Expression::Cast { .. })); + assert!(matches!(outputs[0].1, Expression::FieldAccess { .. })); +} diff --git a/front/parser/tests/grammar_contract.rs b/front/parser/tests/grammar_contract.rs new file mode 100644 index 00000000..199c0868 --- /dev/null +++ b/front/parser/tests/grammar_contract.rs @@ -0,0 +1,94 @@ +//! Normative grammar fixtures and complete token-inventory drift detection. +use lexer::Lexer; +use parser::parse_syntax_with_spans; +use std::collections::BTreeSet; +use std::path::Path; + +#[test] +fn every_token_kind_is_classified_and_its_example_matches_the_lexer() { + let vocabulary = include_str!("../../lexer/src/token.rs") + .split("pub enum TokenType {") + .nth(1) + .unwrap() + .split("\n}") + .next() + .unwrap(); + let variants: BTreeSet<_> = vocabulary + .lines() + .filter_map(|line| { + let line = line.trim(); + if line.starts_with("//") || line.is_empty() { + return None; + } + Some(line.split(['(', ',']).next().unwrap()) + }) + .collect(); + let grammar = include_str!("../../../spec/alpha-0.ebnf"); + let productions: BTreeSet<_> = grammar + .lines() + .filter_map(|line| line.split_once(" = ").map(|p| p.0)) + .collect(); + let mut documented = BTreeSet::new(); + for row in include_str!("../../../spec/tokens.tsv") + .lines() + .filter(|line| !line.starts_with('#')) + { + let fields: Vec<_> = row.split('\t').collect(); + assert_eq!(fields.len(), 5, "{row}"); + assert!( + documented.insert(fields[0]), + "duplicate token {}", + fields[0] + ); + assert!( + matches!( + fields[1], + "implemented" | "reserved" | "removed" | "internal" + ), + "{row}" + ); + assert!(productions.contains(fields[3]), "{row}"); + if fields[4] != "-" { + let tokens = Lexer::new(fields[4]).tokenize().unwrap(); + assert_eq!(tokens.len(), 2, "{row}: {tokens:?}"); + if fields[1] == "reserved" { + assert!(tokens[0].token_type.reserved_spelling().is_some(), "{row}"); + for body in [format!("{};", fields[4]), format!("1 {} 2;", fields[4])] { + let source = format!("fun f() {{ {body} }}"); + let tokens = Lexer::new(&source).tokenize().unwrap(); + assert!(parse_syntax_with_spans(&tokens).is_err(), "{source}"); + } + } + let actual = format!("{:?}", tokens[0].token_type); + assert_eq!(actual.split('(').next().unwrap(), fields[0], "{row}"); + } + } + assert_eq!( + variants, documented, + "update spec/tokens.tsv when changing the token vocabulary" + ); +} + +#[test] +fn grammar_examples_accept_and_reject_as_documented() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../spec"); + let grammar = include_str!("../../../spec/alpha-0.ebnf"); + for row in include_str!("../../../spec/fixtures.tsv") + .lines() + .filter(|line| !line.starts_with('#')) + { + let (name, productions) = row.split_once('\t').unwrap(); + for production in productions.split(',') { + assert!(grammar.contains(&format!("{production} = "))); + } + for (verdict, accept) in [("accept", true), ("reject", false)] { + let file = root.join("fixtures").join(format!("{name}.{verdict}.wave")); + let source = std::fs::read_to_string(&file).unwrap(); + let result = Lexer::new_with_file(&source, file.display().to_string()) + .tokenize() + .map_err(|e| format!("{e:?}")) + .and_then(|tokens| parse_syntax_with_spans(&tokens).map_err(|e| format!("{e:?}"))); + assert_eq!(result.is_ok(), accept, "{}: {result:?}", file.display()); + } + } +} diff --git a/front/parser/tests/source_spans.rs b/front/parser/tests/source_spans.rs new file mode 100644 index 00000000..79c6c078 --- /dev/null +++ b/front/parser/tests/source_spans.rs @@ -0,0 +1,185 @@ +//! Source occurrence identity must survive frontend lowering without text searches. +use lexer::Lexer; +use parser::ast::{ASTNode, Expression, StatementNode}; +use parser::generics::monomorphize_generics; +use parser::hir::TypedProgram; +use parser::parse_syntax_with_spans; +use parser::verification::validate_program_detailed; + +fn parse(source: &str) -> Vec { + let tokens = Lexer::new_with_file(source, "unicode.wave") + .tokenize() + .unwrap(); + parse_syntax_with_spans(&tokens).unwrap() +} + +#[test] +fn tokens_preserve_raw_spelling_bytes_and_unicode_columns() { + let source = "// 🙂\r\nfun 이름() { \"a\\n\"; 이름; }"; + let tokens = Lexer::new_with_file(source, "unicode.wave") + .tokenize() + .unwrap(); + for token in &tokens { + let span = token.span.as_ref().unwrap(); + assert_eq!(&source[span.start..span.end], token.lexeme); + assert_eq!(span.file, "unicode.wave"); + assert_eq!( + source[..span.start].bytes().filter(|b| *b == b'\n').count() + 1, + span.line + ); + assert_eq!( + source[..span.start] + .rsplit('\n') + .next() + .unwrap() + .chars() + .count() + + 1, + span.column + ); + } + let string = tokens.iter().find(|t| t.lexeme == "\"a\\n\"").unwrap(); + assert_eq!( + string.span.as_ref().unwrap().end - string.span.as_ref().unwrap().start, + 5 + ); +} + +#[test] +fn semantic_errors_identify_the_failing_occurrence() { + let source = "fun main() { var 이름: i32 = 1; 이름; 이름 = \"bad\"; }"; + let diagnostic = validate_program_detailed(&parse(source)).unwrap_err(); + let span = diagnostic.span.unwrap(); + assert_eq!(&source[span.start..span.end], "\"bad\""); + assert_eq!(span.column, source[..span.start].chars().count() + 1); + let source = "fun main() { var x: i32 = 1; x; missing; missing; }"; + let diagnostic = validate_program_detailed(&parse(source)).unwrap_err(); + assert_eq!( + diagnostic.span.unwrap().start, + source.find("missing").unwrap() + ); +} + +#[test] +fn hir_preserves_all_binary_operand_occurrences_and_default_origins() { + let source = "fun sum(x: i32 = 16) -> i32 { return x + x + x; } fun main() { sum(); }"; + let hir = TypedProgram::lower(monomorphize_generics(parse(source)).unwrap()).unwrap(); + let ASTNode::Function(sum) = &hir.syntax()[0] else { + panic!() + }; + let ASTNode::Statement(StatementNode::Return(Some(expression))) = &sum.body[0] else { + panic!() + }; + fn visit(hir: &TypedProgram, expression: &Expression, spans: &mut Vec) { + let id = hir.expression_id(expression).unwrap(); + let span = hir + .expression_span(id) + .expect("every physical expression has a span"); + if let Expression::Variable(_) = expression { + spans.push(span.start); + } + if let Expression::BinaryExpression { left, right, .. } = expression { + visit(hir, left, spans); + visit(hir, right, spans); + } + } + let mut spans = vec![]; + visit(&hir, expression, &mut spans); + assert_eq!(spans.len(), 3); + assert!(spans.windows(2).all(|w| w[0] < w[1])); + let ASTNode::Function(main) = &hir.syntax()[1] else { + panic!() + }; + let ASTNode::Statement(StatementNode::Expression(Expression::FunctionCall { args, .. })) = + &main.body[0] + else { + panic!() + }; + let span = hir + .expression_span(hir.expression_id(&args[0]).unwrap()) + .unwrap(); + assert_eq!(span.start, source.find("16").unwrap()); +} + +#[test] +fn syntax_errors_point_to_unexpected_token_not_function_start() { + let source = "fun main() { 1 ? 2; }"; + let tokens = Lexer::new_with_file(source, "unicode.wave") + .tokenize() + .unwrap(); + let error = parse_syntax_with_spans(&tokens).unwrap_err(); + assert_eq!(error.span().unwrap().start, source.find('?').unwrap()); +} + +#[test] +fn target_filter_preserves_byte_offsets_including_crlf_and_unicode() { + use parser::import::{preprocess_target_attrs, TargetConditionContext}; + let source = "#[target(arch=\"arm64\")]\r\nvariant 이름 {\r\n Value(i32),\r\n}\r\nfun main() { missing; }\r\n"; + let filtered = preprocess_target_attrs( + source, + &TargetConditionContext { + arch: Some("amd64".into()), + ..Default::default() + }, + ); + assert_eq!(source.len(), filtered.len()); + let diagnostic = validate_program_detailed(&parse(&filtered)).unwrap_err(); + assert_eq!( + diagnostic.span.unwrap().start, + source.find("missing").unwrap() + ); +} + +#[test] +fn generic_instances_and_synthetic_nodes_have_explicit_provenance() { + let source = "fun identity(x: T) -> T { return x; } fun main() { identity(1); identity(2); }"; + let hir = TypedProgram::lower(monomorphize_generics(parse(source)).unwrap()).unwrap(); + let mut ids = Vec::new(); + for node in hir.syntax() { + let ASTNode::Function(f) = node else { continue }; + if !f.name.contains("identity") { + continue; + } + let span = hir.node_span(hir.node_id(node).unwrap()).unwrap(); + assert!(!span.expansion.is_empty()); + let ASTNode::Statement(StatementNode::Return(Some(value))) = &f.body[0] else { + panic!() + }; + let id = hir.expression_id(value).unwrap(); + let span = hir.expression_span(id).unwrap(); + assert_eq!(&source[span.start..span.end], "x"); + assert!(!span.expansion.is_empty()); + ids.push(id); + } + assert_eq!(ids.len(), 2); + assert_ne!(ids[0], ids[1]); + let tokens = Lexer::new("fun main() {}").tokenize().unwrap(); + let synthetic = TypedProgram::lower(parser::parse_syntax_only(&tokens).unwrap()).unwrap(); + assert!(synthetic + .node_span(synthetic.node_id(&synthetic.syntax()[0]).unwrap()) + .is_none()); +} + +#[test] +fn variant_pattern_ids_retain_recursive_source_ranges() { + use parser::ast::MatchPattern; + let source = + "variant V { A(i32), B } fun f(v: V) { match (v) { V::A(x) => { x; }, V::B => {} } }"; + let hir = TypedProgram::lower(monomorphize_generics(parse(source)).unwrap()).unwrap(); + let ASTNode::Function(f) = &hir.syntax()[1] else { + panic!() + }; + let ASTNode::Statement(StatementNode::Match { arms, .. }) = &f.body[0] else { + panic!() + }; + let pattern = &arms[0].pattern; + let span = hir.pattern_span(hir.pattern_id(pattern).unwrap()).unwrap(); + assert_eq!(&source[span.start..span.end], "V::A(x)"); + let MatchPattern::Variant { payloads, .. } = pattern else { + panic!() + }; + let span = hir + .pattern_span(hir.pattern_id(&payloads[0]).unwrap()) + .unwrap(); + assert_eq!(&source[span.start..span.end], "x"); +} diff --git a/llvm/src/codegen/abi_c.rs b/llvm/src/codegen/abi_c.rs index 2358cb87..84b4f542 100644 --- a/llvm/src/codegen/abi_c.rs +++ b/llvm/src/codegen/abi_c.rs @@ -1044,7 +1044,7 @@ pub fn lower_extern_c<'ctx>( .collect(); let wave_ret_layout: Option> = match &ext.return_type { - WaveType::Void => None, + WaveType::Void | WaveType::Never => None, ty => Some(wave_type_to_llvm_type( context, ty, @@ -1158,6 +1158,13 @@ pub fn apply_extern_c_attrs<'ctx>( f: FunctionValue<'ctx>, info: &ExternCInfo<'ctx>, ) { + if info.wave_ret == WaveType::Never { + f.add_attribute( + AttributeLoc::Function, + context.create_enum_attribute(Attribute::get_named_enum_kind_id("noreturn"), 0), + ); + } + let mut llvm_param_index: u32 = 0; if let Some(extension) = info.ret_extension { diff --git a/llvm/src/codegen/consts.rs b/llvm/src/codegen/consts.rs index 40ca77f8..c94d685a 100644 --- a/llvm/src/codegen/consts.rs +++ b/llvm/src/codegen/consts.rs @@ -70,52 +70,11 @@ fn value_type_name<'ctx>(v: BasicValueEnum<'ctx>) -> String { } fn parse_signed_and_radix(s: &str) -> (bool, StringRadix, String) { - let mut t = s.trim().replace('_', ""); - if t.is_empty() { - return (false, StringRadix::Decimal, "".to_string()); - } - - let mut neg = false; - if let Some(rest) = t.strip_prefix('-') { - neg = true; - t = rest.to_string(); - } else if let Some(rest) = t.strip_prefix('+') { - t = rest.to_string(); - } - - let (radix, digits) = if let Some(rest) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) - { - (StringRadix::Hexadecimal, rest) - } else if let Some(rest) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) { - (StringRadix::Binary, rest) - } else if let Some(rest) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) { - (StringRadix::Octal, rest) - } else { - (StringRadix::Decimal, t.as_str()) - }; - - (neg, radix, digits.to_string()) + super::number::parse_integer(s).unwrap_or((false, StringRadix::Decimal, String::new())) } fn is_zero_like(s: &str) -> bool { - let s = s.trim().replace('_', ""); - let s = s.strip_prefix('+').unwrap_or(&s); - let s = s.strip_prefix('-').unwrap_or(s); - - let s = s - .strip_prefix("0x") - .or_else(|| s.strip_prefix("0X")) - .unwrap_or(s); - let s = s - .strip_prefix("0b") - .or_else(|| s.strip_prefix("0B")) - .unwrap_or(s); - let s = s - .strip_prefix("0o") - .or_else(|| s.strip_prefix("0O")) - .unwrap_or(s); - - !s.is_empty() && s.chars().all(|c| c == '0') + lexer::number::IntegerLiteral::parse(s).is_some_and(|n| n.is_zero()) } fn strip_struct_prefix(raw: &str) -> &str { @@ -377,6 +336,12 @@ fn const_from_expected<'ctx>( // --- ints --- Expression::Literal(Literal::Int(s)) => match expected { + BasicTypeEnum::FloatType(float_ty) => { + let value = lexer::number::IntegerLiteral::parse(s) + .and_then(|value| value.to_f64()) + .ok_or_else(|| ConstEvalError::InvalidLiteral(s.clone()))?; + Ok(float_ty.const_float(value).as_basic_value_enum()) + } BasicTypeEnum::IntType(int_ty) => { let (neg, radix, digits) = parse_signed_and_radix(s); let mut iv = int_ty diff --git a/llvm/src/codegen/ir.rs b/llvm/src/codegen/ir.rs index 304e9b02..ae7e7d77 100644 --- a/llvm/src/codegen/ir.rs +++ b/llvm/src/codegen/ir.rs @@ -1014,7 +1014,9 @@ fn build_module( context.i32_type().fn_type(¶m_types, false) } else { match return_type { - None | Some(WaveType::Void) => context.void_type().fn_type(¶m_types, false), + None | Some(WaveType::Void | WaveType::Never) => { + context.void_type().fn_type(¶m_types, false) + } Some(wave_ret_ty) => { let llvm_ret_type = wave_type_to_llvm_type( context, @@ -1040,7 +1042,7 @@ fn build_module( }) .collect::>(); let wave_ret_type = return_type.as_ref().and_then(|return_type| { - if *return_type == WaveType::Void { + if matches!(return_type, WaveType::Void | WaveType::Never) { None } else { Some(wave_type_to_llvm_type( @@ -1073,12 +1075,24 @@ fn build_module( let wrapper = module.add_function(&lowered.llvm_name, lowered.fn_type, None); apply_extern_c_attrs(context, wrapper, &lowered.info); apply_function_codegen_attrs(context, wrapper, disable_red_zone, cpu, features); + if matches!(return_type, Some(WaveType::Never)) { + wrapper.add_attribute( + AttributeLoc::Function, + context.create_enum_attribute(Attribute::get_named_enum_kind_id("noreturn"), 0), + ); + } apply_wasm_export_attr(context, wrapper, abi_target, &lowered.llvm_name); let implementation_name = format!("__wave_export_impl_{}", symbol); let implementation = module.add_function(&implementation_name, fn_type, Some(Linkage::Internal)); apply_function_codegen_attrs(context, implementation, disable_red_zone, cpu, features); + if matches!(return_type, Some(WaveType::Never)) { + implementation.add_attribute( + AttributeLoc::Function, + context.create_enum_attribute(Attribute::get_named_enum_kind_id("noreturn"), 0), + ); + } functions.insert(symbol.clone(), implementation); extern_c_info.insert(symbol.clone(), lowered.info.clone()); @@ -1092,6 +1106,12 @@ fn build_module( } else { let function = module.add_function(symbol, fn_type, None); apply_function_codegen_attrs(context, function, disable_red_zone, cpu, features); + if matches!(return_type, Some(WaveType::Never)) { + function.add_attribute( + AttributeLoc::Function, + context.create_enum_attribute(Attribute::get_named_enum_kind_id("noreturn"), 0), + ); + } functions.insert(symbol.clone(), function); } } @@ -1197,6 +1217,8 @@ fn build_module( if implicit_i32_main { let zero = context.i32_type().const_zero(); builder.build_return(Some(&zero)).unwrap(); + } else if func_node.return_type == Some(WaveType::Never) { + builder.build_unreachable().unwrap(); } else if is_void_like { builder.build_return(None).unwrap(); } else { @@ -1246,34 +1268,7 @@ fn pipeline_from_opt_flag(opt_flag: &str) -> &'static str { } fn parse_int_literal(raw: &str) -> Option { - let mut s = raw.trim().replace('_', ""); - if s.is_empty() { - return None; - } - - let neg = if let Some(rest) = s.strip_prefix('-') { - s = rest.to_string(); - true - } else if let Some(rest) = s.strip_prefix('+') { - s = rest.to_string(); - false - } else { - false - }; - - let (radix, digits) = if let Some(rest) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) - { - (16, rest) - } else if let Some(rest) = s.strip_prefix("0b").or_else(|| s.strip_prefix("0B")) { - (2, rest) - } else if let Some(rest) = s.strip_prefix("0o").or_else(|| s.strip_prefix("0O")) { - (8, rest) - } else { - (10, s.as_str()) - }; - - let v = i128::from_str_radix(digits, radix).ok()?; - Some(if neg { -v } else { v }) + lexer::number::IntegerLiteral::parse(raw)?.to_i128() } fn repr_bits_signed(ty: &WaveType) -> Option<(u32, bool)> { diff --git a/llvm/src/codegen/mod.rs b/llvm/src/codegen/mod.rs index 9b55c20d..982bdec2 100644 --- a/llvm/src/codegen/mod.rs +++ b/llvm/src/codegen/mod.rs @@ -34,3 +34,5 @@ pub use ir::{emit_codegen_file, generate_ir, CodegenFileKind}; pub use types::{wave_type_to_llvm_type, VariableInfo}; pub use legacy::{create_alloc, get_llvm_type}; + +pub(crate) mod number; diff --git a/llvm/src/codegen/number.rs b/llvm/src/codegen/number.rs new file mode 100644 index 00000000..e634238e --- /dev/null +++ b/llvm/src/codegen/number.rs @@ -0,0 +1,13 @@ +//! Conversion of validated frontend numbers to LLVM radix constants. +use inkwell::types::StringRadix; + +pub(crate) fn parse_integer(raw: &str) -> Option<(bool, StringRadix, String)> { + let value = lexer::number::IntegerLiteral::parse(raw)?; + let radix = match value.radix { + 2 => StringRadix::Binary, + 8 => StringRadix::Octal, + 16 => StringRadix::Hexadecimal, + _ => StringRadix::Decimal, + }; + Some((value.negative, radix, value.digits)) +} diff --git a/llvm/src/codegen/types.rs b/llvm/src/codegen/types.rs index 0d731812..c24b58ed 100644 --- a/llvm/src/codegen/types.rs +++ b/llvm/src/codegen/types.rs @@ -56,6 +56,9 @@ pub fn wave_type_to_llvm_type<'ctx>( flavor: TypeFlavor, ) -> BasicTypeEnum<'ctx> { match wave_type { + WaveType::Isz | WaveType::Usz | WaveType::Never => { + unreachable!("typed HIR must resolve target-sized integers") + } WaveType::Int(bits) | WaveType::Uint(bits) => context .custom_width_int_type(*bits as u32) .as_basic_type_enum(), diff --git a/llvm/src/codegen/variants.rs b/llvm/src/codegen/variants.rs index 7aafda16..44a5f48e 100644 --- a/llvm/src/codegen/variants.rs +++ b/llvm/src/codegen/variants.rs @@ -238,6 +238,8 @@ fn split_variant_application(name: &str) -> Option<(String, Vec)> { fn display_wave_type(ty: &WaveType) -> String { match ty { + WaveType::Isz => "isz".to_string(), + WaveType::Usz => "usz".to_string(), WaveType::Int(bits) => format!("i{bits}"), WaveType::Uint(bits) => format!("u{bits}"), WaveType::Float(bits) => format!("f{bits}"), @@ -248,6 +250,7 @@ fn display_wave_type(ty: &WaveType) -> String { WaveType::Pointer(inner) => format!("ptr<{}>", display_wave_type(inner)), WaveType::Array(inner, length) => format!("array<{},{}>", display_wave_type(inner), length), WaveType::Void => "void".to_string(), + WaveType::Never => "!".to_string(), WaveType::Struct(name) | WaveType::Variant(name) => name.clone(), } } @@ -265,6 +268,7 @@ fn collect_type_variants(ty: &WaveType, names: &mut BTreeSet) { fn collect_node_variant_types(nodes: &[ASTNode], names: &mut BTreeSet) { for node in nodes { match node { + ASTNode::Located { .. } => unreachable!("typed HIR detaches source wrappers"), ASTNode::Function(function) => { for parameter in &function.parameters { collect_type_variants(¶meter.param_type, names); diff --git a/llvm/src/expression/rvalue/calls.rs b/llvm/src/expression/rvalue/calls.rs index cbde1192..46f3ebce 100644 --- a/llvm/src/expression/rvalue/calls.rs +++ b/llvm/src/expression/rvalue/calls.rs @@ -544,7 +544,7 @@ pub(crate) fn gen_function_call<'ctx, 'a>( // 3) return match &info.ret { RetLowering::Void => { - if info.wave_ret != WaveType::Void { + if !matches!(info.wave_ret, WaveType::Void | WaveType::Never) { return expected_type.map_or_else( || env.context.i32_type().const_zero().as_basic_value_enum(), BasicTypeEnum::const_zero, diff --git a/llvm/src/expression/rvalue/dispatch.rs b/llvm/src/expression/rvalue/dispatch.rs index 8a248b45..ec14a5bf 100644 --- a/llvm/src/expression/rvalue/dispatch.rs +++ b/llvm/src/expression/rvalue/dispatch.rs @@ -27,6 +27,7 @@ pub(crate) fn gen_expr<'ctx, 'a>( expected_type: Option>, ) -> BasicValueEnum<'ctx> { match expr { + Expression::Located { .. } => unreachable!("typed HIR detaches source wrappers"), Expression::Literal(lit) => literals::gen(env, lit, expected_type), Expression::Null => literals::gen_null(env, expected_type), Expression::Variable(name) => { diff --git a/llvm/src/expression/rvalue/literals.rs b/llvm/src/expression/rvalue/literals.rs index 926ea6b2..c724a32b 100644 --- a/llvm/src/expression/rvalue/literals.rs +++ b/llvm/src/expression/rvalue/literals.rs @@ -17,55 +17,17 @@ //! does not invent a pointee type for an untyped null literal. use super::ExprGenEnv; -use inkwell::types::{BasicTypeEnum, StringRadix}; +use inkwell::types::BasicTypeEnum; use inkwell::values::{BasicValue, BasicValueEnum}; use inkwell::AddressSpace; use parser::ast::Literal; -fn parse_signed_decimal<'a>(s: &'a str) -> (bool, &'a str) { - if let Some(rest) = s.strip_prefix('-') { - (true, rest) - } else { - (false, s) - } -} - -fn parse_int_radix(s: &str) -> (StringRadix, &str) { - if let Some(rest) = s.strip_prefix("0b").or_else(|| s.strip_prefix("0B")) { - (StringRadix::Binary, rest) - } else if let Some(rest) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) { - (StringRadix::Hexadecimal, rest) - } else if let Some(rest) = s.strip_prefix("0o").or_else(|| s.strip_prefix("0O")) { - (StringRadix::Octal, rest) - } else { - (StringRadix::Decimal, s) - } -} - fn parse_int_as_f64(s: &str) -> Option { - let normalized = s.trim().replace('_', ""); - let (negative, raw) = parse_signed_decimal(&normalized); - let (radix, digits) = - if let Some(rest) = raw.strip_prefix("0b").or_else(|| raw.strip_prefix("0B")) { - (2, rest) - } else if let Some(rest) = raw.strip_prefix("0x").or_else(|| raw.strip_prefix("0X")) { - (16, rest) - } else if let Some(rest) = raw.strip_prefix("0o").or_else(|| raw.strip_prefix("0O")) { - (8, rest) - } else { - (10, raw) - }; - let magnitude = u128::from_str_radix(digits, radix).ok()? as f64; - Some(if negative { -magnitude } else { magnitude }) + lexer::number::IntegerLiteral::parse(s)?.to_f64() } fn is_zero_int_literal(s: &str) -> bool { - let s = s.trim(); - let s = s.strip_prefix('+').unwrap_or(s); - let (_neg, raw) = parse_signed_decimal(s); - let (_radix, digits) = parse_int_radix(raw); - - !digits.is_empty() && digits.chars().all(|c| c == '0') + lexer::number::IntegerLiteral::parse(s).is_some_and(|n| n.is_zero()) } pub(crate) fn gen_null<'ctx, 'a>( @@ -95,11 +57,11 @@ pub(crate) fn gen<'ctx, 'a>( Literal::Int(v) => match expected_type { Some(BasicTypeEnum::IntType(int_ty)) => { let s = v.as_str(); - let (neg, raw) = parse_signed_decimal(s); - let (radix, digits) = parse_int_radix(raw); + let (neg, radix, digits) = + crate::codegen::number::parse_integer(s).expect("validated integer literal"); let mut iv = int_ty - .const_int_from_string(digits, radix) + .const_int_from_string(&digits, radix) .unwrap_or_else(|| panic!("invalid int literal: {}", s)); if neg { @@ -134,11 +96,11 @@ pub(crate) fn gen<'ctx, 'a>( // Default untyped integer literal to i32 when no contextual type exists. let int_ty = env.context.i32_type(); let s = v.as_str(); - let (neg, raw) = parse_signed_decimal(s); - let (radix, digits) = parse_int_radix(raw); + let (neg, radix, digits) = + crate::codegen::number::parse_integer(s).expect("validated integer literal"); let mut iv = int_ty - .const_int_from_string(digits, radix) + .const_int_from_string(&digits, radix) .unwrap_or_else(|| panic!("invalid int literal: {}", s)); if neg { diff --git a/llvm/src/statement/control.rs b/llvm/src/statement/control.rs index ff295ec3..d697b3d6 100644 --- a/llvm/src/statement/control.rs +++ b/llvm/src/statement/control.rs @@ -24,7 +24,6 @@ use crate::statement::variable::{coerce_basic_value, expression_is_unsigned, Coe use inkwell::basic_block::BasicBlock; use inkwell::module::Module; use inkwell::targets::TargetData; -use inkwell::types::StringRadix; use inkwell::types::{BasicType, StructType}; use inkwell::values::{AnyValue, BasicValueEnum, FunctionValue, IntValue, PointerValue}; use inkwell::{FloatPredicate, IntPredicate}; @@ -103,39 +102,20 @@ fn node_breaks_current_loop(node: &ASTNode) -> bool { } } -fn parse_signed_decimal<'a>(s: &'a str) -> (bool, &'a str) { - if let Some(rest) = s.strip_prefix('-') { - (true, rest) - } else { - (false, s) - } -} - -fn parse_int_radix(s: &str) -> (StringRadix, &str) { - if let Some(rest) = s.strip_prefix("0b").or_else(|| s.strip_prefix("0B")) { - (StringRadix::Binary, rest) - } else if let Some(rest) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) { - (StringRadix::Hexadecimal, rest) - } else if let Some(rest) = s.strip_prefix("0o").or_else(|| s.strip_prefix("0O")) { - (StringRadix::Octal, rest) - } else { - (StringRadix::Decimal, s) - } -} - fn eval_match_case_const<'ctx>( discr_ty: inkwell::types::IntType<'ctx>, pattern: &MatchPattern, global_consts: &HashMap>, ) -> inkwell::values::IntValue<'ctx> { match pattern { + MatchPattern::Located { .. } => unreachable!("typed HIR detaches source wrappers"), MatchPattern::Int(raw) => { let text = raw.as_str(); - let (neg, digits_src) = parse_signed_decimal(text); - let (radix, digits) = parse_int_radix(digits_src); + let (neg, radix, digits) = + crate::codegen::number::parse_integer(text).expect("validated match integer"); let mut iv = discr_ty - .const_int_from_string(digits, radix) + .const_int_from_string(&digits, radix) .unwrap_or_else(|| panic!("invalid integer literal in match case: {}", raw)); if neg { iv = iv.const_neg(); @@ -194,6 +174,7 @@ fn gen_variant_pattern_test<'ctx>( struct_types: &HashMap>, ) -> (IntValue<'ctx>, Vec>) { match pattern { + MatchPattern::Located { .. } => unreachable!("typed HIR detaches source wrappers"), MatchPattern::Wildcard => (context.bool_type().const_int(1, false), Vec::new()), MatchPattern::Binding(name) => ( context.bool_type().const_int(1, false), @@ -789,6 +770,7 @@ pub(super) fn gen_match_ir<'ctx>( for (idx, arm) in arms.iter().enumerate() { match &arm.pattern { + MatchPattern::Located { .. } => unreachable!("typed HIR detaches source wrappers"), MatchPattern::Wildcard => { if default_arm.is_some() { panic!("duplicate wildcard match arm (`_`)"); diff --git a/llvm/src/statement/mod.rs b/llvm/src/statement/mod.rs index 1c330cce..119394bc 100644 --- a/llvm/src/statement/mod.rs +++ b/llvm/src/statement/mod.rs @@ -260,6 +260,17 @@ pub fn generate_statement_ir<'ctx>( extern_c_info, program, ); + if matches!( + program.type_of(expr), + Some(parser::hir::HirExpressionType::Resolved( + parser::ast::WaveType::Never + )) + ) && builder + .get_insert_block() + .is_some_and(|block| block.get_terminator().is_none()) + { + builder.build_unreachable().unwrap(); + } } ASTNode::Statement(StatementNode::Assign { variable, value }) => { diff --git a/spec/README.md b/spec/README.md new file mode 100644 index 00000000..e20e733a --- /dev/null +++ b/spec/README.md @@ -0,0 +1,151 @@ +# Wave Alpha language contract, revision 0 + +[alpha-0.ebnf](alpha-0.ebnf) defines the source grammar implemented by this +frontend. [tokens.tsv](tokens.tsv) inventories **every** `TokenType` variant, +including internal compatibility variants, reserved syntax and removed syntax. +This is a language contract revision, not a compiler release announcement. +Changes to accepted syntax must update the grammar, inventory and conformance +fixtures together. Correctness fixes to existing productions do not require a +new language version. + +## Reading the grammar + +Quotes denote source terminals; commas concatenate; `|` chooses; brackets are +optional; braces repeat zero or more times. `? ... ?` is a lexical condition +specified here. Longest-token matching applies before parsing. For example, +`--x` is decrement, while `- -x` is two unary negations. A token cannot be split +by whitespace or comments. Types such as `i64` are single tokens, despite their +spelling being factored in `integer-type`. Generic closing chevrons are split +contextually from `>>` by the generic parser. + +Whitespace, CRLF/LF newlines, `//` comments and `/* ... */` comments separate +tokens and do not terminate statements. Block comments do not nest. All local +variables, expression statements, returns, breaks, continues and I/O statements +require `;`, even immediately before `}`. Block constructs do not require `;`; +standalone `asm` accepts one optional trailing `;`. Empty statements are not +part of this grammar. A failed statement never permits the parser to skip its +remaining tokens and resume silently. + +Assignment is right associative. Other binary operators and repeated casts +associate left to right. Precedence from low to high is assignment, `||`, `&&`, +`|`, `^`, binary `&`, equality, comparison, shift, addition, multiplication, +`as`, prefix operators, postfix operators and primary expressions. Address-of +uses the same `&` token as bitwise AND. `deref` is the dereference operator; +prefix `*` is not supported. Conditions reject assignment and mutation in the +semantic pass, even when their expression syntax is otherwise valid. + +An unparenthesized match subject ends at the first `{`; parenthesize subjects +containing a record literal. Match arm bodies are blocks. Integer and enum +patterns match scalar subjects. Variant patterns may recursively bind payloads; +a bare name in a payload position binds a variable, while a qualified name +selects a case. `_` is a wildcard. Arms permit a single optional `,` or `;` +separator. Duplicate arms, duplicate bindings and nonexhaustive variant matches +are semantic errors. + +## Names and declarations + +Identifiers start with a Unicode Alphabetic scalar or `_` and continue with +Unicode Alphabetic or Numeric scalars or `_` (Rust's Unicode predicates). +No normalization is performed: different scalar sequences are different names. +Combining marks outside these predicates are not identifier characters. +Keywords in the inventory cannot be identifiers. `bool`, `void`, `ptr` and +`array` are contextual type spellings represented by identifier tokens. +Reserved syntax (`module`, `class`, `is`, `xnand`, `~^`, `!&`, `!|`, `?`, `??`, +`?:`) has no accepted production. `let` and `mut` declarations were removed; +use `var`. Ranges, for-in loops, propagation operators, unsafe blocks, function +pointer types, slices, destructuring, expression-valued if/match and match +guards are not added by this revision. + +`pub` controls module visibility and is separate from the C ABI export +attribute. `main` must remain private and nongeneric. Imports use +`import("path" as alias);` or `import("path")::{name, other};`; aliases and +selections cannot be combined. A public import requires explicit selections. +`extern(c)` and `extern(system)` declare foreign functions; `export(c)` and +`export(system)` define foreign entry points. These headers accept an optional +string symbol. Extern parameters may omit their names, and a variadic marker +must be last. Exported functions cannot be generic. + +The `#[target(...)]` attribute occupies its own source line and applies to one +following declaration. Keys are `arch`, `os`, `env`, and `abi`, without repeated +keys. The same filtering applies to imported sources and to variant declarations. +Inactive declarations and attributes are replaced by spaces that preserve byte +lengths and newline positions. Stacked target attributes and attributes inside +bodies are outside this contract. + +Required parameters precede default parameters. Defaults are literal values +(including signed numbers and null), not arbitrary constant expressions. Their +values are checked against the declared type even if the function is never +called. Parameter, payload and enum lists allow a trailing comma; calls, array +literals and explicit type-argument lists do not. Generic parameter lists must +be nonempty and contain unique names. `void` and `!` are restricted to return positions. `!` declares a function that +cannot return: it must end in a provably endless loop or a call to another +never-returning function (or an asm block declaring `clobber("noreturn")`). Explicit return statements are rejected in such functions. +A never-returning call is allowed as a statement and terminates that control-flow +path; this revision does not introduce bottom-type coercions in value expressions. +`const` and `static` belong at top level. Field names, bindings, signatures, +return coverage, visibility and ABI compatibility have additional semantic +checks; syntactic acceptance is not a promise that a program is well typed. + +## Numbers and text + +Integers accept decimal, binary (`0b`/`0B`), octal (`0o`/`0O`) and hexadecimal +(`0x`/`0X`). Every prefix requires digits. Exactly one underscore may appear +between digits of the same numeric component. Decimal floats require a +fractional part with digits on both sides of the point or a decimal exponent: +`1.0`, `1e3`, `1_000.5e-2`. Exponent signs are allowed; number suffixes and +hexadecimal floats are not. Thus `1.`, `.5`, `1__2`, `0x_1`, `0b102`, `1e+` and +`1u32` are rejected. Invalid adjacent digit/identifier text is diagnosed as one +malformed number, not separate valid tokens. A decimal point directly after a +number belongs to that numeric token; use parentheses for postfix access on a +numeric primary. + +Integer spelling and sign are preserved until a target type is known. Supported +signed/unsigned widths are 8, 16, 32, 64, 128, 256, 512 and 1024. Decimal signed +literals must fit the signed range; nondecimal positive literals may spell the +full-width bit pattern. Negative signed minimum values are supported. Unsigned +initializers cannot be negative. Explicit casts retain the existing conversion +rules. Array lengths accept integer literals fitting `u32`. `isz` and `usz` +remain symbolic in the AST, then resolve to the selected target's pointer +width **before** semantic analysis and generic specialization: wasm32 uses 32; +the supported native 64-bit targets and wasm64 use 64. Unsupported numeric +width spellings are errors, not user-defined types. + +Floating literals are converted once to finite IEEE binary64 values; a literal +with an expected f32 type must also fit finite binary32. Overflow is diagnosed; +underflow may round to zero. Integer-to-float constant conversion accumulates +exact integer digits before one rounded conversion. The shared implementation +is `front/lexer/src/number.rs`; semantic checking and LLVM constants use it too. + +Strings contain Unicode scalar values and use UTF-8 when emitted. Supported +string escapes are `\\`, `\"`, `\n`, `\t`, `\r`, and `\xNN`; the last denotes +the scalar U+00NN. Character literals contain exactly one value in 0..255, +matching Wave's 8-bit `char`; their escapes additionally include `\'`. +`\0` is not an escape: use `\x00`. Physical newlines and unknown or incomplete +escapes are rejected. Escaped strings retain their original source spelling +and byte span separately from the decoded value. + +## Source provenance and conformance + +Locations use half-open UTF-8 byte ranges and one-based line/column coordinates; +columns count Unicode scalars, not terminal cells or UTF-8 bytes. Parser-selected +name spans focus declaration diagnostics. Imports retain their original file +paths. Generic instances and inserted defaults retain definition locations and +record their expansion reason. Synthetic nodes without a physical origin have +no span, represented as null, rather than an invented location on line 1. + +`parse_syntax_with_spans` is the compiler's source-preserving entry point. +`parse_syntax_only` is a compatibility API for consumers that explicitly discard +locations. Typed HIR exposes stable declaration/statement, expression and pattern +IDs with source-span accessors. The diagnostic renderer uses these spans directly; +it does not search for matching text in the source again. + +[fixtures.tsv](fixtures.tsv) maps production families to positive and negative +source files in [fixtures](fixtures/). `grammar_contract.rs` executes both sides +and verifies that every token kind has exactly one inventory row referring to a +real production. Lexical examples must produce the documented token kind. +`numeric_contract.rs`, `alpha_frontend.rs` and `source_spans.rs` add detailed +numeric, block, target-filter and provenance regressions. Linux CI runs the +workspace tests, so frontend conformance runs independently of LLVM test filters. + +Run locally with `cargo test --locked -p lexer -p parser --jobs 2`; run compiler +and backend integration with `cargo test --locked --workspace --all-targets --jobs 2`. diff --git a/spec/alpha-0.ebnf b/spec/alpha-0.ebnf new file mode 100644 index 00000000..e07a8d68 --- /dev/null +++ b/spec/alpha-0.ebnf @@ -0,0 +1,89 @@ +(* Wave Alpha grammar contract, revision 0. See README.md for lexical and semantic constraints. *) +program = { [ target-attribute ], declaration } ; +target-attribute = "#[target(", target-key, "=", string, { ",", target-key, "=", string }, ")]" ; +target-key = "arch" | "os" | "env" | "abi" ; +declaration = [ "pub" ], ( function | structure | enumeration | variant | alias | global | import | export ) | extern | proto ; +function = "fun", identifier, [ generic-parameters ], "(", [ parameters ], ")", [ "->", type ], block ; +generic-parameters = "<", identifier, { ",", identifier }, [ "," ], ">" ; +parameters = parameter, { ",", parameter }, [ "," ] ; +parameter = identifier, ":", type, [ "=", default ] ; +default = { "+" | "-" }, number | string | character | "true" | "false" | "null" ; +structure = "struct", identifier, [ generic-parameters ], "{", { field | function }, "}" ; +field = identifier, ":", type, ";" ; +proto = "proto", identifier, "{", { function }, "}" ; +enumeration = "enum", identifier, "->", type, "{", [ enum-cases ], "}" ; +enum-cases = enum-case, { ",", enum-case }, [ "," ] ; +enum-case = identifier, [ "=", { "+" | "-" }, integer ] ; +variant = "variant", identifier, [ generic-parameters ], "{", [ variant-cases ], "}" ; +variant-cases = variant-case, { ",", variant-case }, [ "," ] ; +variant-case = identifier, [ "(", [ type, { ",", type }, [ "," ] ], ")" ] ; +alias = "type", identifier, "=", type, ";" ; +global = ( "const" | "static" ), identifier, ":", type, [ "=", expression ], ";" ; +import = "import", "(", string, [ "as", identifier ], ")", [ "::", "{", identifier, { ",", identifier }, [ "," ], "}" ], ";" ; +ffi-header = "(", identifier, [ ",", string ], ")" ; +extern = "extern", ffi-header, ( extern-function | "{", { extern-function }, "}", [ ";" ] ) ; +extern-function = "fun", identifier, "(", [ extern-parameters ], ")", [ "->", type ], ";" ; +extern-parameters = extern-parameter, { ",", extern-parameter }, [ ",", ".", ".", "." | "," ] | ".", ".", "." ; +extern-parameter = [ identifier, ":" ], type ; +export = "export", ffi-header, ( function | "{", { function }, "}", [ ";" ] ) ; +type = integer-type | float-type | "bool" | "char" | "byte" | "str" | "void" | "!" | "ptr", "<", type, ">" | "array", "<", type, ",", integer, ">" | qualified-name, [ type-arguments ] ; +integer-type = "isz" | "usz" | ( "i" | "u" ), ( "8" | "16" | "32" | "64" | "128" | "256" | "512" | "1024" ) ; +float-type = "f32" | "f64" ; +type-arguments = "<", type, { ",", type }, ">" ; +block = "{", { statement }, "}" ; +statement = local | if | while | for | match | io | "return", [ expression ], ";" | ( "break" | "continue" ), ";" | asm, [ ";" ] | expression, ";" ; +local = "var", identifier, ":", type, [ "=", expression ], ";" ; +if = "if", "(", expression, ")", block, { "else", "if", "(", expression, ")", block }, [ "else", block ] ; +while = "while", "(", expression, ")", block ; +for = "for", "(", for-initializer, ";", expression, ";", expression, ")", block ; +for-initializer = [ "var" ], identifier, ":", type, [ "=", expression ] | expression ; +match = "match", ( "(", expression, ")" | expression ), "{", { match-arm, [ "," | ";" ] }, "}" ; +match-arm = pattern, "=", ">", block ; +pattern = [ "-" ], integer | qualified-name, [ "(", [ pattern, { ",", pattern } ], ")" ] | "_" ; +io = ( "print" | "println" | "input" ), "(", string, { ",", expression }, ")", ";" ; +expression = logical-or, [ assignment-operator, expression ] ; +assignment-operator = "=" | "+=" | "-=" | "*=" | "/=" | "%=" ; +logical-or = logical-and, { "||", logical-and } ; +logical-and = bitwise-or, { "&&", bitwise-or } ; +bitwise-or = bitwise-xor, { "|", bitwise-xor } ; +bitwise-xor = bitwise-and, { "^", bitwise-and } ; +bitwise-and = equality, { "&", equality } ; +equality = relational, { ( "==" | "!=" ), relational } ; +relational = shift, { ( "<" | "<=" | ">" | ">=" ), shift } ; +shift = additive, { ( "<<" | ">>" ), additive } ; +additive = multiplicative, { ( "+" | "-" ), multiplicative } ; +multiplicative = cast, { ( "*" | "/" | "%" ), cast } ; +cast = unary, { "as", type } ; +unary = ( "+" | "-" | "!" | "~" | "&" | "deref" | "++" | "--" ), unary | postfix ; +postfix = primary, { ".", identifier, [ arguments ] | "[", expression, "]" }, [ "++" | "--" ] ; +primary = literal | "null" | "(", expression, ")" | "[", [ expression, { ",", expression } ], "]" | asm | qualified-name, [ type-arguments ], ( arguments | record-fields ) | qualified-name ; +arguments = "(", [ expression, { ",", expression } ], ")" ; +record-fields = "{", [ identifier, ":", expression, { ",", identifier, ":", expression }, [ "," ] ], "}" ; +asm = "asm", "{", { string | asm-operand | clobber | "," | ";" }, "}" ; +asm-operand = ( "in" | "out" ), "(", ( string | identifier ), ")", expression ; +clobber = "clobber", "(", [ ( string | identifier ), { ",", ( string | identifier ) } ], ")" ; +literal = number | string | character | "true" | "false" ; +number = integer | float ; +integer = decimal-integer | ( "0b" | "0B" ), binary-digits | ( "0o" | "0O" ), octal-digits | ( "0x" | "0X" ), hex-digits ; +decimal-integer = decimal-digit, { [ "_" ], decimal-digit } ; +binary-digits = binary-digit, { [ "_" ], binary-digit } ; +octal-digits = octal-digit, { [ "_" ], octal-digit } ; +hex-digits = hex-digit, { [ "_" ], hex-digit } ; +float = decimal-integer, ( ".", decimal-integer, [ exponent ] | exponent ) ; +exponent = ( "e" | "E" ), [ "+" | "-" ], decimal-integer ; +qualified-name = identifier, { "::", identifier } ; +identifier = identifier-start, { identifier-continue } ; +identifier-start = ? Unicode Alphabetic scalar or underscore ? ; +identifier-continue = ? Unicode Alphabetic or Numeric scalar or underscore ? ; +decimal-digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ; +binary-digit = "0" | "1" ; +octal-digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" ; +hex-digit = decimal-digit | "a" | "b" | "c" | "d" | "e" | "f" | "A" | "B" | "C" | "D" | "E" | "F" ; +string = '"', { string-character | escape }, '"' ; +character = "'", ( character-scalar | escape ), "'" ; +escape = ? Backslash followed by n, t, r, backslash, matching quote or x and exactly two hex digits, as specified in README.md ? ; +string-character = ? Unicode scalar except newline, backslash and double quote ? ; +character-scalar = ? Exactly one accepted character scalar; see README.md for byte restriction ? ; +reserved = "module" | "class" | "is" | "xnand" | "~^" | "!&" | "!|" | "?" | "??" | "?:" ; +removed = "let" | "mut" ; +internal = ? Token variants retained for parser/API compatibility; no standalone source spelling ? ; diff --git a/spec/fixtures.tsv b/spec/fixtures.tsv new file mode 100644 index 00000000..f4f4208d --- /dev/null +++ b/spec/fixtures.tsv @@ -0,0 +1,13 @@ +# name productions +functions function,parameter,default,generic-parameters +records structure,field,proto +enums enumeration,enum-case +variants variant,variant-case +aliases alias,global,type +imports import,declaration +ffi extern,export,ffi-header,extern-function +control block,statement,local,if,while,for +matching match,match-arm,pattern +expressions expression,assignment-operator,postfix,primary +io_asm io,asm,asm-operand,clobber +numbers number,integer,float,identifier,string,character diff --git a/spec/fixtures/aliases.accept.wave b/spec/fixtures/aliases.accept.wave new file mode 100644 index 00000000..46fba394 --- /dev/null +++ b/spec/fixtures/aliases.accept.wave @@ -0,0 +1 @@ +type Count = usz; const LIMIT: i128 = 0o20; static ptr_value: ptr = null; diff --git a/spec/fixtures/aliases.reject.wave b/spec/fixtures/aliases.reject.wave new file mode 100644 index 00000000..7b347225 --- /dev/null +++ b/spec/fixtures/aliases.reject.wave @@ -0,0 +1 @@ +type Bad = i24; diff --git a/spec/fixtures/control.accept.wave b/spec/fixtures/control.accept.wave new file mode 100644 index 00000000..fa502ee6 --- /dev/null +++ b/spec/fixtures/control.accept.wave @@ -0,0 +1 @@ +fun f() { var i: i32 = 0; if (true) { true; } else if (false) { !false; } else { +1; } while (i < 3) { i++; continue; } for (i: i32 = 0; i < 4; i++) { break; } return; } diff --git a/spec/fixtures/control.reject.wave b/spec/fixtures/control.reject.wave new file mode 100644 index 00000000..db891109 --- /dev/null +++ b/spec/fixtures/control.reject.wave @@ -0,0 +1 @@ +fun f() { while (true) { break } } diff --git a/spec/fixtures/enums.accept.wave b/spec/fixtures/enums.accept.wave new file mode 100644 index 00000000..f4401262 --- /dev/null +++ b/spec/fixtures/enums.accept.wave @@ -0,0 +1 @@ +enum Code -> i32 { Zero, Negative = -1, Hex = 0x10, } diff --git a/spec/fixtures/enums.reject.wave b/spec/fixtures/enums.reject.wave new file mode 100644 index 00000000..bb421d91 --- /dev/null +++ b/spec/fixtures/enums.reject.wave @@ -0,0 +1 @@ +enum Code -> i32 { Bad = "text" } diff --git a/spec/fixtures/expressions.accept.wave b/spec/fixtures/expressions.accept.wave new file mode 100644 index 00000000..0043d3c7 --- /dev/null +++ b/spec/fixtures/expressions.accept.wave @@ -0,0 +1 @@ +fun f() { a = b = 1 + 2 * 3; a += 1; a.b[0]++; deref p; &a; ~a; a << 2; a >> 1; a & b ^ c | d; a < b && b != c || a == c; (1 as i64); [1, 2]; Box { value: 1 }; } diff --git a/spec/fixtures/expressions.reject.wave b/spec/fixtures/expressions.reject.wave new file mode 100644 index 00000000..e81118ff --- /dev/null +++ b/spec/fixtures/expressions.reject.wave @@ -0,0 +1 @@ +fun f() { a() b(); } diff --git a/spec/fixtures/ffi.accept.wave b/spec/fixtures/ffi.accept.wave new file mode 100644 index 00000000..c0c79d09 --- /dev/null +++ b/spec/fixtures/ffi.accept.wave @@ -0,0 +1 @@ +extern(c) { fun puts(str) -> i32; fun log(fmt: str, ...); } export(c, "entry") fun entry() {} diff --git a/spec/fixtures/ffi.reject.wave b/spec/fixtures/ffi.reject.wave new file mode 100644 index 00000000..49f4efd9 --- /dev/null +++ b/spec/fixtures/ffi.reject.wave @@ -0,0 +1 @@ +extern(c) fun bad(x i32); diff --git a/spec/fixtures/functions.accept.wave b/spec/fixtures/functions.accept.wave new file mode 100644 index 00000000..3a4541a1 --- /dev/null +++ b/spec/fixtures/functions.accept.wave @@ -0,0 +1,2 @@ +fun id(x: T) -> T { return x; } +fun value(x: i32 = 0x10) -> i32 { return x; } diff --git a/spec/fixtures/functions.reject.wave b/spec/fixtures/functions.reject.wave new file mode 100644 index 00000000..4943b48e --- /dev/null +++ b/spec/fixtures/functions.reject.wave @@ -0,0 +1 @@ +fun bad(x: i32 = 0x) {} diff --git a/spec/fixtures/imports.accept.wave b/spec/fixtures/imports.accept.wave new file mode 100644 index 00000000..29c8ba38 --- /dev/null +++ b/spec/fixtures/imports.accept.wave @@ -0,0 +1 @@ +import("std::bytes" as bytes); pub import("module")::{Thing, value}; diff --git a/spec/fixtures/imports.reject.wave b/spec/fixtures/imports.reject.wave new file mode 100644 index 00000000..e876fab5 --- /dev/null +++ b/spec/fixtures/imports.reject.wave @@ -0,0 +1 @@ +import("module")::{}; diff --git a/spec/fixtures/io_asm.accept.wave b/spec/fixtures/io_asm.accept.wave new file mode 100644 index 00000000..1ea1ea00 --- /dev/null +++ b/spec/fixtures/io_asm.accept.wave @@ -0,0 +1 @@ +fun f() { print("{}", 1); println("ok"); input("{}", &x); asm { "nop"; in("r") (&x as i64); out("r") x; clobber("memory"); } } diff --git a/spec/fixtures/io_asm.reject.wave b/spec/fixtures/io_asm.reject.wave new file mode 100644 index 00000000..a190c1de --- /dev/null +++ b/spec/fixtures/io_asm.reject.wave @@ -0,0 +1 @@ +fun f() { asm { "nop" ? } } diff --git a/spec/fixtures/matching.accept.wave b/spec/fixtures/matching.accept.wave new file mode 100644 index 00000000..23312cc2 --- /dev/null +++ b/spec/fixtures/matching.accept.wave @@ -0,0 +1 @@ +fun f() { match (value) { Option::Some(item) => { item; }, Option::None => { return; } } } diff --git a/spec/fixtures/matching.reject.wave b/spec/fixtures/matching.reject.wave new file mode 100644 index 00000000..f9a2c7cc --- /dev/null +++ b/spec/fixtures/matching.reject.wave @@ -0,0 +1 @@ +fun f() { match (value) { _ => 1; } } diff --git a/spec/fixtures/numbers.accept.wave b/spec/fixtures/numbers.accept.wave new file mode 100644 index 00000000..5266b0ee --- /dev/null +++ b/spec/fixtures/numbers.accept.wave @@ -0,0 +1 @@ +fun 숫자(x: f64 = 1_000.5e-2) { 0b10; 0o7; 0xFF; 1_000; "str\n"; 'a'; } diff --git a/spec/fixtures/numbers.reject.wave b/spec/fixtures/numbers.reject.wave new file mode 100644 index 00000000..3bd1c502 --- /dev/null +++ b/spec/fixtures/numbers.reject.wave @@ -0,0 +1 @@ +fun f() { 0b102; } diff --git a/spec/fixtures/records.accept.wave b/spec/fixtures/records.accept.wave new file mode 100644 index 00000000..349976f8 --- /dev/null +++ b/spec/fixtures/records.accept.wave @@ -0,0 +1,2 @@ +struct Box { value: T; fun get(self: ptr>) -> T { return self.value; } } +proto Plain { fun f() {} } diff --git a/spec/fixtures/records.reject.wave b/spec/fixtures/records.reject.wave new file mode 100644 index 00000000..81bf5bfd --- /dev/null +++ b/spec/fixtures/records.reject.wave @@ -0,0 +1 @@ +struct Bad { value: i32 } diff --git a/spec/fixtures/variants.accept.wave b/spec/fixtures/variants.accept.wave new file mode 100644 index 00000000..92378d34 --- /dev/null +++ b/spec/fixtures/variants.accept.wave @@ -0,0 +1 @@ +variant Option { None, Some(T), } diff --git a/spec/fixtures/variants.reject.wave b/spec/fixtures/variants.reject.wave new file mode 100644 index 00000000..3df7a14a --- /dev/null +++ b/spec/fixtures/variants.reject.wave @@ -0,0 +1 @@ +variant Bad { Value(i32 i64) } diff --git a/spec/tokens.tsv b/spec/tokens.tsv new file mode 100644 index 00000000..e913f2a1 --- /dev/null +++ b/spec/tokens.tsv @@ -0,0 +1,110 @@ +# TokenType status spellings production lexical example (- means internal/split token) +Fun implemented fun function fun +Extern implemented extern extern extern +Export implemented export export export +Pub implemented pub declaration pub +Type implemented type alias type +Enum implemented enum enumeration enum +Variant implemented variant variant variant +Static implemented static global static +Var implemented var local var +Let removed let removed let +Mut removed mut removed mut +Deref implemented deref unary deref +Const implemented const global const +If implemented if if if +Else implemented else if else +While implemented while while while +For implemented for for for +Import implemented import import import +Return implemented return statement return +Continue implemented continue statement continue +Input implemented input io input +Print implemented print io print +Println implemented println io println +Module reserved module reserved module +Class reserved class reserved class +Match implemented match match match +LogicalAnd implemented && logical-and && +AddressOf implemented & bitwise-and & +LogicalOr implemented || logical-or || +BitwiseOr implemented | bitwise-or | +NotEqual implemented != equality != +Xor implemented ^ bitwise-xor ^ +Xnor reserved ~^ reserved ~^ +BitwiseNot implemented ~ unary ~ +Nand reserved !& reserved !& +Nor reserved !| reserved !| +Not implemented ! unary ! +Condition reserved ? reserved ? +NullCoalesce reserved ?? reserved ?? +Conditional reserved ?: reserved - +In implemented in asm-operand in +Out implemented out asm-operand out +Is reserved is reserved is +As implemented as cast as +Asm implemented asm asm asm +Rol implemented << shift << +Ror implemented >> shift >> +Xnand reserved xnand reserved xnand +Operator internal - internal - +TokenTypeInt implemented isz,i8,i16,i32,i64,i128,i256,i512,i1024 integer-type isz +TokenTypeUint implemented usz,u8,u16,u32,u64,u128,u256,u512,u1024 integer-type usz +TokenTypeFloat implemented f32,f64 float-type f32 +TypeInt internal - internal - +TypeUint internal - internal - +TypeFloat internal - internal - +TypeBool internal bool (contextual identifier) internal - +TypeChar implemented char type char +TypeByte implemented byte type byte +TypeString implemented str type str +TypeCustom internal - internal - +TypePointer internal - internal - +TypeArray internal - internal - +Identifier implemented ptr,array identifier sample_name +String implemented "text" string "text" +IntLiteral implemented 0x10 integer 0x10 +Float implemented 1.25e2 float 1.25e2 +Plus implemented + additive + +Increment implemented ++ postfix ++ +PlusEq implemented += assignment-operator += +Minus implemented - additive - +Decrement implemented -- postfix -- +MinusEq implemented -= assignment-operator -= +Star implemented * multiplicative * +StarEq implemented *= assignment-operator *= +Div implemented / multiplicative / +DivEq implemented /= assignment-operator /= +Remainder implemented % multiplicative % +RemainderEq implemented %= assignment-operator %= +Equal implemented = assignment-operator = +EqualTwo implemented == equality == +Comma implemented , parameters , +Dot implemented . postfix . +SemiColon implemented ; block ; +Colon implemented : parameters : +DoubleColon implemented :: qualified-name :: +Lchevr implemented < relational < +LchevrEq implemented <= relational <= +Rchevr implemented > relational > +RchevrEq implemented >= relational >= +Lparen implemented ( parameters ( +Rparen implemented ) parameters ) +Lbrace implemented { block { +Rbrace implemented } block } +Lbrack implemented [ postfix [ +Rbrack implemented ] postfix ] +Eof internal - internal - +Error internal - internal - +Whitespace internal - internal - +Break implemented break statement break +Arrow implemented -> function -> +Array internal - internal - +Newline internal - internal - +Proto implemented proto proto proto +Struct implemented struct structure struct +TypeVoid internal - internal - +CharLiteral implemented 'a' character 'a' +BoolLiteral implemented true,false literal true +Null implemented null primary null +Clobber implemented clobber clobber clobber diff --git a/src/module_resolver.rs b/src/module_resolver.rs index 2892435a..25cd7607 100644 --- a/src/module_resolver.rs +++ b/src/module_resolver.rs @@ -146,7 +146,7 @@ impl Resolver<'_> { }; for node in &ast { - let ASTNode::Statement(StatementNode::Import(import)) = node else { + let ASTNode::Statement(StatementNode::Import(import)) = node.unspanned() else { continue; }; @@ -199,7 +199,10 @@ impl Resolver<'_> { let mut lowered = Vec::new(); for node in ast { - if matches!(node, ASTNode::Statement(StatementNode::Import(_))) { + if matches!( + node.unspanned(), + ASTNode::Statement(StatementNode::Import(_)) + ) { continue; } lowered.push(rewrite_top_level(node, &names, &key, is_entry)?); @@ -280,7 +283,7 @@ fn collect_symbols( ) -> Result { let mut symbols = HashMap::new(); for node in ast { - match node { + match node.unspanned() { ASTNode::Function(function) => { if !is_entry && function.name == "main" { return Err(module_error( @@ -341,16 +344,14 @@ fn collect_symbols( SymbolKind::Value, is_entry, )?; - if enumeration.visibility == Visibility::Public { - symbols.insert( - format!("{}::{}", enumeration.name, variant.name), - ModuleSymbol { - lowered: internal_name(path, &variant.name, is_entry), - visibility: Visibility::Public, - kind: SymbolKind::Value, - }, - ); - } + symbols.insert( + format!("{}::{}", enumeration.name, variant.name), + ModuleSymbol { + lowered: internal_name(path, &variant.name, is_entry), + visibility: enumeration.visibility, + kind: SymbolKind::Value, + }, + ); } } ASTNode::Variant(variant) => { @@ -625,6 +626,9 @@ fn rewrite_top_level( is_entry: bool, ) -> Result { match node { + ASTNode::Located { value, span } => { + Ok(rewrite_top_level(*value, names, path, is_entry)?.with_span(Some(span))) + } ASTNode::Function(mut function) => { let original = function.name.clone(); function.name = names.own[&original].lowered.clone(); @@ -751,6 +755,14 @@ fn rewrite_block( let mut out = Vec::with_capacity(nodes.len()); for node in nodes { match node { + ASTNode::Located { value, span } => { + let rewritten = rewrite_block(vec![*value], names, path, locals)?; + out.extend( + rewritten + .into_iter() + .map(|node| node.with_span(Some(span.clone()))), + ); + } ASTNode::Variable(mut variable) => { variable.type_name = rewrite_type(variable.type_name, names, path)?; if let Some(value) = variable.initial_value.take() { @@ -931,6 +943,8 @@ fn rewrite_match_pattern( locals: &HashSet, ) -> Result<(), WaveError> { match pattern { + MatchPattern::Located { value, span } => rewrite_match_pattern(value, names, path, locals) + .map_err(|e| e.with_span(Some(span)))?, MatchPattern::Ident(name) => { if !locals.contains(name) { if let Some(symbol) = resolve_name(name, names, path)? { @@ -974,6 +988,7 @@ fn rewrite_match_pattern( fn collect_pattern_bindings(pattern: &MatchPattern, locals: &mut HashSet) { match pattern { + MatchPattern::Located { value, .. } => collect_pattern_bindings(value, locals), MatchPattern::Binding(name) => { locals.insert(name.clone()); } @@ -993,6 +1008,9 @@ fn rewrite_expression( locals: &HashSet, ) -> Result { Ok(match expression { + Expression::Located { value, span } => rewrite_expression(*value, names, path, locals) + .map_err(|e| e.with_span(Some(&span)))? + .with_span(Some(span)), Expression::StructLiteral { name, fields } => Expression::StructLiteral { name: rewrite_type_name(&name, names, path)?, fields: fields diff --git a/src/runner.rs b/src/runner.rs index 61697d79..ad792f49 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -26,7 +26,7 @@ use ::parser::ast::*; use ::parser::generics::monomorphize_generics; use ::parser::hir::TypedProgram; use ::parser::import::*; -use ::parser::verification::{validate_program_detailed, SemanticSpanHint, SemanticSpanKind}; +use ::parser::verification::validate_program_detailed; use ::parser::*; use lexer::Lexer; use llvm::backend::*; @@ -61,7 +61,7 @@ fn parse_wave_tokens_or_exit( source: &str, tokens: &[lexer::Token], ) -> Vec { - parse_syntax_only(tokens).unwrap_or_else(|err| { + parse_syntax_with_spans(tokens).unwrap_or_else(|err| { let (kind, title, code) = match &err { ParseError::Syntax(_) => ( WaveErrorKind::SyntaxError(err.message().to_string()), @@ -85,6 +85,7 @@ fn parse_wave_tokens_or_exit( .with_code(code) .with_source_code(source.to_string()); + wave_err = wave_err.with_span(err.span()); if let Some(ctx) = err.context() { wave_err = wave_err.with_context(ctx.to_string()); } @@ -113,23 +114,22 @@ fn lower_wave_hir_or_exit(file_path: &Path, source: &str, ast: Vec) -> match TypedProgram::lower(ast) { Ok(program) => program, Err(error) => { - let (ast, diagnostic) = error.into_parts(); - let node = ast.get(diagnostic.top_level_index); - let (line, column, span_len) = diagnostic - .primary + let (_ast, diagnostic) = error.into_parts(); + let span = diagnostic.span.clone(); + let diagnostic_source = span .as_ref() - .and_then(|hint| semantic_hint_position(source, node, 1, hint)) - .unwrap_or((1, 1, 1)); + .and_then(|s| fs::read_to_string(&s.file).ok()) + .unwrap_or_else(|| source.to_string()); let mut error = WaveError::new( WaveErrorKind::InvalidStatement(diagnostic.message.clone()), format!("semantic validation failed: {}", diagnostic.message), file_path.display().to_string(), - line, - column, + 0, + 0, ) .with_code(diagnostic.code) - .with_source_code(source.to_string()) - .with_span_len(span_len) + .with_source_code(diagnostic_source) + .with_span(span.as_ref()) .with_context("semantic validation") .with_label(diagnostic.label) .with_help(diagnostic.help); @@ -160,33 +160,28 @@ fn validate_expanded_ast_or_exit(expanded: &ExpandedWaveAst) { .copied() .unwrap_or(0); let source_unit = expanded.sources.get(origin).unwrap_or(&expanded.sources[0]); - let node = expanded.ast.get(diagnostic.top_level_index); - let scope_occurrence = node.map_or(1, |target| { - let key = semantic_node_key(target); - 1 + expanded.ast[..diagnostic.top_level_index] - .iter() - .zip(&expanded.origins[..diagnostic.top_level_index]) - .filter(|(candidate, candidate_origin)| { - **candidate_origin == origin && semantic_node_key(candidate) == key - }) - .count() - }); - let (line, column, span_len) = diagnostic - .primary + let span = diagnostic.span.clone(); + let diagnostic_source = span .as_ref() - .and_then(|hint| semantic_hint_position(&source_unit.source, node, scope_occurrence, hint)) - .unwrap_or((1, 1, 1)); + .and_then(|span| { + expanded + .sources + .iter() + .find(|unit| unit.path.to_string_lossy() == span.file) + }) + .map(|unit| unit.source.clone()) + .unwrap_or_else(|| source_unit.source.clone()); let mut error = WaveError::new( WaveErrorKind::InvalidStatement(diagnostic.message.clone()), format!("semantic validation failed: {}", diagnostic.message), source_unit.path.display().to_string(), - line, - column, + 0, + 0, ) .with_code(diagnostic.code) - .with_source_code(source_unit.source.clone()) - .with_span_len(span_len) + .with_source_code(diagnostic_source) + .with_span(span.as_ref()) .with_context("semantic validation") .with_label(diagnostic.label) .with_help(diagnostic.help); @@ -198,178 +193,6 @@ fn validate_expanded_ast_or_exit(expanded: &ExpandedWaveAst) { process::exit(1); } -fn semantic_hint_position( - source: &str, - node: Option<&ASTNode>, - scope_occurrence: usize, - hint: &SemanticSpanHint, -) -> Option<(usize, usize, usize)> { - let (scope_start, scope_end) = - semantic_node_scope(source, node, scope_occurrence).unwrap_or((0, source.len())); - let scope = &source[scope_start..scope_end]; - let alternatives: Vec<&str> = hint.text.split('|').collect(); - let mut matches = Vec::new(); - - for alternative in alternatives { - if alternative.is_empty() { - continue; - } - let mut offset = 0usize; - while let Some(relative) = scope[offset..].find(alternative) { - let found = offset + relative; - let absolute = scope_start + found; - let boundary_ok = if alternative - .chars() - .all(|ch| ch.is_ascii_alphanumeric() || ch == '_') - { - identifier_boundary(source, absolute, alternative.len()) - } else { - true - }; - let declaration_ok = !matches!(hint.kind, SemanticSpanKind::Declaration) - || is_declaration_occurrence(source, absolute, alternative); - if boundary_ok && declaration_ok { - matches.push((absolute, alternative.len())); - } - offset = found + alternative.len(); - } - } - - matches.sort_unstable(); - matches.dedup(); - let (offset, span_len) = *matches.get(hint.occurrence.saturating_sub(1))?; - let (line, column) = source_position(source, offset); - Some((line, column, span_len.max(1))) -} - -fn identifier_boundary(source: &str, offset: usize, len: usize) -> bool { - let is_identifier = |byte: u8| byte.is_ascii_alphanumeric() || byte == b'_'; - let before_ok = offset == 0 || !is_identifier(source.as_bytes()[offset - 1]); - let after = offset + len; - let after_ok = after >= source.len() || !is_identifier(source.as_bytes()[after]); - before_ok && after_ok -} - -fn is_declaration_occurrence(source: &str, offset: usize, name: &str) -> bool { - let line_start = source[..offset].rfind('\n').map_or(0, |index| index + 1); - let prefix = source[line_start..offset].trim_start(); - [ - "fun ", "struct ", "proto ", "enum ", "variant ", "type ", "var ", "const ", "static ", - ] - .iter() - .any(|keyword| prefix.ends_with(keyword)) - || source[offset + name.len()..].trim_start().starts_with(':') -} - -fn semantic_node_key(node: &ASTNode) -> (u8, String) { - match node { - ASTNode::Function(function) => (0, function.name.clone()), - ASTNode::ExternFunction(function) => (0, function.name.clone()), - ASTNode::Struct(structure) => (1, structure.name.clone()), - ASTNode::ProtoImpl(implementation) => (2, implementation.target.clone()), - ASTNode::TypeAlias(alias) => (3, alias.name.clone()), - ASTNode::Enum(enumeration) => (4, enumeration.name.clone()), - ASTNode::Variant(variant) => (5, variant.name.clone()), - ASTNode::Variable(variable) => (6, variable.name.clone()), - ASTNode::Statement(_) => (7, String::new()), - ASTNode::Expression(_) => (8, String::new()), - ASTNode::Program(_) => (9, String::new()), - } -} - -fn semantic_node_scope( - source: &str, - node: Option<&ASTNode>, - occurrence: usize, -) -> Option<(usize, usize)> { - let node = node?; - let needle = match node { - ASTNode::Function(function) => { - format!("fun {}(", demangle_module_names(&function.name)) - } - ASTNode::ExternFunction(function) => { - format!("fun {}(", demangle_module_names(&function.name)) - } - ASTNode::Struct(structure) => { - format!("struct {}", demangle_module_names(&structure.name)) - } - ASTNode::ProtoImpl(implementation) => { - format!("proto {}", demangle_module_names(&implementation.target)) - } - ASTNode::TypeAlias(alias) => { - format!("type {}", demangle_module_names(&alias.name)) - } - ASTNode::Enum(enumeration) => { - format!("enum {}", demangle_module_names(&enumeration.name)) - } - ASTNode::Variant(variant) => { - format!("variant {}", demangle_module_names(&variant.name)) - } - ASTNode::Variable(variable) => demangle_module_names(&variable.name), - ASTNode::Statement(_) | ASTNode::Expression(_) | ASTNode::Program(_) => { - return Some((0, source.len())); - } - }; - let mut starts = source.match_indices(&needle); - let start = starts.nth(occurrence.saturating_sub(1))?.0; - let Some(open_relative) = source[start..].find('{') else { - let end = source[start..] - .find('\n') - .map_or(source.len(), |relative| start + relative); - return Some((start, end)); - }; - let open = start + open_relative; - let end = matching_source_brace(source, open).unwrap_or(source.len()); - Some((start, end)) -} - -fn matching_source_brace(source: &str, open: usize) -> Option { - let bytes = source.as_bytes(); - let mut depth = 0usize; - let mut index = open; - let mut quote = None; - let mut escaped = false; - while index < bytes.len() { - let byte = bytes[index]; - if let Some(active_quote) = quote { - if escaped { - escaped = false; - } else if byte == b'\\' { - escaped = true; - } else if byte == active_quote { - quote = None; - } - index += 1; - continue; - } - if byte == b'"' || byte == b'\'' { - quote = Some(byte); - } else if byte == b'/' && bytes.get(index + 1) == Some(&b'/') { - index = source[index..] - .find('\n') - .map_or(bytes.len(), |relative| index + relative); - continue; - } else if byte == b'{' { - depth += 1; - } else if byte == b'}' { - depth = depth.checked_sub(1)?; - if depth == 0 { - return Some(index + 1); - } - } - index += 1; - } - None -} - -fn source_position(source: &str, byte_offset: usize) -> (usize, usize) { - let prefix = &source[..byte_offset]; - let line = prefix.bytes().filter(|byte| *byte == b'\n').count() + 1; - let line_start = prefix.rfind('\n').map_or(0, |index| index + 1); - let column = source[line_start..byte_offset].chars().count() + 1; - (line, column) -} - fn panic_payload_to_string(payload: &(dyn std::any::Any + Send)) -> String { if let Some(s) = payload.downcast_ref::() { return s.clone(); @@ -831,7 +654,21 @@ fn expand_imports_for_codegen( ast: Vec, import_config: &ImportConfig, ) -> Result { - let graph = resolve_import_graph(entry_path, entry_source, ast, import_config)?; + let mut graph = resolve_import_graph(entry_path, entry_source, ast, import_config)?; + let pointer_bits = if import_config.target.arch.as_deref() == Some("wasm32") { + 32 + } else { + 64 + }; + ::parser::hir::resolve_target_types(&mut graph.ast, pointer_bits).map_err(|message| { + WaveError::new( + WaveErrorKind::InvalidStatement(message.clone()), + message, + entry_path.display().to_string(), + 0, + 0, + ) + })?; Ok(ExpandedWaveAst { ast: graph.ast, origins: graph.origins, diff --git a/tests/codegen_regressions.rs b/tests/codegen_regressions.rs index 5efa145c..f00eb4da 100644 --- a/tests/codegen_regressions.rs +++ b/tests/codegen_regressions.rs @@ -1806,7 +1806,7 @@ fun value() -> i32 { let output = run_wavec_raw([OsStr::new("check"), source.as_os_str()]); assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("wrong_return.wave:3:5"), "{}", stderr); + assert!(stderr.contains("wrong_return.wave:3:12"), "{}", stderr); assert!(stderr.contains("return \"text\";"), "{}", stderr); let repeated_return = write_wave( @@ -1824,7 +1824,7 @@ fun value(flag: bool) -> i32 { let output = run_wavec_raw([OsStr::new("check"), repeated_return.as_os_str()]); assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("repeated_return.wave:6:5"), "{}", stderr); + assert!(stderr.contains("repeated_return.wave:6:12"), "{}", stderr); assert!(stderr.contains("return \"text\";"), "{}", stderr); let duplicate = write_wave( diff --git a/tests/frontend_regressions.rs b/tests/frontend_regressions.rs new file mode 100644 index 00000000..a2e915c2 --- /dev/null +++ b/tests/frontend_regressions.rs @@ -0,0 +1,255 @@ +//! Driver-level Alpha frontend contracts, including imports and target selection. +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; +use std::sync::atomic::{AtomicUsize, Ordering}; + +static NEXT: AtomicUsize = AtomicUsize::new(0); +fn directory() -> PathBuf { + let path = std::env::temp_dir().join(format!( + "wave-alpha-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + std::fs::create_dir_all(&path).unwrap(); + path +} +fn wave(args: &[&OsStr]) -> Output { + Command::new(env!("CARGO_BIN_EXE_wavec")) + .args(args) + .output() + .unwrap() +} +fn frontend_target() -> String { + let output = wave(&[OsStr::new("print"), OsStr::new("target-list")]); + successful(&output); + String::from_utf8(output.stdout) + .unwrap() + .lines() + .next() + .expect("at least one LLVM target must be enabled") + .to_owned() +} +fn check(path: &Path, target: &str) -> Output { + wave(&[ + OsStr::new("check"), + path.as_os_str(), + OsStr::new("--target"), + OsStr::new(target), + ]) +} +fn successful(output: &Output) { + assert!( + output.status.success(), + "status {:?}\n{}\n{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn imported_variants_respect_active_and_inactive_target_attributes() { + let dir = directory(); + let target = frontend_target(); + let active_arch = target.split('-').next().unwrap(); + let inactive_arch = if active_arch == "wasm64" { + "arm64" + } else { + "wasm64" + }; + let declarations = |value_arch: &str, missing_arch: &str| { + format!( + r#" +#[target(arch="{missing_arch}")] +pub variant Choice {{ + Missing(Unavailable), +}} +#[target(arch="{value_arch}")] +pub variant Choice {{ + Value(i32), +}} +"# + ) + }; + let library = dir.join("choices.wave"); + std::fs::write(&library, declarations(active_arch, inactive_arch)).unwrap(); + let source = dir.join("main.wave"); + std::fs::write( + &source, + "import(\"./choices\")::{Choice};\nfun main() { var x: Choice = Choice::Value(16); }\n", + ) + .unwrap(); + successful(&check(&source, &target)); + std::fs::write(&library, declarations(inactive_arch, active_arch)).unwrap(); + let output = check(&source, &target); + assert!(!output.status.success()); + assert!( + String::from_utf8_lossy(&output.stderr).contains("Choice::Value"), + "{}", + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn json_diagnostics_preserve_imported_byte_ranges() { + let dir = directory(); + let library = dir.join("broken.wave"); + let text = "fun broken() {\n var 이름: i32 = 1; 이름; missing; missing;\n var after: i32 = 2;\n}\n"; + std::fs::write(&library, text).unwrap(); + let source = dir.join("main.wave"); + std::fs::write(&source, "import(\"./broken\"); fun main() {}\n").unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_wavec")) + .args(["--error-format=json", "check"]) + .arg(&source) + .args(["--target", &frontend_target()]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("broken.wave"), "{stderr}"); + let human = check(&source, &frontend_target()); + let human = String::from_utf8_lossy(&human.stderr); + assert!( + human.find('^').unwrap() < human.find("var after").unwrap(), + "the marker must immediately follow the failing source line: {human}" + ); + assert!( + stderr.contains(&format!("\"start\":{}", text.find("missing").unwrap())), + "{stderr}" + ); + assert!( + stderr.contains(&format!("\"end\":{}", text.find("missing").unwrap() + 7)), + "{stderr}" + ); +} + +#[cfg(any(feature = "llvm-target-core64", feature = "llvm-target-all"))] +#[test] +fn literal_defaults_compile_and_run_with_their_shared_numeric_values() { + let dir = directory(); + let source = dir.join("defaults.wave"); + std::fs::write(&source,r#" +const RADIX: i128 = 0x10; +static OCTAL: u128 = 0o20; +const LARGE: f64 = 18446744073709551616 as f64; +const EXPONENT: f64 = 1e2; +enum E -> i32 { Min = -1, Hex = 0x10 } +fun hex(x: i32 = 0x10) -> i32 { return x; } +fun binary(x: i32 = 0b1_0000) -> i32 { return x; } +fun octal(x: i32 = 0o20) -> i32 { return x; } +fun decimal(x: i32 = 1_6) -> i32 { return x; } +fun exponent(x: f64 = 1.6e1) -> f64 { return x; } +fun main() -> i32 { + if (hex() != 16 || binary() != 16 || octal() != 16 || decimal() != 16 || exponent() != 16.0) { return 1; } + if (RADIX != 16 || OCTAL != 16 || LARGE != 18446744073709551616.0 || EXPONENT != 100.0 || E::Min != -1 || E::Hex != 16) { return 3; } + var pointer_word: usz = 16; + if (pointer_word != 16) { return 2; } + return 0; +} +"#).unwrap(); + successful(&wave(&[ + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--run"), + OsStr::new("--out-dir"), + dir.as_os_str(), + ])); +} + +#[cfg(any(feature = "llvm-target-wasm", feature = "llvm-target-all"))] +#[test] +fn pointer_sized_integer_ranges_follow_wasm_target_width() { + let dir = directory(); + let source = dir.join("word.wave"); + std::fs::write(&source, "fun main() { var x: usz = 4294967296; }\n").unwrap(); + successful(&check(&source, "wasm64-unknown-unknown")); + let output = check(&source, "wasm32-unknown-unknown"); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("u32") && !stderr.contains("panicked"), + "{stderr}" + ); + std::fs::write( + &source, + "fun word(x: isz) -> isz { return x; }\nfun main() {}\n", + ) + .unwrap(); + for (target, bits) in [ + ("wasm32-unknown-unknown", 32), + ("wasm64-unknown-unknown", 64), + ] { + let output_dir = dir.join(target); + successful(&wave(&[ + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new(target), + OsStr::new("--emit=ir"), + OsStr::new("--out-dir"), + output_dir.as_os_str(), + ])); + let ir = std::fs::read_to_string(output_dir.join("word.ll")).unwrap(); + assert!( + ir.contains(&format!("define i{bits} @word(i{bits}")), + "{ir}" + ); + } +} + +#[cfg(any(feature = "llvm-target-core64", feature = "llvm-target-all"))] +#[test] +fn never_returning_calls_lower_to_noreturn_and_unreachable() { + let dir = directory(); + let source = dir.join("never.wave"); + std::fs::write( + &source, + "fun stop() -> ! { while (true) {} } fun value() -> i32 { stop(); } fun main() {}\n", + ) + .unwrap(); + successful(&wave(&[ + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--emit=ir"), + OsStr::new("--out-dir"), + dir.as_os_str(), + ])); + let ir = std::fs::read_to_string(dir.join("never.ll")).unwrap(); + assert!(ir.contains("define void @stop()"), "{ir}"); + assert!(ir.contains("noreturn"), "{ir}"); + let value = ir + .split("define i32 @value()") + .nth(1) + .unwrap() + .split("\n}") + .next() + .unwrap(); + assert!( + value.contains("call void @stop()") && value.contains("unreachable"), + "{value}" + ); +} + +#[test] +fn file_errors_do_not_invent_a_source_position() { + let source = directory().join("missing.wave"); + let target = frontend_target(); + let human = check(&source, &target); + assert!(!human.status.success()); + let human = String::from_utf8_lossy(&human.stderr); + assert!(human.contains("failed to read file"), "{human}"); + assert!(!human.contains("missing.wave:1:1"), "{human}"); + let json = wave(&[ + OsStr::new("--error-format=json"), + OsStr::new("check"), + source.as_os_str(), + OsStr::new("--target"), + OsStr::new(&target), + ]); + assert!(!json.status.success()); + let json = String::from_utf8_lossy(&json.stderr); + assert!(json.contains("\"span\":null"), "{json}"); + assert!(json.contains("\"line\":0"), "{json}"); + assert!(json.contains("\"column\":0"), "{json}"); +}