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..2df8e904 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -82,11 +82,15 @@ 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
- 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: >-
@@ -328,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
@@ -453,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:
@@ -872,6 +879,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..4f4878ed 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -91,26 +91,52 @@ 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`
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:
@@ -152,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
```
---
@@ -177,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`
@@ -189,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