From 7a817450ab0b0317f364cbec88fd60d03532ee35 Mon Sep 17 00:00:00 2001 From: LunaStev Date: Sun, 27 Sep 2026 12:10:07 +0900 Subject: [PATCH 1/3] Stabilize frontend semantics and std boundaries Fix 17 frontend, numeric, CLI, and standard-library issues. Extract typed HIR and async lowering into front/hir and keep exact constant arithmetic in dependency-free utils. Signed-off-by: LunaStev --- .github/workflows/rust.yml | 9 +- Cargo.lock | 12 + Cargo.toml | 2 + MAINTAINERS | 4 + front/hir/Cargo.toml | 10 + front/{parser => hir}/src/async_lower.rs | 4 +- .../src/hir => hir/src}/conversions.rs | 178 +++++- front/{parser/src/hir.rs => hir/src/lib.rs} | 264 ++------- front/hir/src/numeric_checks.rs | 427 ++++++++++++++ front/{parser => hir}/tests/alpha_frontend.rs | 4 +- front/{parser => hir}/tests/async_frontend.rs | 12 +- .../{parser => hir}/tests/generic_methods.rs | 4 +- .../tests/io_match_contracts.rs | 3 +- .../tests/parse_var_and_generics.rs | 2 +- .../{parser => hir}/tests/phase1_frontend.rs | 2 +- front/{parser => hir}/tests/source_spans.rs | 2 +- front/{parser => hir}/tests/typed_hir.rs | 4 +- .../{parser => hir}/tests/variant_frontend.rs | 2 +- front/parser/Cargo.toml | 2 +- front/parser/src/ast.rs | 2 + front/parser/src/ast/visit.rs | 200 +++++++ front/parser/src/lib.rs | 8 +- front/parser/src/parser/control.rs | 19 + front/parser/src/parser/functions.rs | 130 ++--- front/parser/src/parser/items.rs | 339 ++++-------- front/parser/src/parser/parse.rs | 16 +- front/parser/src/verification.rs | 101 ++-- .../src/verification/const_dependencies.rs | 2 +- front/parser/src/verification/facts.rs | 60 ++ front/parser/src/verification/numeric.rs | 16 + llvm/Cargo.toml | 1 + llvm/src/codegen/address.rs | 10 +- llvm/src/codegen/consts.rs | 7 +- llvm/src/codegen/conversions.rs | 274 ++++++++- llvm/src/codegen/ir.rs | 6 +- llvm/src/codegen/validation.rs | 2 +- llvm/src/codegen/variants.rs | 2 +- llvm/src/expression/lvalue.rs | 2 +- llvm/src/expression/rvalue/assign.rs | 2 +- llvm/src/expression/rvalue/mod.rs | 6 +- llvm/src/statement/asm.rs | 6 +- llvm/src/statement/assign.rs | 2 +- llvm/src/statement/control.rs | 2 +- llvm/src/statement/expr_stmt.rs | 2 +- llvm/src/statement/io.rs | 2 +- llvm/src/statement/mod.rs | 4 +- llvm/src/statement/variable.rs | 2 +- llvm/tests/codegen_errors.rs | 2 +- src/cli.rs | 17 + src/runner.rs | 4 +- std/bytes/cursor.wave | 11 + std/bytes/leb128.wave | 20 + std/bytes/types.wave | 21 + std/env/environ.wave | 15 +- std/env/parse.wave | 11 +- std/net/tcp.wave | 1 + std/net/vectored.wave | 5 + std/sys/freebsd/common/tty.wave | 1 + std/sys/linux/amd64/tty.wave | 1 + std/sys/linux/arm64/tty.wave | 1 + std/sys/linux/riscv64/tty.wave | 1 + std/sys/macos/amd64/tty.wave | 1 + std/sys/macos/arm64/tty.wave | 1 + tests/fixtures/stabilization_17/bytes.wave | 50 ++ .../stabilization_17/environment.wave | 24 + tests/fixtures/stabilization_17/numbers.wave | 31 ++ tests/fixtures/stabilization_17/tcp_zero.wave | 47 ++ tests/fixtures/stabilization_17/wasm.cjs | 22 + tests/fixtures/stabilization_17/wasm.wave | 11 + tests/io_constant_regressions.rs | 2 +- tests/stabilization_17.rs | 297 ++++++++++ tools/test_std_boundary_runtime.py | 43 ++ utils/src/const_int.rs | 523 ++++++++++++++++++ utils/src/lib.rs | 1 + utils/tests/fixtures/const_int.tsv | 101 ++++ 75 files changed, 2712 insertions(+), 725 deletions(-) create mode 100644 front/hir/Cargo.toml rename front/{parser => hir}/src/async_lower.rs (99%) rename front/{parser/src/hir => hir/src}/conversions.rs (77%) rename front/{parser/src/hir.rs => hir/src/lib.rs} (78%) create mode 100644 front/hir/src/numeric_checks.rs rename front/{parser => hir}/tests/alpha_frontend.rs (99%) rename front/{parser => hir}/tests/async_frontend.rs (87%) rename front/{parser => hir}/tests/generic_methods.rs (98%) rename front/{parser => hir}/tests/io_match_contracts.rs (97%) rename front/{parser => hir}/tests/parse_var_and_generics.rs (99%) rename front/{parser => hir}/tests/phase1_frontend.rs (99%) rename front/{parser => hir}/tests/source_spans.rs (99%) rename front/{parser => hir}/tests/typed_hir.rs (98%) rename front/{parser => hir}/tests/variant_frontend.rs (99%) create mode 100644 front/parser/src/ast/visit.rs create mode 100644 front/parser/src/verification/facts.rs create mode 100644 tests/fixtures/stabilization_17/bytes.wave create mode 100644 tests/fixtures/stabilization_17/environment.wave create mode 100644 tests/fixtures/stabilization_17/numbers.wave create mode 100644 tests/fixtures/stabilization_17/tcp_zero.wave create mode 100644 tests/fixtures/stabilization_17/wasm.cjs create mode 100644 tests/fixtures/stabilization_17/wasm.wave create mode 100644 tests/stabilization_17.rs create mode 100644 utils/src/const_int.rs create mode 100644 utils/tests/fixtures/const_int.tsv diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8a2accd2..2f9c18b8 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -861,7 +861,7 @@ jobs: env: WAVE_RUNTIME_ARTIFACT_DIR: ${{ runner.temp }} run: >- - cargo test --locked --no-fail-fast --test runtime_regressions --test std_io_regressions --test native_providers --target aarch64-pc-windows-msvc + cargo test --locked --no-fail-fast --test runtime_regressions --test std_io_regressions --test native_providers --test stabilization_17 --target aarch64-pc-windows-msvc --no-default-features --features llvm-target-aarch64 --jobs 2 - name: Save failed native runtime compiler and fixture @@ -1014,6 +1014,13 @@ jobs: cargo test --locked --no-default-features --features llvm-target-wasm --test release_stabilization webassembly_ --jobs 2 + - name: Run checked numeric conversion and shift regressions + env: + WAVE_RUN_WASM_RUNTIME_TESTS: "1" + run: >- + cargo test --locked --no-default-features --features llvm-target-wasm + --test stabilization_17 webassembly_ --jobs 2 + - name: Build and run browser and WASI modules run: | set -euo pipefail diff --git a/Cargo.lock b/Cargo.lock index bc38f9dc..b8c58970 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,6 +31,16 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "hir" +version = "0.1.0" +dependencies = [ + "error", + "lexer", + "parser", + "utils", +] + [[package]] name = "inkwell" version = "0.8.0" @@ -79,6 +89,7 @@ name = "llvm" version = "0.1.0" dependencies = [ "error", + "hir", "inkwell", "lexer", "llvm-sys", @@ -196,6 +207,7 @@ name = "wavec" version = "0.2.1-pre-beta-dev" dependencies = [ "error", + "hir", "lexer", "llvm", "parser", diff --git a/Cargo.toml b/Cargo.toml index b83cf94b..2481af8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ path = "src/lib.rs" utils = { path = "utils" } lexer = { path = "front/lexer" } parser = { path = "front/parser" } +hir = { path = "front/hir" } error = { path = "front/error" } llvm = { path = "./llvm", default-features = false } @@ -28,6 +29,7 @@ llvm-target-x86 = ["llvm/llvm-target-x86"] members = [ "front/lexer", "front/parser", + "front/hir", "llvm", "front/error", "utils", diff --git a/MAINTAINERS b/MAINTAINERS index eb459054..c850b66b 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -6,6 +6,10 @@ F: front/lexer/ M: luna@lunastev.org F: front/parser/ +[Compiler Frontend - HIR] +M: luna@lunastev.org +F: front/hir/ + [Error System] M: luna@lunastev.org F: front/error/ diff --git a/front/hir/Cargo.toml b/front/hir/Cargo.toml new file mode 100644 index 00000000..bdf48675 --- /dev/null +++ b/front/hir/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "hir" +version = "0.1.0" +edition = "2021" + +[dependencies] +parser = { path = "../parser" } +lexer = { path = "../lexer" } +error = { path = "../error" } +utils = { path = "../../utils" } diff --git a/front/parser/src/async_lower.rs b/front/hir/src/async_lower.rs similarity index 99% rename from front/parser/src/async_lower.rs rename to front/hir/src/async_lower.rs index 865195fa..bd0e9245 100644 --- a/front/parser/src/async_lower.rs +++ b/front/hir/src/async_lower.rs @@ -2,8 +2,8 @@ //! //! Source expressions are evaluated in order into frame slots. A pending await //! returns from the resume function; re-entry selects only its saved state. -use crate::ast::*; -use crate::hir::{HirExpressionType, TypedProgram}; +use crate::{HirExpressionType, TypedProgram}; +use parser::ast::*; use std::collections::HashMap; #[derive(Debug, Clone)] diff --git a/front/parser/src/hir/conversions.rs b/front/hir/src/conversions.rs similarity index 77% rename from front/parser/src/hir/conversions.rs rename to front/hir/src/conversions.rs index 463a2dd7..d44232bb 100644 --- a/front/parser/src/hir/conversions.rs +++ b/front/hir/src/conversions.rs @@ -14,8 +14,8 @@ //! conversions, including equal-width signedness changes and explicit identity //! casts. ABI storage/transport conversion is a separate backend boundary. use super::{ExpressionId, HirExpressionType, TypedProgram}; -use crate::ast::{AssignOperator, Expression, Literal, Operator, WaveType}; use error::SourceSpan; +use parser::ast::{AssignOperator, Expression, Literal, Operator, WaveType}; use std::collections::HashSet; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -30,8 +30,11 @@ pub enum ConversionKind { SignExtend, ZeroExtend, Truncate, + IntegerToBool, + FloatToBool, SignedToFloat, UnsignedToFloat, + // Checked, truncation toward zero; invalid values trap. FloatToSigned, FloatToUnsigned, FloatExtend, @@ -58,6 +61,8 @@ pub struct NumericExpressionInfo { /// Required destination at this occurrence, absent for an unconstrained value. pub context_type: Option, pub conversions: Vec, + /// Original count type; checked before conversion to the LHS width. + pub shift_count_type: Option, } #[derive(Clone, Debug)] pub struct ConversionError { @@ -95,6 +100,15 @@ pub fn conversion_kind(source: &WaveType, target: &WaveType) -> Option Option bool { + value.is_finite() + && value < self.upper + && if self.lower_inclusive { + value >= self.lower + } else { + value > self.lower + } + } +} +pub fn float_integer_range(source: &WaveType, target: &WaveType) -> FloatIntegerRange { + let WaveType::Float(source_bits) = source else { + panic!("non-float conversion source") + }; + let round = |v: f64| { + if *source_bits == 32 { + (v as f32) as f64 + } else { + v + } + }; + let signed = !unsigned(target); + let bits = integer_width(target).expect("integer conversion destination"); + let upper = round(2f64.powi(i32::from(bits - u16::from(signed)))); + let minimum = if signed { -upper } else { 0.0 }; + let before_minimum = round(minimum - 1.0); + FloatIntegerRange { + lower: before_minimum, + lower_inclusive: before_minimum == minimum, + upper, + } +} + impl NumericExpressionInfo { pub fn verify(&self) -> Result<(), String> { let mut previous = &self.evaluation_type; @@ -272,6 +327,7 @@ impl Planner<'_> { result_type: source, context_type: demand.clone().filter(scalar), conversions: Vec::new(), + shift_count_type: None, }; match expr { Expression::Cast { @@ -282,7 +338,9 @@ impl Planner<'_> { Some(target_type.clone()) } else { literal(inner).then(|| { - if pointer(target_type) { + if target_type == &WaveType::Bool { + WaveType::Int(1024) + } else if pointer(target_type) { WaveType::Int(64) } else { target_type.clone() @@ -351,6 +409,16 @@ impl Planner<'_> { Some(if numeric(&rt) { WaveType::Int(64) } else { rt }), None, ); + } else if matches!(operator, Operator::ShiftLeft | Operator::ShiftRight) { + let left_type = self.plan(left, None, left_hint.cloned())?; + // An untyped count must not wrap at the default i32 width + // before the checked-shift range test. + let count_hint = literal(right).then_some(WaveType::Int(1024)); + let right_type = self.plan(right, None, count_hint)?; + fact.evaluation_type = left_type.clone(); + fact.result_type = left_type.clone(); + fact.computation_type = Some(left_type); + fact.shift_count_type = Some(right_type); } else if numeric(<) && numeric(&rt) { if matches!(operator, Operator::LogicalAnd | Operator::LogicalOr) { self.plan(left, None, None); @@ -432,6 +500,17 @@ pub(super) fn verify_expression( fact: &NumericExpressionInfo, ) -> Result<(), String> { fact.verify()?; + if fact.shift_count_type.is_some() + != matches!( + expr, + Expression::BinaryExpression { + operator: Operator::ShiftLeft | Operator::ShiftRight, + .. + } + ) + { + return Err("missing or extraneous shift count contract".into()); + } let child = |expr: &Expression| { program .numeric_expression_of(expr) @@ -487,6 +566,20 @@ pub(super) fn verify_expression( { return Err("invalid logical computation".into()); } + } else if matches!(operator, Operator::ShiftLeft | Operator::ShiftRight) { + if &left.result_type != computation + || fact.evaluation_type != *computation + || fact.shift_count_type.as_ref() != Some(&right.result_type) + || !matches!( + right.result_type, + WaveType::Int(_) + | WaveType::Uint(_) + | WaveType::Byte + | WaveType::Char + ) + { + return Err("invalid checked shift contract".into()); + } } else { if &left.result_type != computation || &right.result_type != computation { return Err("operand conversion does not reach computation type".into()); @@ -541,12 +634,12 @@ pub(super) fn verify_expression( #[cfg(test)] mod tests { use super::*; - use crate::ast::ASTNode; + use parser::ast::ASTNode; fn program(source: &str) -> TypedProgram { let tokens = lexer::Lexer::new_with_file(source, "conversions.wave") .tokenize() .unwrap(); - TypedProgram::lower(crate::parse_syntax_with_spans(&tokens).unwrap()).unwrap() + TypedProgram::lower(parser::parse_syntax_with_spans(&tokens).unwrap()).unwrap() } fn initializer(p: &TypedProgram, index: usize) -> &Expression { let ASTNode::Function(f) = &p.syntax()[0] else { @@ -557,6 +650,83 @@ mod tests { }; v.initial_value.as_ref().unwrap() } + #[test] + fn checked_ranges_match_exact_integer_boundaries() { + use utils::const_int::ConstInt; + for source_bits in [32, 64] { + for bits in [8, 16, 32, 64, 128, 256, 512, 1024] { + for signed in [false, true] { + let target = if signed { + WaveType::Int(bits) + } else { + WaveType::Uint(bits) + }; + let range = float_integer_range(&WaveType::Float(source_bits), &target); + let edge = 2f64.powi(i32::from(bits - u16::from(signed))); + let mut values = vec![ + 0.0, + -0.0, + -0.5, + -1.0, + f64::NAN, + f64::INFINITY, + f64::NEG_INFINITY, + edge, + -edge, + edge - 1.0, + -edge - 1.0, + ]; + if source_bits == 32 { + let raw = (edge as f32).to_bits(); + for delta in [raw.saturating_sub(1), raw, raw.saturating_add(1)] { + let n = f32::from_bits(delta) as f64; + values.extend([n, -n]); + } + } else { + for delta in [ + edge.to_bits().saturating_sub(1), + edge.to_bits(), + edge.to_bits().saturating_add(1), + ] { + let n = f64::from_bits(delta); + values.extend([n, -n]); + } + } + for value in values { + let value = if source_bits == 32 { + (value as f32) as f64 + } else { + value + }; + let exact = ConstInt::from_f64(value).is_some_and(|n| n.fits(bits, signed)); + assert_eq!( + range.contains(value), + exact, + "f{source_bits} to {target:?}: {value}" + ); + } + } + } + } + } + + #[test] + fn shifts_preserve_the_original_count_and_bool_casts_compare_with_zero() { + let p = + program("fun check(x: u8, n: u64) { var y: u8 = x << n; var b: bool = n as bool; }"); + p.verify_conversions().unwrap(); + let shift = p.numeric_expression_of(initializer(&p, 0)).unwrap(); + assert_eq!(shift.shift_count_type, Some(WaveType::Uint(64))); + assert_eq!(shift.computation_type, Some(WaveType::Uint(8))); + assert_eq!( + p.numeric_expression_of(initializer(&p, 1)) + .unwrap() + .conversions[0] + .kind, + ConversionKind::IntegerToBool + ); + } + #[test] fn ordered_casts_preserve_truncation_signedness_modes_and_spans() { let source = "fun check(x: i32) { var y: i64 = x as u8 as i32; var z: u32 = x as u32; var identity: i32 = x as i32; }"; diff --git a/front/parser/src/hir.rs b/front/hir/src/lib.rs similarity index 78% rename from front/parser/src/hir.rs rename to front/hir/src/lib.rs index a991180e..50b9fb71 100644 --- a/front/parser/src/hir.rs +++ b/front/hir/src/lib.rs @@ -18,12 +18,19 @@ //! lets future variant and async lowering attach semantic facts without using //! backend-owned state or expression addresses as public identities. +// Preserve the existing diagnostic payloads while extracting this crate. +// The parser previously allowed this lint for these same APIs. +#![allow(clippy::result_large_err)] + +pub mod async_lower; pub mod conversions; +mod numeric_checks; use conversions::{ConversionError, NumericExpressionInfo}; +use parser::ast::visit::{walk_expression, walk_node, walk_nodes}; -use crate::ast::{ASTNode, Expression, MatchPattern, StatementNode, WaveType}; -use crate::types::{parse_type, split_top_level_generic_args, token_type_to_wave_type}; -use crate::verification::{analyze_hir_expression_types, SemanticDiagnostic}; +use parser::ast::{ASTNode, Expression, MatchPattern, StatementNode, WaveType}; +use parser::types::{parse_type, split_top_level_generic_args, token_type_to_wave_type}; +use parser::verification::{analyze_semantic_facts, SemanticDiagnostic, SemanticFacts}; use std::collections::{HashMap, HashSet}; use std::fmt; @@ -56,39 +63,10 @@ impl PatternId { } } -/// The semantic type known before contextual lowering is performed. -/// -/// Literal forms remain explicit because their final representation can depend -/// on an assignment, argument, return, or aggregate context. They are not -/// silently committed to a backend type at this boundary. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum HirExpressionType { - Resolved(WaveType), - IntegerLiteral, - FloatLiteral, - Null, - ArrayLiteral, - AddressedArrayLiteral, - Unknown, -} - -/// Fully resolved variant constructor selected by semantic analysis. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct HirVariantConstruction { - pub variant_type: WaveType, - pub case_name: String, - pub discriminant: u32, - pub payload_types: Vec, -} - -/// Concrete variant case selected by a semantically validated pattern. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct HirVariantPattern { - pub variant_type: WaveType, - pub case_name: String, - pub discriminant: u32, - pub payload_types: Vec, -} +pub use parser::verification::{ + AnalyzedExpressionType as HirExpressionType, VariantConstruction as HirVariantConstruction, + VariantPattern as HirVariantPattern, +}; /// Semantically validated frontend program consumed by later lowering passes. /// @@ -149,14 +127,14 @@ 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, - analyzed_expected, - analyzed_integers, - ) = match analyze_hir_expression_types(&syntax, &source_map) { + let source_map = parser::source::SourceMap::detach(&mut syntax); + let SemanticFacts { + expression_types: analyzed_types, + variant_constructions: analyzed_variants, + variant_patterns: analyzed_patterns, + expected_types: analyzed_expected, + integer_patterns: analyzed_integers, + } = match analyze_semantic_facts(&syntax, &source_map) { Ok(analysis) => analysis, Err(diagnostic) => return Err(HirLoweringError { syntax, diagnostic }), }; @@ -226,6 +204,12 @@ impl TypedProgram { pattern_spans, }; program.numeric_expressions = conversions::build(&program); + if let Err(diagnostic) = numeric_checks::validate(&program) { + return Err(HirLoweringError { + syntax: program.syntax, + diagnostic, + }); + } Ok(program) } @@ -283,7 +267,7 @@ impl TypedProgram { pub fn uses_async_runtime(&self) -> bool { let mut found = false; walk_nodes(self.syntax(), &mut |e| { - if matches!(e,Expression::FunctionCall{name,..} if crate::async_intrinsics::is_intrinsic(name)) + if matches!(e,Expression::FunctionCall{name,..} if parser::async_intrinsics::is_intrinsic(name)) { found = true; } @@ -296,7 +280,7 @@ impl TypedProgram { let mut requirements = std::collections::BTreeMap::new(); walk_nodes(self.syntax(), &mut |expression| { if let Expression::FunctionCall { name, .. } = expression { - for &symbol in crate::async_intrinsics::runtime_symbols(name) { + for &symbol in parser::async_intrinsics::runtime_symbols(name) { requirements.entry(symbol).or_insert_with(|| { self.expression_id(expression) .and_then(|id| self.expression_span(id)) @@ -554,7 +538,7 @@ fn canonicalize_type(ty: &mut WaveType, named: &HashMap) { } fn canonicalize_function_types( - function: &mut crate::ast::FunctionNode, + function: &mut parser::ast::FunctionNode, named: &HashMap, ) { for parameter in &mut function.parameters { @@ -777,192 +761,6 @@ fn canonicalize_expression_types(expression: &mut Expression, named: &HashMap 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 { - for parameter in &method.parameters { - if let Some(default) = ¶meter.initial_value { - walk_expression(default, visit); - } - } - walk_nodes(&method.body, visit); - } - } - ASTNode::ProtoImpl(implementation) => { - for method in &implementation.methods { - for parameter in &method.parameters { - if let Some(default) = ¶meter.initial_value { - walk_expression(default, visit); - } - } - walk_nodes(&method.body, visit); - } - } - ASTNode::Statement(statement) => walk_statement(statement, visit), - ASTNode::Variable(variable) => { - if let Some(initializer) = &variable.initial_value { - walk_expression(initializer, visit); - } - } - ASTNode::Expression(expression) => walk_expression(expression, visit), - ASTNode::ExternFunction(_) - | ASTNode::Program(_) - | ASTNode::TypeAlias(_) - | ASTNode::Enum(_) - | ASTNode::Variant(_) => {} - } -} - -fn walk_statement(statement: &StatementNode, visit: &mut impl FnMut(&Expression)) { - match statement { - StatementNode::PrintFormat { args, .. } - | StatementNode::PrintlnFormat { args, .. } - | StatementNode::Input { args, .. } => { - for argument in args { - walk_expression(argument, visit); - } - } - StatementNode::If { - condition, - body, - else_if_blocks, - else_block, - } => { - walk_expression(condition, visit); - walk_nodes(body, visit); - if let Some(blocks) = else_if_blocks { - for (condition, body) in blocks.iter() { - walk_expression(condition, visit); - walk_nodes(body, visit); - } - } - if let Some(body) = else_block { - walk_nodes(body, visit); - } - } - StatementNode::For { - initialization, - condition, - increment, - body, - } => { - walk_node(initialization, visit); - walk_expression(condition, visit); - walk_expression(increment, visit); - walk_nodes(body, visit); - } - StatementNode::While { condition, body } => { - walk_expression(condition, visit); - walk_nodes(body, visit); - } - StatementNode::Match { value, arms } => { - walk_expression(value, visit); - for arm in arms { - walk_nodes(&arm.body, visit); - } - } - StatementNode::Assign { value, .. } => walk_expression(value, visit), - StatementNode::AsmBlock { - inputs, outputs, .. - } => { - for (_, expression) in inputs.iter().chain(outputs.iter()) { - walk_expression(expression, visit); - } - } - StatementNode::Return(Some(expression)) | StatementNode::Expression(expression) => { - walk_expression(expression, visit) - } - StatementNode::Print(_) - | StatementNode::Println(_) - | StatementNode::Variable(_) - | StatementNode::Import(_) - | StatementNode::Break - | StatementNode::Continue - | StatementNode::Return(None) => {} - } -} - -pub(crate) fn walk_expression(expression: &Expression, visit: &mut impl FnMut(&Expression)) { - if let Expression::Located { value, .. } = expression { - walk_expression(value, visit); - return; - } - visit(expression); - match expression { - Expression::Located { value, .. } => walk_expression(value, visit), - Expression::StructLiteral { fields, .. } => { - for (_, value) in fields { - walk_expression(value, visit); - } - } - Expression::FunctionCall { args, .. } => { - for argument in args { - walk_expression(argument, visit); - } - } - Expression::MethodCall { object, args, .. } => { - walk_expression(object, visit); - for argument in args { - walk_expression(argument, visit); - } - } - Expression::Deref(inner) - | Expression::AddressOf(inner) - | Expression::Await(inner) - | Expression::Grouped(inner) - | Expression::Unary { expr: inner, .. } - | Expression::Cast { expr: inner, .. } - | Expression::FieldAccess { object: inner, .. } - | Expression::IncDec { target: inner, .. } => walk_expression(inner, visit), - Expression::BinaryExpression { left, right, .. } - | Expression::IndexAccess { - target: left, - index: right, - } - | Expression::AssignOperation { - target: left, - value: right, - .. - } - | Expression::Assignment { - target: left, - value: right, - } => { - walk_expression(left, visit); - walk_expression(right, visit); - } - Expression::ArrayLiteral(values) => { - for value in values { - walk_expression(value, visit); - } - } - Expression::AsmBlock { - inputs, outputs, .. - } => { - for (_, expression) in inputs.iter().chain(outputs.iter()) { - walk_expression(expression, visit); - } - } - Expression::Null | Expression::Literal(_) | Expression::Variable(_) => {} - } -} - fn walk_patterns_in_nodes(nodes: &[ASTNode], visit: &mut impl FnMut(&MatchPattern)) { for node in nodes { match node { diff --git a/front/hir/src/numeric_checks.rs b/front/hir/src/numeric_checks.rs new file mode 100644 index 00000000..8c673dfb --- /dev/null +++ b/front/hir/src/numeric_checks.rs @@ -0,0 +1,427 @@ +// SPDX-License-Identifier: MPL-2.0 +//! Diagnose statically invalid checked numeric operations using the same ordered +//! HIR conversions as runtime lowering. Mutable bindings are never propagated. +use super::{conversions::*, *}; +use parser::ast::{FunctionNode, Literal, Mutability, Operator}; +use utils::const_int::ConstInt; + +type Scope = HashMap>; +#[derive(Clone)] +enum Number { + Int(ConstInt), + Float(f64), + Array(Vec>), + Struct(HashMap>), +} +impl Number { + fn truth(&self) -> bool { + match self { + Self::Int(n) => !n.is_zero(), + Self::Float(n) => *n != 0.0, + _ => unreachable!("aggregate is not a numeric truth value"), + } + } + fn normalize(self, ty: &WaveType) -> Self { + match (self, ty) { + (Self::Int(n), ty) if integer_width(ty).is_some() => { + let bits = integer_width(ty).unwrap(); + Self::Int(n.normalize(bits, !unsigned(ty))) + } + (Self::Float(n), WaveType::Float(32)) => Self::Float((n as f32) as f64), + (n, _) => n, + } + } +} +struct Checker<'a> { + program: &'a TypedProgram, + globals: Scope, +} +type Failure = (ExpressionId, String); +impl Checker<'_> { + fn fail(&self, expr: &Expression, message: &str) -> Failure { + (self.program.expression_id(expr).unwrap(), message.into()) + } + fn eval(&mut self, expr: &Expression, scope: &Scope) -> Result, Failure> { + let fact = self.program.numeric_expression_of(expr); + let value = match expr { + Expression::ArrayLiteral(items) => { + let mut values = Vec::new(); + for item in items { + values.push(self.eval(item, scope)?); + } + Some(Number::Array(values)) + } + Expression::StructLiteral { fields, .. } => { + let mut values = HashMap::new(); + for (name, item) in fields { + values.insert(name.clone(), self.eval(item, scope)?); + } + Some(Number::Struct(values)) + } + Expression::FieldAccess { object, field } => { + if let Some(Number::Struct(fields)) = self.eval(object, scope)? { + fields.get(field).cloned().flatten() + } else { + None + } + } + Expression::IndexAccess { target, index } => { + match (self.eval(target, scope)?, self.eval(index, scope)?) { + (Some(Number::Array(items)), Some(Number::Int(index))) => index + .to_usize() + .and_then(|i| items.get(i).cloned().flatten()), + _ => None, + } + } + Expression::Literal(Literal::Int(raw)) => { + let parsed = lexer::number::IntegerLiteral::parse(raw).unwrap(); + let mut n = + ConstInt::from_digits(&parsed.digits, parsed.radix).ok_or_else(|| { + self.fail(expr, "integer literal exceeds the supported 1024-bit range") + })?; + if n.bits() > 1024 { + return Err( + self.fail(expr, "integer literal exceeds the supported 1024-bit range") + ); + } + if parsed.negative { + n = n.negated(); + } + Some(Number::Int(n)) + } + Expression::Literal(Literal::Float(n)) => Some(Number::Float(*n)), + Expression::Literal(Literal::Bool(n)) => { + Some(Number::Int(ConstInt::from_u64((*n) as u64))) + } + Expression::Literal(Literal::Byte(n)) => { + Some(Number::Int(ConstInt::from_u64((*n) as u64))) + } + Expression::Literal(Literal::Char(n)) => { + Some(Number::Int(ConstInt::from_u64((*n as u32) as u64))) + } + Expression::Variable(name) => { + if let Some(value) = scope.get(name) { + value.clone() + } else { + self.globals.get(name).cloned().flatten() + } + } + + Expression::Cast { expr, .. } | Expression::Grouped(expr) => self.eval(expr, scope)?, + Expression::Unary { + operator, + expr: inner, + } => match (operator, self.eval(inner, scope)?) { + (Operator::Neg, Some(Number::Int(n))) => Some(Number::Int(n.negated())), + (Operator::Neg, Some(Number::Float(n))) => Some(Number::Float(-n)), + (Operator::BitwiseNot, Some(Number::Int(n))) => n.checked_not().map(Number::Int), + (Operator::Not | Operator::LogicalNot, Some(n)) => { + Some(Number::Int(ConstInt::from_u64((!n.truth()) as u64))) + } + _ => None, + }, + Expression::BinaryExpression { + left, + operator, + right, + } => { + let a = self.eval(left, scope)?; + let b = self.eval(right, scope)?; + let Some(fact) = fact else { + return Ok(None); + }; + if fact.shift_count_type.is_some() { + if let Some(Number::Int(n)) = &b { + let width = integer_width(fact.computation_type.as_ref().unwrap()).unwrap(); + if n.is_negative() || n >= &ConstInt::from_u64((width) as u64) { + return Err( + self.fail(right, &format!("shift count must be in 0..{width}")) + ); + } + } + } + match (a, b) { + (Some(Number::Int(a)), Some(Number::Int(b))) => { + use Operator::*; + let n = match operator { + Add => a.checked_add(&b), + Subtract => a.checked_sub(&b), + Multiply => a.checked_mul(&b), + Divide if !b.is_zero() => a.div_rem(&b).map(|(q, _)| q), + Remainder if !b.is_zero() => a.div_rem(&b).map(|(_, r)| r), + ShiftLeft => b.to_usize().and_then(|n| a.checked_shl(n)), + ShiftRight => b.to_usize().map(|n| a.shifted_right(n)), + BitwiseAnd => a.bitand(&b), + BitwiseOr => a.bitor(&b), + BitwiseXor => a.bitxor(&b), + Equal => Some(ConstInt::from_u64((a == b) as u64)), + NotEqual => Some(ConstInt::from_u64((a != b) as u64)), + Less => Some(ConstInt::from_u64((a < b) as u64)), + LessEqual => Some(ConstInt::from_u64((a <= b) as u64)), + Greater => Some(ConstInt::from_u64((a > b) as u64)), + GreaterEqual => Some(ConstInt::from_u64((a >= b) as u64)), + LogicalAnd => { + Some(ConstInt::from_u64((!a.is_zero() && !b.is_zero()) as u64)) + } + LogicalOr => { + Some(ConstInt::from_u64((!a.is_zero() || !b.is_zero()) as u64)) + } + _ => None, + }; + n.map(Number::Int) + } + (Some(Number::Float(a)), Some(Number::Float(b))) => { + use Operator::*; + let single = fact.computation_type == Some(WaveType::Float(32)); + let result = match operator { + Add => Some(if single { + ((a as f32) + (b as f32)) as f64 + } else { + a + b + }), + Subtract => Some(if single { + ((a as f32) - (b as f32)) as f64 + } else { + a - b + }), + Multiply => Some(if single { + ((a as f32) * (b as f32)) as f64 + } else { + a * b + }), + Divide => Some(if single { + ((a as f32) / (b as f32)) as f64 + } else { + a / b + }), + Remainder => Some(if single { + ((a as f32) % (b as f32)) as f64 + } else { + a % b + }), + _ => None, + }; + result.map(Number::Float) + } + _ => None, + } + } + _ => None, + }; + let Some(mut value) = value else { + return Ok(None); + }; + let Some(fact) = fact else { + return Ok(Some(value)); + }; + value = value.normalize(&fact.evaluation_type); + for step in &fact.conversions { + use ConversionKind::*; + value = match step.kind { + IntegerToBool | FloatToBool => Number::Int(ConstInt::from_u64((value.truth()) as u64)), + SignedToFloat | UnsignedToFloat => { + let Number::Int(n) = value else { return Ok(None); }; + Number::Float(if step.target_type == WaveType::Float(32) { n.to_f32() as f64 } else { n.to_f64() }) + } + FloatToSigned | FloatToUnsigned => { + let Number::Float(n) = value else { return Ok(None); }; + let integer = ConstInt::from_f64(n.trunc()).ok_or_else(|| self.fail(expr, "invalid float-to-integer conversion: NaN or infinity"))?; + if !float_integer_range(&step.source_type, &step.target_type).contains(n) { + return Err(self.fail(expr, "float-to-integer conversion is out of range after truncation toward zero")); + } + Number::Int(integer) + } + PointerCast | PointerToInteger | IntegerToPointer => return Ok(None), + _ => value, + }.normalize(&step.target_type); + } + Ok(Some(value)) + } + fn inspect(&mut self, expr: &Expression, scope: &Scope) -> Result<(), Failure> { + let mut result = Ok(()); + super::walk_expression(expr, &mut |inner| { + if result.is_ok() { + result = self.eval(inner, scope).map(|_| ()); + } + }); + result + } + fn function(&mut self, f: &FunctionNode) -> Result<(), Failure> { + let mut scope = Scope::new(); + for p in &f.parameters { + if let Some(default) = &p.initial_value { + self.inspect(default, &scope)?; + } + scope.insert(p.name.clone(), None); + } + self.nodes(&f.body, &mut scope) + } + fn nodes(&mut self, nodes: &[ASTNode], scope: &mut Scope) -> Result<(), Failure> { + for node in nodes { + self.node(node, scope)?; + } + Ok(()) + } + fn node(&mut self, node: &ASTNode, scope: &mut Scope) -> Result<(), Failure> { + match node { + ASTNode::Located { value, .. } => self.node(value, scope)?, + ASTNode::Function(f) => self.function(f)?, + ASTNode::Struct(s) => { + for f in &s.methods { + self.function(f)?; + } + } + ASTNode::ProtoImpl(p) => { + for f in &p.methods { + self.function(f)?; + } + } + ASTNode::Variable(v) => { + let mut value = None; + if let Some(expr) = &v.initial_value { + self.inspect(expr, scope)?; + if v.mutability == Mutability::Const { + value = self.eval(expr, scope)?; + } + } + scope.insert(v.name.clone(), value); + } + ASTNode::Statement(StatementNode::If { + condition, + body, + else_if_blocks, + else_block, + }) => { + self.inspect(condition, scope)?; + self.nodes(body, &mut scope.clone())?; + if let Some(blocks) = else_if_blocks { + for (c, b) in blocks.iter() { + self.inspect(c, scope)?; + self.nodes(b, &mut scope.clone())?; + } + } + if let Some(b) = else_block { + self.nodes(b, &mut scope.clone())?; + } + } + ASTNode::Statement(StatementNode::While { condition, body }) => { + self.inspect(condition, scope)?; + self.nodes(body, &mut scope.clone())?; + } + ASTNode::Statement(StatementNode::For { + initialization, + condition, + increment, + body, + }) => { + let mut scope = scope.clone(); + self.node(initialization, &mut scope)?; + self.inspect(condition, &scope)?; + self.inspect(increment, &scope)?; + self.nodes(body, &mut scope)?; + } + ASTNode::Statement(StatementNode::Match { value, arms }) => { + self.inspect(value, scope)?; + for arm in arms { + let mut scope = scope.clone(); + super::walk_pattern(&arm.pattern, &mut |p| { + if let MatchPattern::Binding(name) = p { + scope.insert(name.clone(), None); + } + }); + self.nodes(&arm.body, &mut scope)?; + } + } + _ => { + let mut result = Ok(()); + super::walk_node(node, &mut |expr| { + if result.is_ok() { + result = self.eval(expr, scope).map(|_| ()); + } + }); + result?; + } + } + Ok(()) + } +} +pub(super) fn validate(program: &TypedProgram) -> Result<(), SemanticDiagnostic> { + let diagnostic = |index, (id, message): Failure| SemanticDiagnostic { + code: "E3001".into(), + message: message.clone(), + top_level_index: index, + primary: None, + span: program.expression_span(id).cloned(), + label: message, + note: None, + help: "use a valid shift count or a finite value within the destination integer range" + .into(), + }; + let definitions: HashMap<_, _> = program + .syntax() + .iter() + .enumerate() + .filter_map(|(i, node)| match node { + ASTNode::Variable(v) if v.mutability == Mutability::Const => { + v.initial_value.as_ref().map(|e| (v.name.clone(), (i, e))) + } + _ => None, + }) + .collect(); + let mut checker = Checker { + program, + globals: Scope::new(), + }; + for node in program.syntax() { + if let ASTNode::Enum(e) = node { + let mut next = ConstInt::zero(); + for variant in &e.variants { + if let Some(raw) = &variant.explicit_value { + let parsed = lexer::number::IntegerLiteral::parse(raw).unwrap(); + next = ConstInt::from_digits(&parsed.digits, parsed.radix) + .expect("validated enum integer"); + if parsed.negative { + next = next.negated(); + } + } + checker.globals.insert( + variant.name.clone(), + Some(Number::Int(next.clone()).normalize(&e.repr_type)), + ); + next = next + .checked_add(&ConstInt::from_u64(1)) + .expect("enum value fits constant storage"); + } + } + } + // Constant dependency cycles are rejected by semantic validation. Evaluate + // this DAG iteratively so long forward-reference chains do not use the stack. + let mut visited = HashSet::new(); + for root in definitions.keys() { + let mut stack = vec![(root.clone(), false)]; + while let Some((name, ready)) = stack.pop() { + let &(index, expr) = &definitions[&name]; + if ready { + let value = checker + .eval(expr, &Scope::new()) + .map_err(|e| diagnostic(index, e))?; + checker.globals.insert(name, value); + } else if visited.insert(name.clone()) { + stack.push((name, true)); + super::walk_expression(expr, &mut |e| { + if let Expression::Variable(name) = e { + if definitions.contains_key(name) { + stack.push((name.clone(), false)); + } + } + }); + } + } + } + for (index, node) in program.syntax().iter().enumerate() { + checker + .node(node, &mut Scope::new()) + .map_err(|e| diagnostic(index, e))?; + } + Ok(()) +} diff --git a/front/parser/tests/alpha_frontend.rs b/front/hir/tests/alpha_frontend.rs similarity index 99% rename from front/parser/tests/alpha_frontend.rs rename to front/hir/tests/alpha_frontend.rs index d24d4878..c18755a9 100644 --- a/front/parser/tests/alpha_frontend.rs +++ b/front/hir/tests/alpha_frontend.rs @@ -1,8 +1,8 @@ //! Parser regressions: malformed source must never be silently accepted. +use hir::TypedProgram; 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; @@ -142,8 +142,8 @@ fn target_filter_removes_complete_multiline_declarations() { #[test] fn target_sized_types_resolve_recursively_without_host_assumptions() { + use hir::resolve_target_types; 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()), diff --git a/front/parser/tests/async_frontend.rs b/front/hir/tests/async_frontend.rs similarity index 87% rename from front/parser/tests/async_frontend.rs rename to front/hir/tests/async_frontend.rs index a077095f..616caf64 100644 --- a/front/parser/tests/async_frontend.rs +++ b/front/hir/tests/async_frontend.rs @@ -1,8 +1,8 @@ +use hir::TypedProgram; use lexer::Lexer; use parser::{ ast::{ASTNode, WaveType}, generics::monomorphize_generics, - hir::TypedProgram, parse_syntax_with_spans, verification::validate_program, }; @@ -36,13 +36,13 @@ fn async_and_await_preserve_types_spans_and_specialization() { } }) .unwrap(); - let plan = parser::async_lower::plan(&program, function).unwrap(); + let plan = hir::async_lower::plan(&program, function).unwrap(); assert!(plan .states .iter() - .any(|s| matches!(s.transition, parser::async_lower::Transition::Await { .. }))); + .any(|s| matches!(s.transition, hir::async_lower::Transition::Await { .. }))); // The ordinary output is itself type checked before any backend consumes it. - TypedProgram::lower(parser::async_lower::lower_program(&program).unwrap()).unwrap(); + TypedProgram::lower(hir::async_lower::lower_program(&program).unwrap()).unwrap(); } #[test] fn rejects_invalid_async_declarations_and_operands() { @@ -82,11 +82,11 @@ fn await_is_not_an_lvalue_and_short_circuit_has_distinct_states() { _ => None, }) .unwrap(); - let plan = parser::async_lower::plan(&program, f).unwrap(); + let plan = hir::async_lower::plan(&program, f).unwrap(); assert!(plan .states .iter() - .any(|s| matches!(s.transition, parser::async_lower::Transition::Branch(..)))); + .any(|s| matches!(s.transition, hir::async_lower::Transition::Branch(..)))); let bad = syntax( "async fun f()->i32{return 1;} async fun g(){var p:ptr = &(await f());} fun main(){}", ); diff --git a/front/parser/tests/generic_methods.rs b/front/hir/tests/generic_methods.rs similarity index 98% rename from front/parser/tests/generic_methods.rs rename to front/hir/tests/generic_methods.rs index 2b8a3b71..3f1627ed 100644 --- a/front/parser/tests/generic_methods.rs +++ b/front/hir/tests/generic_methods.rs @@ -1,6 +1,6 @@ +use hir::TypedProgram; use lexer::Lexer; use parser::generics::monomorphize_generics; -use parser::hir::TypedProgram; use parser::{ast::ASTNode, parse_syntax_with_spans}; fn specialize(source: &str) -> Result, String> { @@ -75,8 +75,8 @@ fn recursive_generic_methods_reuse_instances_and_expanding_recursion_is_rejected #[test] fn nested_method_receivers_have_resolved_hir_types() { + use hir::HirExpressionType; use parser::ast::{Expression, WaveType}; - use parser::hir::HirExpressionType; let ast = specialize( r#" struct S { diff --git a/front/parser/tests/io_match_contracts.rs b/front/hir/tests/io_match_contracts.rs similarity index 97% rename from front/parser/tests/io_match_contracts.rs rename to front/hir/tests/io_match_contracts.rs index 571a6f61..8243a4e0 100644 --- a/front/parser/tests/io_match_contracts.rs +++ b/front/hir/tests/io_match_contracts.rs @@ -1,5 +1,6 @@ +use hir::TypedProgram; use lexer::Lexer; -use parser::{generics::monomorphize_generics, hir::TypedProgram, parse_syntax_with_spans}; +use parser::{generics::monomorphize_generics, parse_syntax_with_spans}; fn validate(source: &str) -> Result { let tokens = Lexer::new_with_file(source, "contracts.wave") diff --git a/front/parser/tests/parse_var_and_generics.rs b/front/hir/tests/parse_var_and_generics.rs similarity index 99% rename from front/parser/tests/parse_var_and_generics.rs rename to front/hir/tests/parse_var_and_generics.rs index f9614ab4..b072662b 100644 --- a/front/parser/tests/parse_var_and_generics.rs +++ b/front/hir/tests/parse_var_and_generics.rs @@ -1,9 +1,9 @@ //! Regression coverage for variable declarations and generic type syntax. +use hir::TypedProgram; use lexer::Lexer; use parser::ast::{ASTNode, Expression, StatementNode, Visibility, WaveType}; use parser::generics::monomorphize_generics; -use parser::hir::TypedProgram; use parser::parse_syntax_only; fn parse_ok(src: &str) { diff --git a/front/parser/tests/phase1_frontend.rs b/front/hir/tests/phase1_frontend.rs similarity index 99% rename from front/parser/tests/phase1_frontend.rs rename to front/hir/tests/phase1_frontend.rs index 39eb7f4f..2bd32d98 100644 --- a/front/parser/tests/phase1_frontend.rs +++ b/front/hir/tests/phase1_frontend.rs @@ -1,7 +1,7 @@ +use hir::TypedProgram; use lexer::{token::TokenType, Lexer}; use parser::{ ast::WaveType, - hir::TypedProgram, parse_syntax_with_spans, parser::types::{parse_type, split_top_level_generic_args, token_type_to_wave_type}, }; diff --git a/front/parser/tests/source_spans.rs b/front/hir/tests/source_spans.rs similarity index 99% rename from front/parser/tests/source_spans.rs rename to front/hir/tests/source_spans.rs index 79c6c078..e02061d8 100644 --- a/front/parser/tests/source_spans.rs +++ b/front/hir/tests/source_spans.rs @@ -1,8 +1,8 @@ //! Source occurrence identity must survive frontend lowering without text searches. +use hir::TypedProgram; 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; diff --git a/front/parser/tests/typed_hir.rs b/front/hir/tests/typed_hir.rs similarity index 98% rename from front/parser/tests/typed_hir.rs rename to front/hir/tests/typed_hir.rs index fd408fa8..7afeda42 100644 --- a/front/parser/tests/typed_hir.rs +++ b/front/hir/tests/typed_hir.rs @@ -1,8 +1,8 @@ //! Contracts for the backend-neutral typed frontend boundary. +use hir::{HirExpressionType, TypedProgram}; use lexer::Lexer; use parser::ast::{ASTNode, Expression, WaveType}; -use parser::hir::{HirExpressionType, TypedProgram}; use parser::parse_syntax_only; fn lower(source: &str) -> TypedProgram { @@ -218,6 +218,6 @@ fun check(value: u8) { .unwrap() .conversions[0] .kind, - parser::hir::conversions::ConversionKind::ZeroExtend + hir::conversions::ConversionKind::ZeroExtend ); } diff --git a/front/parser/tests/variant_frontend.rs b/front/hir/tests/variant_frontend.rs similarity index 99% rename from front/parser/tests/variant_frontend.rs rename to front/hir/tests/variant_frontend.rs index 5bb18d0f..4f4f4406 100644 --- a/front/parser/tests/variant_frontend.rs +++ b/front/hir/tests/variant_frontend.rs @@ -1,9 +1,9 @@ //! Frontend contracts for payload variants and variant matching. +use hir::{HirExpressionType, TypedProgram}; use lexer::Lexer; use parser::ast::{ASTNode, MatchPattern, StatementNode, WaveType}; use parser::generics::monomorphize_generics; -use parser::hir::{HirExpressionType, TypedProgram}; use parser::parse_syntax_only; fn syntax(source: &str) -> Vec { diff --git a/front/parser/Cargo.toml b/front/parser/Cargo.toml index 985c56e4..6c75e73e 100644 --- a/front/parser/Cargo.toml +++ b/front/parser/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" [dependencies] lexer = { path = "../lexer" } error = { path = "../error" } -utils = { path = "../../utils" } \ No newline at end of file +utils = { path = "../../utils" } diff --git a/front/parser/src/ast.rs b/front/parser/src/ast.rs index 84e1d2d1..996ade34 100644 --- a/front/parser/src/ast.rs +++ b/front/parser/src/ast.rs @@ -17,6 +17,8 @@ //! node forms must be handled by both semantic passes and that rewrite where //! they may contain types or expressions. +pub mod visit; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum WaveType { /// Target-sized integers remain symbolic until the target-resolution pass. diff --git a/front/parser/src/ast/visit.rs b/front/parser/src/ast/visit.rs new file mode 100644 index 00000000..aa507653 --- /dev/null +++ b/front/parser/src/ast/visit.rs @@ -0,0 +1,200 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// +// This Source Code Form is subject to the terms of the +// Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// +// SPDX-License-Identifier: MPL-2.0 +// AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. + +//! Read-only expression traversal shared by semantic analysis and HIR. +use super::{ASTNode, Expression, StatementNode}; + +pub fn walk_nodes(nodes: &[ASTNode], visit: &mut impl FnMut(&Expression)) { + for node in nodes { + walk_node(node, visit); + } +} + +pub fn walk_node(node: &ASTNode, visit: &mut impl FnMut(&Expression)) { + match node { + 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 { + for parameter in &method.parameters { + if let Some(default) = ¶meter.initial_value { + walk_expression(default, visit); + } + } + walk_nodes(&method.body, visit); + } + } + ASTNode::ProtoImpl(implementation) => { + for method in &implementation.methods { + for parameter in &method.parameters { + if let Some(default) = ¶meter.initial_value { + walk_expression(default, visit); + } + } + walk_nodes(&method.body, visit); + } + } + ASTNode::Statement(statement) => walk_statement(statement, visit), + ASTNode::Variable(variable) => { + if let Some(initializer) = &variable.initial_value { + walk_expression(initializer, visit); + } + } + ASTNode::Expression(expression) => walk_expression(expression, visit), + ASTNode::ExternFunction(_) + | ASTNode::Program(_) + | ASTNode::TypeAlias(_) + | ASTNode::Enum(_) + | ASTNode::Variant(_) => {} + } +} + +fn walk_statement(statement: &StatementNode, visit: &mut impl FnMut(&Expression)) { + match statement { + StatementNode::PrintFormat { args, .. } + | StatementNode::PrintlnFormat { args, .. } + | StatementNode::Input { args, .. } => { + for argument in args { + walk_expression(argument, visit); + } + } + StatementNode::If { + condition, + body, + else_if_blocks, + else_block, + } => { + walk_expression(condition, visit); + walk_nodes(body, visit); + if let Some(blocks) = else_if_blocks { + for (condition, body) in blocks.iter() { + walk_expression(condition, visit); + walk_nodes(body, visit); + } + } + if let Some(body) = else_block { + walk_nodes(body, visit); + } + } + StatementNode::For { + initialization, + condition, + increment, + body, + } => { + walk_node(initialization, visit); + walk_expression(condition, visit); + walk_expression(increment, visit); + walk_nodes(body, visit); + } + StatementNode::While { condition, body } => { + walk_expression(condition, visit); + walk_nodes(body, visit); + } + StatementNode::Match { value, arms } => { + walk_expression(value, visit); + for arm in arms { + walk_nodes(&arm.body, visit); + } + } + StatementNode::Assign { value, .. } => walk_expression(value, visit), + StatementNode::AsmBlock { + inputs, outputs, .. + } => { + for (_, expression) in inputs.iter().chain(outputs.iter()) { + walk_expression(expression, visit); + } + } + StatementNode::Return(Some(expression)) | StatementNode::Expression(expression) => { + walk_expression(expression, visit) + } + StatementNode::Print(_) + | StatementNode::Println(_) + | StatementNode::Variable(_) + | StatementNode::Import(_) + | StatementNode::Break + | StatementNode::Continue + | StatementNode::Return(None) => {} + } +} + +pub fn walk_expression(expression: &Expression, visit: &mut impl FnMut(&Expression)) { + if let Expression::Located { value, .. } = expression { + walk_expression(value, visit); + return; + } + visit(expression); + match expression { + Expression::Located { value, .. } => walk_expression(value, visit), + Expression::StructLiteral { fields, .. } => { + for (_, value) in fields { + walk_expression(value, visit); + } + } + Expression::FunctionCall { args, .. } => { + for argument in args { + walk_expression(argument, visit); + } + } + Expression::MethodCall { object, args, .. } => { + walk_expression(object, visit); + for argument in args { + walk_expression(argument, visit); + } + } + Expression::Deref(inner) + | Expression::AddressOf(inner) + | Expression::Await(inner) + | Expression::Grouped(inner) + | Expression::Unary { expr: inner, .. } + | Expression::Cast { expr: inner, .. } + | Expression::FieldAccess { object: inner, .. } + | Expression::IncDec { target: inner, .. } => walk_expression(inner, visit), + Expression::BinaryExpression { left, right, .. } + | Expression::IndexAccess { + target: left, + index: right, + } + | Expression::AssignOperation { + target: left, + value: right, + .. + } + | Expression::Assignment { + target: left, + value: right, + } => { + walk_expression(left, visit); + walk_expression(right, visit); + } + Expression::ArrayLiteral(values) => { + for value in values { + walk_expression(value, visit); + } + } + Expression::AsmBlock { + inputs, outputs, .. + } => { + for (_, expression) in inputs.iter().chain(outputs.iter()) { + walk_expression(expression, visit); + } + } + Expression::Null | Expression::Literal(_) | Expression::Variable(_) => {} + } +} diff --git a/front/parser/src/lib.rs b/front/parser/src/lib.rs index 72738ad6..668b1c9b 100644 --- a/front/parser/src/lib.rs +++ b/front/parser/src/lib.rs @@ -13,9 +13,9 @@ //! Wave syntax, AST, import expansion, generic specialization, and semantic validation. //! //! Parsing intentionally produces a source-oriented AST first. Imports and -//! generics are expanded before the semantic verifier establishes a -//! backend-neutral [`hir::TypedProgram`]. Backend lowering consumes that typed -//! boundary directly and queries semantic facts through stable HIR identities. +//! generics are expanded before the semantic verifier establishes +//! semantic facts consumed by the separate `hir` crate. That crate builds the +//! typed program and stable identities used by backend lowering. // These legacy parser APIs are being migrated incrementally; keep new lints fatal // without forcing risky mechanical rewrites into a release hardening change. @@ -43,11 +43,9 @@ macro_rules! println { pub mod arch; pub mod ast; pub mod async_intrinsics; -pub mod async_lower; pub mod expr; pub mod format; pub mod generics; -pub mod hir; pub mod import; pub mod layout_intrinsics; pub mod os; diff --git a/front/parser/src/parser/control.rs b/front/parser/src/parser/control.rs index c8bec249..7191982a 100644 --- a/front/parser/src/parser/control.rs +++ b/front/parser/src/parser/control.rs @@ -56,6 +56,25 @@ fn parse_match_pattern( let before = tokens.clone(); let anchor = tokens.peek().copied(); let value = match anchor.map(|t| &t.token_type) { + Some(TokenType::Minus) => { + tokens.next(); + skip_ws_and_newlines(tokens); + let token = tokens.peek().copied(); + let Some(Token { + token_type: TokenType::IntLiteral(value), + .. + }) = token + else { + return Err(ParseError::expected_at( + token, + anchor, + "integer literal after '-'", + "match pattern", + )); + }; + tokens.next(); + MatchPattern::Int(format!("-{value}")) + } Some(TokenType::IntLiteral(value)) => { tokens.next(); MatchPattern::Int(value.clone()) diff --git a/front/parser/src/parser/functions.rs b/front/parser/src/parser/functions.rs index 6b17fbf3..5b913c0c 100644 --- a/front/parser/src/parser/functions.rs +++ b/front/parser/src/parser/functions.rs @@ -18,7 +18,6 @@ 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 crate::parser::ParseError; use lexer::token::TokenType; use lexer::Token; @@ -87,107 +86,68 @@ pub fn parse_parameters( tokens: &mut Peekable>, ) -> Result, ParseError> { let anchor = tokens.peek().copied(); - let invalid = |token| { - ParseError::expected_at( - token, - anchor, - "valid function parameters", - "function parameters", - ) - }; - let mut params = vec![]; + let context = "function parameters"; + let mut params = Vec::new(); + let mut names = HashSet::new(); loop { skip_ws(tokens); - if tokens .peek() - .map_or(false, |t| t.token_type == TokenType::Rparen) + .is_some_and(|t| t.token_type == TokenType::Rparen) { + tokens.next(); break; } - let before = tokens.clone(); - let name = if let Some(Token { - token_type: TokenType::Identifier(n), - .. - }) = tokens.peek().copied() - { - let name = n.clone(); - tokens.next(); - name - } else { - println!("Error: Expected parameter name"); - return Err(invalid(tokens.peek().copied())); - }; - - skip_ws(tokens); - if tokens - .peek() - .map_or(true, |t| t.token_type != TokenType::Colon) - { - println!("Error: Expected ':' after parameter name '{}'", name); - return Err(invalid(tokens.peek().copied())); + let at = tokens.peek().copied(); + let name = crate::expr::identifier(tokens, anchor, context)?; + if !names.insert(name.clone()) { + return Err( + ParseError::syntax_at(at, format!("duplicate parameter '{name}'")) + .with_context(context), + ); } - tokens.next(); - - let param_type = match parse_type_from_stream(tokens) { - Some(pt) => pt, - None => { - println!("Error: Failed to parse type for parameter '{}'", name); - return Err(invalid(tokens.peek().copied())); - } - }; - + crate::expr::expect_token(tokens, anchor, TokenType::Colon, "':'", context)?; + skip_ws(tokens); + let param_type = crate::types::parse_type_checked(tokens, "parameter type")?; + skip_ws(tokens); let initial_value = if tokens .peek() .is_some_and(|t| t.token_type == TokenType::Equal) { tokens.next(); + skip_ws(tokens); + let at = tokens.peek().copied(); let value = crate::expr::parse_expression(tokens)?; if !matches!(value.unspanned(), Expression::Literal(_) | Expression::Null) { - return Err(invalid(tokens.peek().copied())); + return Err(ParseError::expected_at( + at, + anchor, + "literal default value", + context, + )); } Some(value) } else { None }; - params.push(ParameterNode { span: lexer::consumed_span(before, tokens), name, param_type, initial_value, }); - skip_ws(tokens); - match tokens.peek().map(|t| &t.token_type) { - Some(TokenType::Comma) => { - tokens.next(); // consume ',' - } - Some(TokenType::SemiColon) => { - println!("Error: use `,` instead of `;` to separate parameters"); - return Err(invalid(tokens.peek().copied())); - } - Some(TokenType::Rparen) => { - // loop end - } - _ => { - println!("Error: Expected ',' or ')' after parameter"); - return Err(invalid(tokens.peek().copied())); - } + if tokens + .peek() + .is_some_and(|t| t.token_type == TokenType::Comma) + { + tokens.next(); + } else { + crate::expr::expect_token(tokens, anchor, TokenType::Rparen, "',' or ')'", context)?; + break; } } - - if tokens - .peek() - .map_or(true, |t| t.token_type != TokenType::Rparen) - { - println!("Error: Expected ')' or ',' in parameter list"); - return Err(invalid(tokens.peek().copied())); - } else { - tokens.next(); - } - Ok(params) } @@ -200,8 +160,8 @@ pub fn parse_function_with_export( export: Option, ) -> Result { let anchor = tokens.peek().copied(); - let invalid = |token| { - ParseError::syntax_at(anchor, "failed to parse function declaration") + let invalid = |token: Option<&Token>| { + ParseError::syntax_at(token.or(anchor), "failed to parse function declaration") .with_context("top-level function") .with_expected_many([ "fun name(params) { ... }", @@ -230,13 +190,7 @@ pub fn parse_function_with_export( skip_ws(tokens); - let name = match tokens.next() { - Some(Token { - token_type: TokenType::Identifier(name), - .. - }) => name.clone(), - _ => return Err(invalid(tokens.peek().copied())), - }; + let name = crate::expr::identifier(tokens, anchor, "function name")?; if is_async && (name == "main" || export.is_some()) { return Err(ParseError::syntax_at( @@ -259,17 +213,6 @@ pub fn parse_function_with_export( tokens.next(); // consume '(' let parameters = parse_parameters(tokens)?; - let mut param_names = HashSet::new(); - for param in ¶meters { - if !param_names.insert(param.name.clone()) { - println!( - "Error: Parameter '{}' is declared multiple times", - param.name - ); - return Err(invalid(tokens.peek().copied())); - } - } - skip_ws(tokens); let mut return_type_span = None; let return_type = if let Some(Token { @@ -279,7 +222,8 @@ pub fn parse_function_with_export( { tokens.next(); // consume '->' let before_type = tokens.clone(); - let ty = parse_type_from_stream(tokens).ok_or_else(|| invalid(tokens.peek().copied()))?; + skip_ws(tokens); + let ty = crate::types::parse_type_checked(tokens, "function return type")?; return_type_span = lexer::consumed_span(before_type, tokens); Some(ty) } else { diff --git a/front/parser/src/parser/items.rs b/front/parser/src/parser/items.rs index 8b0ed47d..25bae368 100644 --- a/front/parser/src/parser/items.rs +++ b/front/parser/src/parser/items.rs @@ -21,7 +21,6 @@ use crate::ast::{ }; use crate::parser::functions::{parse_function, parse_generic_param_names}; use crate::parser::ParseError; -use crate::types::parse_type_from_stream; use lexer::token::TokenType; use lexer::Token; use std::iter::Peekable; @@ -38,109 +37,79 @@ fn skip_ws(tokens: &mut Peekable>) { } } -pub fn parse_import(tokens: &mut Peekable>) -> Option { - if tokens.peek()?.token_type != TokenType::Lparen { - println!("Error: Expected '(' after 'import'"); - return None; - } - tokens.next(); - - let import_path = match tokens.next() { - Some(Token { - token_type: TokenType::String(s), - .. - }) => String::from_utf8(s.clone()).ok()?, - other => { - println!( - "Error: Expected string literal in import, found {:?}", - other - ); - return None; - } +pub fn parse_import(tokens: &mut Peekable>) -> Result { + let anchor = tokens.peek().copied(); + let context = "import declaration"; + crate::expr::expect_token(tokens, anchor, TokenType::Lparen, "'('", context)?; + skip_ws(tokens); + let path_token = tokens.peek().copied(); + let Some(Token { + token_type: TokenType::String(bytes), + .. + }) = path_token + else { + return Err(ParseError::expected_at( + path_token, + anchor, + "string literal", + context, + )); }; - - let alias = if tokens.peek()?.token_type == TokenType::As { + let import_path = String::from_utf8(bytes.clone()).map_err(|_| { + ParseError::syntax_at(path_token, "import path must be valid UTF-8").with_context(context) + })?; + tokens.next(); + skip_ws(tokens); + let alias = if tokens.peek().is_some_and(|t| t.token_type == TokenType::As) { tokens.next(); - match tokens.next() { - Some(Token { - token_type: TokenType::Identifier(name), - .. - }) => Some(name.clone()), - other => { - println!("Error: Expected import alias after 'as', found {:?}", other); - return None; - } - } + Some(crate::expr::identifier(tokens, anchor, "import alias")?) } else { None }; - - if tokens.peek()?.token_type != TokenType::Rparen { - println!("Error: Expected ')' after 'import' condition"); - return None; - } - tokens.next(); - + crate::expr::expect_token(tokens, anchor, TokenType::Rparen, "')'", context)?; + skip_ws(tokens); let mut selections = Vec::new(); - if tokens.peek()?.token_type == TokenType::DoubleColon { + if tokens + .peek() + .is_some_and(|t| t.token_type == TokenType::DoubleColon) + { if alias.is_some() { - println!("Error: Import aliases cannot be combined with selective imports"); - return None; + return Err(ParseError::syntax_at( + tokens.peek().copied(), + "import aliases cannot be combined with selective imports", + ) + .with_context(context)); } tokens.next(); - if tokens.next()?.token_type != TokenType::Lbrace { - println!("Error: Expected '{{' after '::' in selective import"); - return None; - } + crate::expr::expect_token(tokens, anchor, TokenType::Lbrace, "'{'", "selective import")?; loop { - match tokens.next() { - Some(Token { - token_type: TokenType::Identifier(name), - .. - }) => selections.push(name.clone()), - Some(Token { - token_type: TokenType::Rbrace, - .. - }) if !selections.is_empty() => break, - other => { - println!( - "Error: Expected symbol name in selective import, found {:?}", - other - ); - return None; - } - } - match tokens.next() { - Some(Token { - token_type: TokenType::Comma, - .. - }) => { - if tokens.peek()?.token_type == TokenType::Rbrace { - tokens.next(); - break; - } - } - Some(Token { - token_type: TokenType::Rbrace, - .. - }) => break, - other => { - println!( - "Error: Expected ',' or '}}' in selective import, found {:?}", - other - ); - return None; + selections.push(crate::expr::identifier(tokens, anchor, "selective import")?); + skip_ws(tokens); + if tokens + .peek() + .is_some_and(|t| t.token_type == TokenType::Comma) + { + tokens.next(); + skip_ws(tokens); + if !tokens + .peek() + .is_some_and(|t| t.token_type == TokenType::Rbrace) + { + continue; } } + crate::expr::expect_token( + tokens, + anchor, + TokenType::Rbrace, + "',' or '}'", + "selective import", + )?; + break; } } - - if tokens.next()?.token_type != TokenType::SemiColon { - println!("Error: Expected ';' after import declaration"); - return None; - } - - Some(ASTNode::Statement(StatementNode::Import(ImportNode { + crate::expr::expect_token(tokens, anchor, TokenType::SemiColon, "';'", context)?; + Ok(ASTNode::Statement(StatementNode::Import(ImportNode { path: import_path, alias, selections, @@ -233,172 +202,60 @@ pub fn parse_proto(tokens: &mut Peekable>) -> Result>) -> Result { let anchor = tokens.peek().copied(); - let invalid = |token| { - ParseError::syntax_at(anchor, "failed to parse struct declaration") - .with_context("top-level struct declaration") - .with_expected("struct Name { field: type; fun method(...) { ... } }") - .with_found_token(token) - .with_help("check field separators (`;`) and method bodies") - }; - let name = match tokens.next() { - Some(Token { - token_type: TokenType::Identifier(name), - .. - }) => name.clone(), - _ => { - println!("Error: Expected struct name after 'struct' keyword."); - return Err(invalid(tokens.peek().copied())); - } - }; - + let name = crate::expr::identifier(tokens, anchor, "struct name")?; let generic_params = parse_generic_param_names(tokens)?; - - if tokens - .peek() - .map_or(true, |t| t.token_type != TokenType::Lbrace) - { - println!("Error: Expected '{{' after struct name '{}'.", name); - return Err(invalid(tokens.peek().copied())); - } - tokens.next(); - + crate::expr::expect_token( + tokens, + anchor, + TokenType::Lbrace, + "'{'", + "struct declaration", + )?; let mut fields = Vec::new(); let mut field_spans = Vec::new(); let mut methods = Vec::new(); - loop { skip_ws(tokens); - - let token_type = if let Some(t) = tokens.peek() { - t.token_type.clone() - } else { - println!( - "Error: Unexpected end of file inside struct '{}' definition.", - name - ); - return Err(invalid(tokens.peek().copied())); - }; - - match token_type { - TokenType::Rbrace => { + match tokens.peek().map(|t| &t.token_type) { + Some(TokenType::Rbrace) => { tokens.next(); break; } - - TokenType::Whitespace | TokenType::Newline => { - tokens.next(); - } - - TokenType::Fun | TokenType::Async => { - if let ASTNode::Function(func_node) = parse_function(tokens)? { - if func_node.return_type.is_none() { - let mut func_node_with_return = func_node.clone(); - func_node_with_return.return_type = Some(WaveType::Void); - methods.push(func_node_with_return); - } else { - methods.push(func_node); - } - } else { - println!("Error: Failed to parse method inside struct '{}'.", name); - return Err(invalid(tokens.peek().copied())); + Some(TokenType::Fun | TokenType::Async) => { + let ASTNode::Function(mut method) = parse_function(tokens)? else { + unreachable!() + }; + if method.return_type.is_none() { + method.return_type = Some(WaveType::Void); } + methods.push(method); } - TokenType::Identifier(_) => { - let mut lookahead = tokens.clone(); - lookahead.next(); - while let Some(t) = lookahead.peek() { - match t.token_type { - TokenType::Whitespace | TokenType::Newline => { - lookahead.next(); - } - _ => break, - } - } - - if matches!( - 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), - .. - }) = tokens.next() - { - n.clone() - } else { - unreachable!() - }; - - skip_ws(tokens); - - // ':' - if tokens - .peek() - .map_or(true, |t| t.token_type != TokenType::Colon) - { - println!( - "Error: Expected ':' after field '{}' in struct '{}'.", - field_name, name - ); - return Err(invalid(tokens.peek().copied())); - } - tokens.next(); // consume ':' - - skip_ws(tokens); - - let wave_type = match parse_type_from_stream(tokens) { - Some(t) => t, - None => { - println!( - "Error: Invalid type for field '{}' in struct '{}'.", - field_name, name - ); - return Err(invalid(tokens.peek().copied())); - } - }; - - skip_ws(tokens); - - if tokens - .peek() - .map_or(true, |t| t.token_type != TokenType::SemiColon) - { - println!( - "Error: Expected ';' after field declaration in struct '{}'.", - name - ); - return Err(invalid(tokens.peek().copied())); - } - tokens.next(); // consume ';' - - field_spans.push(lexer::consumed_span(before, tokens)); - fields.push((field_name, wave_type)); - } else { - let id_str = - if let TokenType::Identifier(id) = &tokens.peek().unwrap().token_type { - id.clone() - } else { - "".to_string() - }; - println!( - "Error: Unexpected identifier '{}' in struct '{}' body. Expected field or method.", - id_str, name - ); - return Err(invalid(tokens.peek().copied())); - } + Some(_) => { + let before = tokens.clone(); + let field_name = crate::expr::identifier(tokens, anchor, "struct field name")?; + crate::expr::expect_token(tokens, anchor, TokenType::Colon, "':'", "struct field")?; + skip_ws(tokens); + let ty = crate::types::parse_type_checked(tokens, "struct field type")?; + crate::expr::expect_token( + tokens, + anchor, + TokenType::SemiColon, + "';'", + "struct field", + )?; + field_spans.push(lexer::consumed_span(before, tokens)); + fields.push((field_name, ty)); } - - other_token => { - println!( - "Error: Unexpected token inside struct body: {:?}", - other_token - ); - return Err(invalid(tokens.peek().copied())); + None => { + return Err(ParseError::expected_at( + None, + anchor, + "'}'", + "struct declaration", + )) } } } - Ok(ASTNode::Struct(StructNode { name, generic_params, diff --git a/front/parser/src/parser/parse.rs b/front/parser/src/parser/parse.rs index a30fceec..021bbad3 100644 --- a/front/parser/src/parser/parse.rs +++ b/front/parser/src/parser/parse.rs @@ -317,20 +317,8 @@ fn parse_syntax_impl(tokens: &[Token]) -> Result, ParseError> { continue; } TokenType::Import => { - let anchor = (*token).clone(); iter.next(); - if let Some(path) = parse_import(&mut iter) { - nodes.push(path); - } else { - return Err(ParseError::syntax_at( - Some(&anchor), - "failed to parse import declaration", - ) - .with_context("top-level import") - .with_expected("import(\"path\");") - .with_found_token(iter.peek().copied()) - .with_help("imports must use parentheses and end with ';'")); - } + nodes.push(parse_import(&mut iter)?); } TokenType::Pub => { let anchor = (*token).clone(); @@ -345,7 +333,7 @@ fn parse_syntax_impl(tokens: &[Token]) -> Result, ParseError> { let declaration = match iter.peek().map(|token| &token.token_type) { Some(TokenType::Import) => { iter.next(); - parse_import(&mut iter) + Some(parse_import(&mut iter)?) } Some(TokenType::Export) => { iter.next(); diff --git a/front/parser/src/verification.rs b/front/parser/src/verification.rs index ea3da306..9a908633 100644 --- a/front/parser/src/verification.rs +++ b/front/parser/src/verification.rs @@ -19,6 +19,8 @@ mod const_dependencies; mod diagnostics; +mod facts; +pub use facts::{AnalyzedExpressionType, SemanticFacts, VariantConstruction, VariantPattern}; mod model; mod numeric; mod program; @@ -33,7 +35,6 @@ use crate::ast::{ ASTNode, AssignOperator, Expression, FunctionNode, IncDecKind, Literal, MatchPattern, Mutability, Operator, StatementNode, WaveType, }; -use crate::hir::{HirExpressionType, HirVariantConstruction, HirVariantPattern}; use std::collections::{HashMap, HashSet}; fn infer_variant_substitution( @@ -127,11 +128,11 @@ struct Validator<'a> { source_span: Option, diagnostic_help: Option, expression_types: HashMap, - hir_expression_types: HashMap, + analyzed_expression_types: HashMap, expected_types: HashMap, - hir_variant_constructions: HashMap, - hir_variant_patterns: HashMap, - hir_integer_patterns: HashMap, + variant_constructions: HashMap, + variant_patterns: HashMap, + integer_patterns: HashMap, generic_method_calls: HashMap, } @@ -152,11 +153,11 @@ impl<'a> Validator<'a> { source_span: None, diagnostic_help: None, expression_types: HashMap::new(), - hir_expression_types: HashMap::new(), + analyzed_expression_types: HashMap::new(), expected_types: HashMap::new(), - hir_variant_constructions: HashMap::new(), - hir_variant_patterns: HashMap::new(), - hir_integer_patterns: HashMap::new(), + variant_constructions: HashMap::new(), + variant_patterns: HashMap::new(), + integer_patterns: HashMap::new(), generic_method_calls: HashMap::new(), } } @@ -664,7 +665,7 @@ impl<'a> Validator<'a> { MatchPattern::Int(raw) => { self.mark_span(SemanticSpanKind::Keyword, raw.clone()); let value = normalize(raw, &ty)?; - self.hir_integer_patterns + self.integer_patterns .insert(&arm.pattern as *const _ as usize, value.clone()); format!("value:{}", value) } @@ -689,7 +690,7 @@ impl<'a> Validator<'a> { })?; let value = normalize(value, &self.program.canonical_type(&binding.ty))?; let value = normalize(&value, &ty)?; - self.hir_integer_patterns + self.integer_patterns .insert(&arm.pattern as *const _ as usize, value.clone()); format!("value:{}", value) } @@ -892,9 +893,9 @@ impl<'a> Validator<'a> { payloads.len() )); } - self.hir_variant_patterns.insert( + self.variant_patterns.insert( pattern as *const MatchPattern as usize, - HirVariantPattern { + VariantPattern { variant_type: WaveType::Variant(expected_name.clone()), case_name: case_name.clone(), discriminant, @@ -1155,9 +1156,9 @@ impl<'a> Validator<'a> { self.program.canonical_type(expected), ); } - self.hir_expression_types.insert( + self.analyzed_expression_types.insert( expression as *const Expression as usize, - hir_expression_type(self.program, expression_type), + analyzed_expression_type(self.program, expression_type), ); if let Some(ty) = canonical_expression_type(self.program, expression_type) { self.expression_types @@ -1320,6 +1321,11 @@ impl<'a> Validator<'a> { SemanticSpanKind::Keyword, operator_source_symbol(operator).unwrap_or("binary operator"), ); + if matches!(operator, Operator::ShiftLeft | Operator::ShiftRight) { + let left_type = self.validate_expr_expected(left, expected)?; + let right_type = self.validate_expr(right)?; + return infer_binary_type(self.program, operator, left_type, right_type); + } let left_contextual = left.is_contextual_integer(); let right_contextual = right.is_contextual_integer(); let (left_type, right_type) = match (left_contextual, right_contextual) { @@ -1925,9 +1931,9 @@ impl<'a> Validator<'a> { } let variant_type = WaveType::Variant(concrete_name); - self.hir_variant_constructions.insert( + self.variant_constructions.insert( expression as *const Expression as usize, - HirVariantConstruction { + VariantConstruction { variant_type: variant_type.clone(), case_name: case_name.to_string(), discriminant, @@ -2481,27 +2487,18 @@ pub fn analyze_expression_types( .map(|analysis| analysis.expression_types) } -pub(crate) fn analyze_hir_expression_types( +/// Analyze a final AST without changing its allocations. The source map must +/// refer to these same nodes; returned facts are valid only for this allocation. +pub fn analyze_semantic_facts( nodes: &[ASTNode], sources: &crate::source::SourceMap, -) -> Result< - ( - HashMap, - HashMap, - HashMap, - HashMap, - HashMap, - ), - SemanticDiagnostic, -> { - analyze_program_types(nodes, sources).map(|analysis| { - ( - analysis.hir_expression_types, - analysis.hir_variant_constructions, - analysis.hir_variant_patterns, - analysis.expected_types, - analysis.hir_integer_patterns, - ) +) -> Result { + analyze_program_types(nodes, sources).map(|analysis| SemanticFacts { + expression_types: analysis.analyzed_expression_types, + variant_constructions: analysis.variant_constructions, + variant_patterns: analysis.variant_patterns, + expected_types: analysis.expected_types, + integer_patterns: analysis.integer_patterns, }) } @@ -2514,11 +2511,11 @@ pub(crate) fn analyze_generic_method_calls( struct ProgramAnalysis { expression_types: HashMap, - hir_expression_types: HashMap, + analyzed_expression_types: HashMap, expected_types: HashMap, - hir_variant_constructions: HashMap, - hir_variant_patterns: HashMap, - hir_integer_patterns: HashMap, + variant_constructions: HashMap, + variant_patterns: HashMap, + integer_patterns: HashMap, generic_method_calls: HashMap, } @@ -2620,11 +2617,11 @@ fn analyze_program_types( Ok(ProgramAnalysis { expression_types: validator.expression_types, - hir_expression_types: validator.hir_expression_types, + analyzed_expression_types: validator.analyzed_expression_types, expected_types: validator.expected_types, - hir_variant_constructions: validator.hir_variant_constructions, - hir_variant_patterns: validator.hir_variant_patterns, - hir_integer_patterns: validator.hir_integer_patterns, + variant_constructions: validator.variant_constructions, + variant_patterns: validator.variant_patterns, + integer_patterns: validator.integer_patterns, generic_method_calls: validator.generic_method_calls, }) } @@ -3042,14 +3039,14 @@ fn contains_future(program: &ProgramTypes, ty: &WaveType, seen: &mut HashSet HirExpressionType { +fn analyzed_expression_type(program: &ProgramTypes, ty: &ExpressionType) -> AnalyzedExpressionType { match ty { - ExpressionType::Known(ty) => HirExpressionType::Resolved(program.canonical_type(ty)), - ExpressionType::IntLiteral(_) => HirExpressionType::IntegerLiteral, - ExpressionType::FloatLiteral => HirExpressionType::FloatLiteral, - ExpressionType::Null => HirExpressionType::Null, - ExpressionType::ArrayLiteral(_) => HirExpressionType::ArrayLiteral, - ExpressionType::AddressedArrayLiteral(_) => HirExpressionType::AddressedArrayLiteral, - ExpressionType::Unknown => HirExpressionType::Unknown, + ExpressionType::Known(ty) => AnalyzedExpressionType::Resolved(program.canonical_type(ty)), + ExpressionType::IntLiteral(_) => AnalyzedExpressionType::IntegerLiteral, + ExpressionType::FloatLiteral => AnalyzedExpressionType::FloatLiteral, + ExpressionType::Null => AnalyzedExpressionType::Null, + ExpressionType::ArrayLiteral(_) => AnalyzedExpressionType::ArrayLiteral, + ExpressionType::AddressedArrayLiteral(_) => AnalyzedExpressionType::AddressedArrayLiteral, + ExpressionType::Unknown => AnalyzedExpressionType::Unknown, } } diff --git a/front/parser/src/verification/const_dependencies.rs b/front/parser/src/verification/const_dependencies.rs index 173093fa..3e78d0aa 100644 --- a/front/parser/src/verification/const_dependencies.rs +++ b/front/parser/src/verification/const_dependencies.rs @@ -25,7 +25,7 @@ pub(super) fn validate( let mut edges = vec![Vec::new(); constants.len()]; for (index, (_, variable)) in constants.iter().enumerate() { if let Some(initializer) = &variable.initial_value { - crate::hir::walk_expression(initializer, &mut |expression| { + crate::ast::visit::walk_expression(initializer, &mut |expression| { if let Expression::Variable(name) = expression { if let Some(&dependency) = names.get(name.as_str()) { edges[index].push(( diff --git a/front/parser/src/verification/facts.rs b/front/parser/src/verification/facts.rs new file mode 100644 index 00000000..2036492f --- /dev/null +++ b/front/parser/src/verification/facts.rs @@ -0,0 +1,60 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// +// This Source Code Form is subject to the terms of the +// Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// +// SPDX-License-Identifier: MPL-2.0 +// AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. + +//! Semantic facts produced from a stable, source-mapped AST. +use crate::ast::WaveType; +use std::collections::HashMap; + +/// The semantic type known before contextual lowering is performed. +/// +/// Literal forms remain explicit because their final representation can depend +/// on an assignment, argument, return, or aggregate context. They are not +/// silently committed to a backend type at this boundary. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum AnalyzedExpressionType { + Resolved(WaveType), + IntegerLiteral, + FloatLiteral, + Null, + ArrayLiteral, + AddressedArrayLiteral, + Unknown, +} + +/// Fully resolved variant constructor selected by semantic analysis. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct VariantConstruction { + pub variant_type: WaveType, + pub case_name: String, + pub discriminant: u32, + pub payload_types: Vec, +} + +/// Concrete variant case selected by a semantically validated pattern. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct VariantPattern { + pub variant_type: WaveType, + pub case_name: String, + pub discriminant: u32, + pub payload_types: Vec, +} + +/// Address-keyed facts for one analyzed AST allocation. Consumers must keep the +/// syntax allocations stable until these facts have been attached to their IR. +#[derive(Debug)] +pub struct SemanticFacts { + pub expression_types: HashMap, + pub variant_constructions: HashMap, + pub variant_patterns: HashMap, + pub expected_types: HashMap, + pub integer_patterns: HashMap, +} diff --git a/front/parser/src/verification/numeric.rs b/front/parser/src/verification/numeric.rs index c0df9956..414960d2 100644 --- a/front/parser/src/verification/numeric.rs +++ b/front/parser/src/verification/numeric.rs @@ -71,6 +71,19 @@ pub(super) fn infer_binary_type( | Operator::BitwiseXor ); + if matches!(operator, Operator::ShiftLeft | Operator::ShiftRight) { + let integer = |ty: &Option| { + matches!( + ty, + Some(WaveType::Int(_) | WaveType::Uint(_) | WaveType::Byte | WaveType::Char) + ) + }; + if !integer(&left_canonical) || !integer(&right_canonical) { + return Err(binary_type_error(operator, &left, &right)); + } + return Ok(left); + } + if arithmetic || comparison || integer_only { validate_contextual_integer_literal(program, &left, &right)?; validate_contextual_integer_literal(program, &right, &left)?; @@ -577,6 +590,9 @@ pub(super) fn is_valid_cast( return false; } + if target == WaveType::Bool && is_pointer_like_type(&source) { + return false; + } let source_integer = integer_bit_width(&source).is_some(); let target_integer = integer_bit_width(&target).is_some(); let source_float = matches!(source, WaveType::Float(_)); diff --git a/llvm/Cargo.toml b/llvm/Cargo.toml index 0ab4dfd4..79bfaa6f 100644 --- a/llvm/Cargo.toml +++ b/llvm/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] parser = { path = "../front/parser" } +hir = { path = "../front/hir" } lexer = { path = "../front/lexer" } error = { path = "../front/error" } inkwell = { version = "0.8.0", default-features = false, features = ["llvm21-1"] } diff --git a/llvm/src/codegen/address.rs b/llvm/src/codegen/address.rs index c44818da..2a83fb19 100644 --- a/llvm/src/codegen/address.rs +++ b/llvm/src/codegen/address.rs @@ -20,12 +20,12 @@ //! allocation (negative pointer offsets may address earlier elements). use crate::expression::rvalue::ExprGenEnv; +use hir::TypedProgram; use inkwell::builder::Builder; use inkwell::context::Context; use inkwell::types::{AsTypeRef, BasicType, BasicTypeEnum, StructType}; use inkwell::values::{IntValue, PointerValue}; use parser::ast::{Expression, WaveType}; -use parser::hir::TypedProgram; use std::collections::HashMap; @@ -49,7 +49,7 @@ pub(crate) fn generate_index_ir<'ctx>( ) -> IntValue<'ctx> { let index_ty = env.context.ptr_sized_int_type(env.target_data, None); let expected = match env.program.type_of(expr) { - Some(parser::hir::HirExpressionType::IntegerLiteral) => Some(index_ty.into()), + Some(hir::HirExpressionType::IntegerLiteral) => Some(index_ty.into()), _ => None, }; let value = env.gen(expr, expected).into_int_value(); @@ -125,8 +125,7 @@ fn pointee_ty_of_ptr_expr<'ctx>( variables: &HashMap>, struct_types: &HashMap>, ) -> BasicTypeEnum<'ctx> { - if let Some(parser::hir::HirExpressionType::Resolved(WaveType::Pointer(inner))) = - program.type_of(expr) + if let Some(hir::HirExpressionType::Resolved(WaveType::Pointer(inner))) = program.type_of(expr) { return wave_type_to_llvm_type(context, inner, struct_types, TypeFlavor::AbiC); } @@ -164,8 +163,7 @@ fn struct_ty_of_ptr_expr<'ctx>( variables: &HashMap>, struct_types: &HashMap>, ) -> StructType<'ctx> { - if let Some(parser::hir::HirExpressionType::Resolved(WaveType::Pointer(inner))) = - program.type_of(expr) + if let Some(hir::HirExpressionType::Resolved(WaveType::Pointer(inner))) = program.type_of(expr) { if let WaveType::Struct(name) = inner.as_ref() { return struct_types[name]; diff --git a/llvm/src/codegen/consts.rs b/llvm/src/codegen/consts.rs index 89a9918b..b87efeee 100644 --- a/llvm/src/codegen/consts.rs +++ b/llvm/src/codegen/consts.rs @@ -17,15 +17,15 @@ //! are reported distinctly so module construction can resolve forward constant //! references in dependency rounds. +use hir::HirExpressionType; use inkwell::context::Context; use inkwell::module::{Linkage, Module}; use inkwell::targets::TargetData; use inkwell::types::{BasicTypeEnum, StringRadix, StructType}; use inkwell::values::{BasicValue, BasicValueEnum}; -use parser::hir::HirExpressionType; +use hir::TypedProgram; use parser::ast::{Expression, Literal, WaveType}; -use parser::hir::TypedProgram; use std::collections::HashMap; use std::fmt; @@ -151,7 +151,8 @@ fn const_from_expected<'ctx>( .into(); } for conversion in &fact.conversions { - value = super::conversions::apply(context, &builder, struct_types, value, conversion); + value = + super::conversions::apply(context, &builder, &scratch, struct_types, value, conversion); } if value.get_type() != expected { // Storage adaptation only. Numeric language conversions must be in HIR. diff --git a/llvm/src/codegen/conversions.rs b/llvm/src/codegen/conversions.rs index 23e01788..222f41d2 100644 --- a/llvm/src/codegen/conversions.rs +++ b/llvm/src/codegen/conversions.rs @@ -12,22 +12,23 @@ //! Lower already-decided Wave numeric semantics. No promotion policy lives here. use super::types::{wave_type_to_llvm_type, TypeFlavor}; +use hir::conversions::{unsigned, ConversionInfo, ConversionKind}; use inkwell::{ builder::Builder, context::Context, + intrinsics::Intrinsic, + module::Module, types::StructType, values::{BasicValue, BasicValueEnum}, FloatPredicate, IntPredicate, }; -use parser::{ - ast::{Operator, WaveType}, - hir::conversions::{unsigned, ConversionInfo, ConversionKind}, -}; +use parser::ast::{Operator, WaveType}; use std::collections::HashMap; pub(crate) fn apply<'ctx>( context: &'ctx Context, builder: &Builder<'ctx>, + module: &Module<'ctx>, structs: &HashMap>, value: BasicValueEnum<'ctx>, info: &ConversionInfo, @@ -61,6 +62,24 @@ pub(crate) fn apply<'ctx>( ) .unwrap() .into(), + IntegerToBool => builder + .build_int_compare( + IntPredicate::NE, + value.into_int_value(), + source.into_int_type().const_zero(), + "convert.bool", + ) + .unwrap() + .into(), + FloatToBool => builder + .build_float_compare( + FloatPredicate::UNE, + value.into_float_value(), + source.into_float_type().const_zero(), + "convert.bool", + ) + .unwrap() + .into(), Truncate => builder .build_int_truncate( value.into_int_value(), @@ -85,22 +104,61 @@ pub(crate) fn apply<'ctx>( ) .unwrap() .into(), - FloatToSigned => builder - .build_float_to_signed_int( - value.into_float_value(), - target.into_int_type(), - "convert.fptosi", - ) - .unwrap() - .into(), - FloatToUnsigned => builder - .build_float_to_unsigned_int( - value.into_float_value(), - target.into_int_type(), - "convert.fptoui", - ) - .unwrap() - .into(), + FloatToSigned | FloatToUnsigned => { + let input = value.into_float_value(); + let ty = input.get_type(); + let signed = info.kind == FloatToSigned; + let bits = target.into_int_type().get_bit_width(); + let range = hir::conversions::float_integer_range(&info.source_type, &info.target_type); + let low = builder + .build_float_compare( + if range.lower_inclusive { + FloatPredicate::OGE + } else { + FloatPredicate::OGT + }, + input, + ty.const_float(range.lower), + "convert.lower", + ) + .unwrap(); + let high = builder + .build_float_compare( + FloatPredicate::OLT, + input, + ty.const_float(range.upper), + "convert.upper", + ) + .unwrap(); + // Very wide integer bounds may round to infinity in the source float. + let finite_low = builder + .build_float_compare( + FloatPredicate::OGT, + input, + ty.const_float(f64::NEG_INFINITY), + "convert.finite", + ) + .unwrap(); + let valid = builder.build_and(low, high, "convert.range").unwrap(); + let valid = builder + .build_and(valid, finite_low, "convert.valid") + .unwrap(); + trap_unless(context, builder, module, valid); + if bits > 64 && input.get_constant().is_none() { + return wide_float_to_int(context, builder, input, target.into_int_type()); + } + if signed { + builder + .build_float_to_signed_int(input, target.into_int_type(), "convert.fptosi") + .unwrap() + .into() + } else { + builder + .build_float_to_unsigned_int(input, target.into_int_type(), "convert.fptoui") + .unwrap() + .into() + } + } FloatExtend | FloatTruncate => builder .build_float_cast( value.into_float_value(), @@ -137,12 +195,43 @@ pub(crate) fn apply<'ctx>( } pub(crate) fn binary<'ctx>( + context: &'ctx Context, + module: &Module<'ctx>, builder: &Builder<'ctx>, left: BasicValueEnum<'ctx>, operator: &Operator, right: BasicValueEnum<'ctx>, computation: &WaveType, + shift_count_type: Option<&WaveType>, ) -> BasicValueEnum<'ctx> { + if let Some(count_type) = shift_count_type { + let lhs = left.into_int_value(); + let rhs = right.into_int_value(); + let count_ty = context.custom_width_int_type(rhs.get_type().get_bit_width().max(16)); + let count = builder + .build_int_cast_sign_flag(rhs, count_ty, !unsigned(count_type), "shift.count") + .unwrap(); + let valid = builder + .build_int_compare( + IntPredicate::ULT, + count, + count_ty.const_int(lhs.get_type().get_bit_width() as u64, false), + "shift.valid", + ) + .unwrap(); + trap_unless(context, builder, module, valid); + let count = builder + .build_int_cast(count, lhs.get_type(), "shift.narrow") + .unwrap(); + return match operator { + Operator::ShiftLeft => builder.build_left_shift(lhs, count, "shl").unwrap().into(), + Operator::ShiftRight => builder + .build_right_shift(lhs, count, !unsigned(computation), "shr") + .unwrap() + .into(), + _ => panic!("ICE: shift facts on a non-shift operation"), + }; + } assert_eq!( left.get_type(), right.get_type(), @@ -275,3 +364,148 @@ pub(crate) fn binary<'ctx>( _ => panic!("ICE: nonnumeric computation"), } } + +// Branch before any potentially poison-producing operation. Constant valid +// guards need no blocks, allowing the same conversions in global initializers. +fn trap_unless<'ctx>( + context: &'ctx Context, + builder: &Builder<'ctx>, + module: &Module<'ctx>, + valid: inkwell::values::IntValue<'ctx>, +) { + if valid.get_zero_extended_constant() == Some(1) { + return; + } + let parent = builder.get_insert_block().unwrap().get_parent().unwrap(); + let ok = context.append_basic_block(parent, "numeric.valid"); + let bad = context.append_basic_block(parent, "numeric.invalid"); + builder.build_conditional_branch(valid, ok, bad).unwrap(); + builder.position_at_end(bad); + let trap = Intrinsic::find("llvm.trap") + .unwrap() + .get_declaration(module, &[]) + .unwrap(); + builder.build_call(trap, &[], "").unwrap(); + builder.build_unreachable().unwrap(); + builder.position_at_end(ok); +} + +// Decode an already checked IEEE value, discarding fractional bits. This also covers i256..i1024 +// without relying on target-specific compiler-rt/libgcc conversion helpers. +fn wide_float_to_int<'ctx>( + context: &'ctx Context, + builder: &Builder<'ctx>, + value: inkwell::values::FloatValue<'ctx>, + target: inkwell::types::IntType<'ctx>, +) -> BasicValueEnum<'ctx> { + let single = value.get_type() == context.f32_type(); + let (bits, fraction, bias, exponent_mask) = if single { + (32, 23, 127, 255) + } else { + (64, 52, 1023, 2047) + }; + let storage = context.custom_width_int_type(bits); + let raw = builder + .build_bit_cast(value, storage, "convert.ieee") + .unwrap() + .into_int_value(); + let negative = builder + .build_int_compare( + IntPredicate::SLT, + raw, + storage.const_zero(), + "convert.negative", + ) + .unwrap(); + let exponent = builder + .build_right_shift( + raw, + storage.const_int(fraction, false), + false, + "convert.exponent", + ) + .unwrap(); + let exponent = builder + .build_and( + exponent, + storage.const_int(exponent_mask, false), + "convert.exponent.bits", + ) + .unwrap(); + let shift = builder + .build_int_sub( + exponent, + storage.const_int(bias + fraction, false), + "convert.shift", + ) + .unwrap(); + let leftward = builder + .build_int_compare( + IntPredicate::SGE, + shift, + storage.const_zero(), + "convert.leftward", + ) + .unwrap(); + let left_count = builder + .build_select(leftward, shift, storage.const_zero(), "convert.left.count") + .unwrap() + .into_int_value(); + let neg_shift = builder.build_int_neg(shift, "convert.right.shift").unwrap(); + let right_count = builder + .build_select( + leftward, + storage.const_zero(), + neg_shift, + "convert.right.count", + ) + .unwrap() + .into_int_value(); + let too_large = builder + .build_int_compare( + IntPredicate::UGT, + right_count, + storage.const_int((bits - 1) as u64, false), + "convert.zero", + ) + .unwrap(); + let right_count = builder + .build_select( + too_large, + storage.const_int((bits - 1) as u64, false), + right_count, + "convert.bounded", + ) + .unwrap() + .into_int_value(); + let significand = builder + .build_and( + raw, + storage.const_int((1u64 << fraction) - 1, false), + "convert.fraction", + ) + .unwrap(); + let significand = builder + .build_or( + significand, + storage.const_int(1u64 << fraction, false), + "convert.significand", + ) + .unwrap(); + let low = builder + .build_right_shift(significand, right_count, false, "convert.low") + .unwrap(); + let magnitude = builder + .build_int_z_extend(low, target, "convert.wide") + .unwrap(); + let left_count = builder + .build_int_z_extend(left_count, target, "convert.wide.count") + .unwrap(); + let magnitude = builder + .build_left_shift(magnitude, left_count, "convert.magnitude") + .unwrap(); + let negated = builder.build_int_neg(magnitude, "convert.negate").unwrap(); + builder + .build_select(negative, negated, magnitude, "convert.integer") + .unwrap() +} diff --git a/llvm/src/codegen/ir.rs b/llvm/src/codegen/ir.rs index e49a9fb6..193c1070 100644 --- a/llvm/src/codegen/ir.rs +++ b/llvm/src/codegen/ir.rs @@ -28,6 +28,7 @@ use inkwell::values::{ }; use inkwell::OptimizationLevel; +use hir::TypedProgram; use inkwell::targets::{ CodeModel, FileType, InitializationConfig, RelocMode, Target, TargetData, TargetMachine, TargetMachineOptions, TargetTriple, @@ -35,7 +36,6 @@ use inkwell::targets::{ use parser::ast::{ ASTNode, EnumNode, ExternFunctionNode, FunctionNode, Mutability, VariableNode, WaveType, }; -use parser::hir::TypedProgram; use std::collections::HashMap; use std::sync::Once; @@ -772,11 +772,11 @@ fn build_module( _ => false, }); let program = if has_async { - let ast = parser::async_lower::lower_program(program).map_err(|e| { + let ast = hir::async_lower::lower_program(program).map_err(|e| { CodegenError::new(CodegenPhase::Lowering, "async state machine", e.message) .with_span(e.span) })?; - lowered = parser::hir::TypedProgram::lower(ast).map_err(|e| { + lowered = hir::TypedProgram::lower(ast).map_err(|e| { CodegenError::new( CodegenPhase::Lowering, "validate async state machine", diff --git a/llvm/src/codegen/validation.rs b/llvm/src/codegen/validation.rs index 72f22392..e46dc8b8 100644 --- a/llvm/src/codegen/validation.rs +++ b/llvm/src/codegen/validation.rs @@ -4,8 +4,8 @@ use super::{ target::CodegenTarget, }; use crate::diagnostic::{CodegenError, CodegenPhase}; +use hir::TypedProgram; use parser::ast::{ASTNode, Expression, StatementNode}; -use parser::hir::TypedProgram; pub(crate) fn validate(program: &TypedProgram, target: CodegenTarget) -> Result<(), CodegenError> { let mut state = State { diff --git a/llvm/src/codegen/variants.rs b/llvm/src/codegen/variants.rs index 1bea5ac5..4e58c315 100644 --- a/llvm/src/codegen/variants.rs +++ b/llvm/src/codegen/variants.rs @@ -19,11 +19,11 @@ //! selected case; projections reinterpret field 2 using that case's tuple type. use super::types::{wave_type_to_llvm_type, TypeFlavor}; +use hir::{HirExpressionType, TypedProgram}; use inkwell::context::Context; use inkwell::targets::TargetData; use inkwell::types::{BasicType, StructType}; use parser::ast::{ASTNode, StatementNode, VariantNode, WaveType}; -use parser::hir::{HirExpressionType, TypedProgram}; use parser::types::{parse_type, split_top_level_generic_args, token_type_to_wave_type}; use std::collections::{BTreeMap, BTreeSet, HashMap}; diff --git a/llvm/src/expression/lvalue.rs b/llvm/src/expression/lvalue.rs index 3f7ddbb1..836f8441 100644 --- a/llvm/src/expression/lvalue.rs +++ b/llvm/src/expression/lvalue.rs @@ -16,6 +16,7 @@ //! address. Loads then use the recovered Wave type, avoiding guesses from //! LLVM's opaque pointer type and preserving aggregate storage representation. +use hir::{HirExpressionType, TypedProgram}; use inkwell::targets::TargetData; use inkwell::{ builder::Builder, @@ -26,7 +27,6 @@ use inkwell::{ AddressSpace, }; use parser::ast::{Expression, WaveType}; -use parser::hir::{HirExpressionType, TypedProgram}; use std::collections::HashMap; use crate::codegen::abi_c::ExternCInfo; diff --git a/llvm/src/expression/rvalue/assign.rs b/llvm/src/expression/rvalue/assign.rs index 82ca5555..e1663c43 100644 --- a/llvm/src/expression/rvalue/assign.rs +++ b/llvm/src/expression/rvalue/assign.rs @@ -115,7 +115,7 @@ fn wave_type_of_lvalue<'ctx, 'a>(env: &ExprGenEnv<'ctx, 'a>, e: &Expression) -> } } Expression::FieldAccess { object, field } => { - if let Some(parser::hir::HirExpressionType::Resolved(ty)) = env.program.type_of(e) { + if let Some(hir::HirExpressionType::Resolved(ty)) = env.program.type_of(e) { return Some(ty.clone()); } let object_ty = wave_type_of_lvalue(env, object)?; diff --git a/llvm/src/expression/rvalue/mod.rs b/llvm/src/expression/rvalue/mod.rs index 52fb9a9f..86b43e73 100644 --- a/llvm/src/expression/rvalue/mod.rs +++ b/llvm/src/expression/rvalue/mod.rs @@ -19,6 +19,7 @@ use crate::codegen::abi_c::ExternCInfo; use crate::codegen::VariableInfo; +use hir::{HirExpressionType, TypedProgram}; use inkwell::builder::Builder; use inkwell::context::Context; use inkwell::module::Module; @@ -26,7 +27,6 @@ use inkwell::targets::TargetData; use inkwell::types::{BasicTypeEnum, StructType}; use inkwell::values::BasicValueEnum; use parser::ast::{Expression, WaveType}; -use parser::hir::{HirExpressionType, TypedProgram}; use std::collections::HashMap; pub mod dispatch; @@ -98,11 +98,14 @@ impl<'ctx, 'a> ExprGenEnv<'ctx, 'a> { let left = self.gen(left, None); let right = self.gen(right, None); crate::codegen::conversions::binary( + self.context, + self.module, self.builder, left, operator, right, fact.computation_type.as_ref().unwrap(), + fact.shift_count_type.as_ref(), ) } _ => dispatch::gen_expr(self, expr, Some(native)), @@ -120,6 +123,7 @@ impl<'ctx, 'a> ExprGenEnv<'ctx, 'a> { value = crate::codegen::conversions::apply( self.context, self.builder, + self.module, self.struct_types, value, conversion, diff --git a/llvm/src/statement/asm.rs b/llvm/src/statement/asm.rs index f9d41fa1..7281f696 100644 --- a/llvm/src/statement/asm.rs +++ b/llvm/src/statement/asm.rs @@ -64,7 +64,7 @@ fn extract_reg_from_constraint(c: &str) -> Option { } pub(super) fn gen_asm_stmt_ir<'ctx>( - program: &parser::hir::TypedProgram, + program: &hir::TypedProgram, context: &'ctx inkwell::context::Context, builder: &'ctx inkwell::builder::Builder<'ctx>, module: &'ctx Module<'ctx>, @@ -210,11 +210,11 @@ pub(super) fn gen_asm_stmt_ir<'ctx>( } fn infer_signedness<'ctx>( - program: &parser::hir::TypedProgram, + program: &hir::TypedProgram, expr: &Expression, variables: &HashMap>, ) -> Option { - if let Some(parser::hir::HirExpressionType::Resolved(ty)) = program.type_of(expr) { + if let Some(hir::HirExpressionType::Resolved(ty)) = program.type_of(expr) { match ty { WaveType::Int(_) => return Some(true), WaveType::Uint(_) | WaveType::Byte | WaveType::Char | WaveType::Bool => { diff --git a/llvm/src/statement/assign.rs b/llvm/src/statement/assign.rs index 8092257a..8b0ccbc4 100644 --- a/llvm/src/statement/assign.rs +++ b/llvm/src/statement/assign.rs @@ -21,12 +21,12 @@ use crate::codegen::types::TypeFlavor; use crate::codegen::{wave_type_to_llvm_type, VariableInfo}; use crate::expression::rvalue::generate_expression_ir; use crate::statement::variable::{coerce_basic_value, expression_is_unsigned, CoercionMode}; +use hir::TypedProgram; use inkwell::module::Module; use inkwell::targets::TargetData; use inkwell::types::{BasicTypeEnum, StructType}; use inkwell::values::BasicValueEnum; use parser::ast::{Expression, Mutability}; -use parser::hir::TypedProgram; use std::collections::HashMap; pub(super) fn gen_assign_ir<'ctx>( diff --git a/llvm/src/statement/control.rs b/llvm/src/statement/control.rs index 11a57976..10fdc96f 100644 --- a/llvm/src/statement/control.rs +++ b/llvm/src/statement/control.rs @@ -21,6 +21,7 @@ use crate::codegen::abi_c::ExternCInfo; use crate::codegen::VariableInfo; use crate::expression::rvalue::generate_expression_ir; use crate::statement::variable::{coerce_basic_value, expression_is_unsigned, CoercionMode}; +use hir::{HirExpressionType, TypedProgram}; use inkwell::basic_block::BasicBlock; use inkwell::module::Module; use inkwell::targets::TargetData; @@ -30,7 +31,6 @@ use inkwell::{FloatPredicate, IntPredicate}; use parser::ast::{ ASTNode, Expression, Literal, MatchArm, MatchPattern, Mutability, StatementNode, WaveType, }; -use parser::hir::{HirExpressionType, TypedProgram}; use std::collections::{HashMap, HashSet}; fn truthy_to_i1<'ctx>( diff --git a/llvm/src/statement/expr_stmt.rs b/llvm/src/statement/expr_stmt.rs index 4d5ace06..188ce3ad 100644 --- a/llvm/src/statement/expr_stmt.rs +++ b/llvm/src/statement/expr_stmt.rs @@ -18,12 +18,12 @@ use crate::codegen::abi_c::ExternCInfo; use crate::codegen::VariableInfo; use crate::expression::rvalue::generate_expression_ir; +use hir::TypedProgram; use inkwell::module::Module; use inkwell::targets::TargetData; use inkwell::types::StructType; use inkwell::values::BasicValueEnum; use parser::ast::Expression; -use parser::hir::TypedProgram; use std::collections::HashMap; pub(super) fn gen_expr_stmt_ir<'ctx>( diff --git a/llvm/src/statement/io.rs b/llvm/src/statement/io.rs index 8b1bf8ac..492e45e8 100644 --- a/llvm/src/statement/io.rs +++ b/llvm/src/statement/io.rs @@ -16,6 +16,7 @@ use crate::codegen::abi_c::ExternCInfo; use crate::codegen::{escape_percent, wave_format_to_c, VariableInfo}; use crate::expression::lvalue::generate_lvalue_ir; use crate::expression::rvalue::generate_expression_ir; +use hir::{HirExpressionType, TypedProgram}; use inkwell::context::Context; use inkwell::module::{Linkage, Module}; use inkwell::targets::TargetData; @@ -24,7 +25,6 @@ use inkwell::values::{BasicMetadataValueEnum, BasicValueEnum, IntValue, PointerV use inkwell::{AddressSpace, IntPredicate}; use parser::ast::{Expression, WaveType}; use parser::format::{format_fragments, FormatFragment}; -use parser::hir::{HirExpressionType, TypedProgram}; use std::collections::HashMap; fn semantic_type(program: &TypedProgram, expression: &Expression) -> WaveType { diff --git a/llvm/src/statement/mod.rs b/llvm/src/statement/mod.rs index 5a0e57d3..6cbe905f 100644 --- a/llvm/src/statement/mod.rs +++ b/llvm/src/statement/mod.rs @@ -27,6 +27,7 @@ pub mod variable; use crate::codegen::abi_c::ExternCInfo; use crate::codegen::VariableInfo; +use hir::TypedProgram; use inkwell::basic_block::BasicBlock; use inkwell::context::Context; use inkwell::targets::TargetData; @@ -34,7 +35,6 @@ use inkwell::types::StructType; use inkwell::values::{BasicValueEnum, FunctionValue}; use parser::ast::WaveType; use parser::ast::{ASTNode, StatementNode}; -use parser::hir::TypedProgram; use std::collections::HashMap; pub fn generate_statement_ir<'ctx>( @@ -279,7 +279,7 @@ pub fn generate_statement_ir<'ctx>( ); if matches!( program.type_of(expr), - Some(parser::hir::HirExpressionType::Resolved( + Some(hir::HirExpressionType::Resolved( parser::ast::WaveType::Never )) ) && builder diff --git a/llvm/src/statement/variable.rs b/llvm/src/statement/variable.rs index e8eb4169..ddfb1782 100644 --- a/llvm/src/statement/variable.rs +++ b/llvm/src/statement/variable.rs @@ -26,8 +26,8 @@ use inkwell::targets::TargetData; use inkwell::types::{BasicType, BasicTypeEnum, StructType}; use inkwell::values::{BasicValue, BasicValueEnum, PointerValue}; +use hir::{HirExpressionType, TypedProgram}; use parser::ast::{Expression, VariableNode, WaveType}; -use parser::hir::{HirExpressionType, TypedProgram}; use std::collections::HashMap; diff --git a/llvm/tests/codegen_errors.rs b/llvm/tests/codegen_errors.rs index 5186f071..dcc898fe 100644 --- a/llvm/tests/codegen_errors.rs +++ b/llvm/tests/codegen_errors.rs @@ -1,8 +1,8 @@ #![cfg(any(feature = "llvm-target-x86", feature = "llvm-target-all"))] +use hir::TypedProgram; use llvm::backend::{link_objects, BackendOptions}; use llvm::codegen::ir::{emit_codegen_file, generate_ir, CodegenFileKind}; use llvm::diagnostic::CodegenPhase; -use parser::hir::TypedProgram; use std::sync::atomic::{AtomicU64, Ordering}; fn program(source: &str) -> TypedProgram { diff --git a/src/cli.rs b/src/cli.rs index eacb3644..bb91dde7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -660,6 +660,13 @@ fn parse_global(args: Vec) -> Result<(Global, Vec), CliError> { } if let Some(mode) = a.strip_prefix("--debug-wave=") { + for value in mode.split(',').map(str::trim).filter(|v| !v.is_empty()) { + if !matches!(value, "tokens" | "ast" | "ir" | "mc" | "hex" | "all") { + return Err(CliError::usage(format!( + "unknown --debug-wave mode: {value}" + ))); + } + } g.debug.apply(mode); i += 1; continue; @@ -669,6 +676,13 @@ fn parse_global(args: Vec) -> Result<(Global, Vec), CliError> { let mode = args.get(i + 1).ok_or_else(|| { CliError::usage("missing value: --debug-wave ") })?; + for value in mode.split(',').map(str::trim).filter(|v| !v.is_empty()) { + if !matches!(value, "tokens" | "ast" | "ir" | "mc" | "hex" | "all") { + return Err(CliError::usage(format!( + "unknown --debug-wave mode: {value}" + ))); + } + } g.debug.apply(mode); i += 2; continue; @@ -741,6 +755,9 @@ fn parse_global(args: Vec) -> Result<(Global, Vec), CliError> { let path = args .get(i + 1) .ok_or_else(|| CliError::usage("missing value: --dep-root "))?; + if path.trim().is_empty() { + return Err(CliError::usage("missing value: --dep-root ")); + } g.dep.roots.push(path.to_string()); i += 2; continue; diff --git a/src/runner.rs b/src/runner.rs index fe2e769e..a273c397 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -22,9 +22,9 @@ use crate::module_resolver::{demangle_module_names, resolve_import_graph}; use crate::{DebugFlags, DepFlags, LinkFlags, LlvmFlags}; use ::error::*; +use ::hir::TypedProgram; use ::parser::ast::*; use ::parser::generics::monomorphize_generics; -use ::parser::hir::TypedProgram; use ::parser::import::*; use ::parser::verification::validate_program_detailed; use ::parser::*; @@ -387,7 +387,7 @@ fn expand_imports_for_codegen( } else { 64 }; - ::parser::hir::resolve_target_types(&mut graph.ast, pointer_bits).map_err(|message| { + ::hir::resolve_target_types(&mut graph.ast, pointer_bits).map_err(|message| { WaveError::new( WaveErrorKind::InvalidStatement(message.clone()), message, diff --git a/std/bytes/cursor.wave b/std/bytes/cursor.wave index d835484b..1bd2f021 100644 --- a/std/bytes/cursor.wave +++ b/std/bytes/cursor.wave @@ -4,6 +4,7 @@ // SPDX-License-Identifier: Apache-2.0 pub import("std::bytes::types")::{Bytes, BytesMut, ByteReader, ByteWriter}; +import("std::bytes::types")::{bytes_subview}; pub import("std::bytes::errors")::{ BYTES_OK, BYTES_ERROR_INVALID, @@ -354,3 +355,13 @@ pub fun bytes_writer_write_exact(writer: ptr, src: Bytes) -> i32 { deref writer.position = deref writer.position + src.len; return BYTES_OK; } + +// The returned range borrows the reader storage. Commit the cursor only on success. +pub fun bytes_reader_read_view(reader: ptr, count: i64, output: ptr) -> i32 { + if (!_bytes_reader_is_valid(reader)) { return BYTES_ERROR_INVALID; } + var src: Bytes = Bytes { data: reader.data, len: reader.len }; + var status: i32 = bytes_subview(src, reader.position, count, output); + if (status != BYTES_OK) { return status; } + reader.position += count; + return BYTES_OK; +} diff --git a/std/bytes/leb128.wave b/std/bytes/leb128.wave index 3e11f5f8..2d142dbe 100644 --- a/std/bytes/leb128.wave +++ b/std/bytes/leb128.wave @@ -131,3 +131,23 @@ pub fun bytes_writer_write_sleb128_i64(writer: ptr, value: i64) -> i writer.position += count; return BYTES_OK; } + +// Exact shortest encoding sizes, with no allocation or cursor mutation. +pub fun bytes_uleb128_u64_len(value: u64) -> i64 { + var remaining: u64 = value; + var count: i64 = 1; + while (remaining >= 128) { remaining = remaining >> 7; count += 1; } + return count; +} + +pub fun bytes_sleb128_i64_len(value: i64) -> i64 { + var remaining: i64 = value; + var count: i64 = 0; + while (true) { + var octet: u8 = (remaining & 127) as u8; + remaining = remaining >> 7; + count += 1; + if ((remaining == 0 && (octet & 64) == 0) || (remaining == -1 && (octet & 64) != 0)) { return count; } + } + return count; +} diff --git a/std/bytes/types.wave b/std/bytes/types.wave index e40766fa..7e288bd0 100644 --- a/std/bytes/types.wave +++ b/std/bytes/types.wave @@ -48,3 +48,24 @@ pub fun bytes_view_mut_is_valid(bytes: BytesMut) -> bool { if (bytes.len > 0 && bytes.data == null) { return false; } return true; } + +import("std::bytes::errors")::{BYTES_OK, BYTES_ERROR_INVALID, BYTES_ERROR_EOF, BYTES_ERROR_NO_SPACE}; + +// Borrow a checked range without copying; failure preserves the output. +pub fun bytes_subview(src: Bytes, offset: i64, count: i64, output: ptr) -> i32 { + if (output == null || !bytes_view_is_valid(src) || offset < 0 || count < 0) { return BYTES_ERROR_INVALID; } + if (offset > src.len || count > src.len - offset) { return BYTES_ERROR_EOF; } + var data: ptr = src.data; + if (offset > 0) { data = data + offset; } + deref output = Bytes { data: data, len: count }; + return BYTES_OK; +} + +pub fun bytes_subview_mut(src: BytesMut, offset: i64, count: i64, output: ptr) -> i32 { + if (output == null || !bytes_view_mut_is_valid(src) || offset < 0 || count < 0) { return BYTES_ERROR_INVALID; } + if (offset > src.len || count > src.len - offset) { return BYTES_ERROR_NO_SPACE; } + var data: ptr = src.data; + if (offset > 0) { data = data + offset; } + deref output = BytesMut { data: data, len: count }; + return BYTES_OK; +} diff --git a/std/env/environ.wave b/std/env/environ.wave index 0bbfa0bf..6cd8fce0 100644 --- a/std/env/environ.wave +++ b/std/env/environ.wave @@ -26,6 +26,8 @@ import("std::env::parse")::{ _env_parse_i64, }; +import("std::env::consts")::{ENV_ERR_NO_SPACE}; + pub struct EnvResult { ok: bool; value: T; @@ -108,14 +110,11 @@ pub fun env_get(name: str, dst: ptr, dst_cap: i64) -> i64 { } pub fun env_exists(name: str) -> bool { - var tmp: array; - var r: i64 = env_get(name, &tmp[0], 2); - - if (r >= 0) { - return true; - } - - return false; + // A one-byte probe copies only the terminator of an empty value. + // NO_SPACE also proves the key exists, without copying its value. + var tmp: array; + var r: i64 = env_get(name, &tmp[0], 1); + return r >= 0 || r == ENV_ERR_NO_SPACE; } pub fun env_get_i64(name: str) -> EnvResult { diff --git a/std/env/parse.wave b/std/env/parse.wave index 5b108fd7..49940be0 100644 --- a/std/env/parse.wave +++ b/std/env/parse.wave @@ -72,6 +72,7 @@ pub fun _env_copy_value( } pub fun _env_parse_i64(raw: ptr, out_value: ptr) -> bool { + if (raw == null || out_value == null) { return false; } var i: i64 = 0; var sign: i64 = 1; @@ -86,6 +87,9 @@ pub fun _env_parse_i64(raw: ptr, out_value: ptr) -> bool { return false; } + // Accumulate negatively so i64::MIN never requires a positive magnitude. + var limit: i64 = -9223372036854775807; + if (sign < 0) { limit = -9223372036854775808; } var value: i64 = 0; while (raw[i] != 0) { var c: u8 = raw[i]; @@ -94,11 +98,14 @@ pub fun _env_parse_i64(raw: ptr, out_value: ptr) -> bool { } var digit: i64 = c - 48; - value = (value * 10) + digit; + if (value < limit / 10) { return false; } + value *= 10; + if (value < limit + digit) { return false; } + value -= digit; i += 1; } - if (sign < 0) { + if (sign > 0) { value = -value; } diff --git a/std/net/tcp.wave b/std/net/tcp.wave index 75bb4e24..0ffe3cd6 100644 --- a/std/net/tcp.wave +++ b/std/net/tcp.wave @@ -340,6 +340,7 @@ pub fun tcp_read_exact(stream: TcpStream, buf: ptr, size: i64) -> i64 { } pub fun tcp_read_result(stream: TcpStream, buf: ptr, size: i64) -> NetIoResult { + if (size == 0) { return net_io_ok(0); } var count: i64 = tcp_read(stream, buf, size); if (count > 0) { return net_io_ok(count); } if (count == 0) { return net_io_eof(0); } diff --git a/std/net/vectored.wave b/std/net/vectored.wave index 167951a1..6c732e17 100644 --- a/std/net/vectored.wave +++ b/std/net/vectored.wave @@ -52,6 +52,11 @@ pub fun net_recv_vectored( var prepared: i64 = _native_slices(slices, count, &native[0]); if (prepared < 0) { return net_io_error(0, prepared); } if (count == 0) { return net_io_ok(0); } + if (eof_on_zero) { + var i: i32 = 0; + while (i < count && slices[i].length == 0) { i += 1; } + if (i == count) { return net_io_ok(0); } + } var result: i64 = vector_recv(fd, &native[0], count, flags); if (result < 0) { return net_io_error(0, result); } if (result == 0 && eof_on_zero) { return net_io_eof(0); } diff --git a/std/sys/freebsd/common/tty.wave b/std/sys/freebsd/common/tty.wave index 506d5f31..56008eea 100644 --- a/std/sys/freebsd/common/tty.wave +++ b/std/sys/freebsd/common/tty.wave @@ -72,6 +72,7 @@ pub fun tty_getattr(fd: i32, t: ptr) -> i64 { } pub fun tty_setattr(fd: i32, action: i32, t: ptr) -> i64 { + if (action != TTY_TCSANOW && action != TTY_TCSADRAIN && action != TTY_TCSAFLUSH) { return -22; } var req: i64 = TTY_TCSETS; if (action == TTY_TCSADRAIN) { diff --git a/std/sys/linux/amd64/tty.wave b/std/sys/linux/amd64/tty.wave index fb91b045..e6e38b7d 100644 --- a/std/sys/linux/amd64/tty.wave +++ b/std/sys/linux/amd64/tty.wave @@ -75,6 +75,7 @@ pub fun tty_getattr(fd: i32, t: ptr) -> i64 { } pub fun tty_setattr(fd: i32, action: i32, t: ptr) -> i64 { + if (action != TTY_TCSANOW && action != TTY_TCSADRAIN && action != TTY_TCSAFLUSH) { return -22; } var req: i64 = TTY_TCSETS; if (action == TTY_TCSADRAIN) { diff --git a/std/sys/linux/arm64/tty.wave b/std/sys/linux/arm64/tty.wave index 82f69ed7..74dbe3f6 100644 --- a/std/sys/linux/arm64/tty.wave +++ b/std/sys/linux/arm64/tty.wave @@ -69,6 +69,7 @@ pub fun tty_getattr(fd: i32, t: ptr) -> i64 { } pub fun tty_setattr(fd: i32, action: i32, t: ptr) -> i64 { + if (action != TTY_TCSANOW && action != TTY_TCSADRAIN && action != TTY_TCSAFLUSH) { return -22; } var req: i64 = TTY_TCSETS; if (action == TTY_TCSADRAIN) { diff --git a/std/sys/linux/riscv64/tty.wave b/std/sys/linux/riscv64/tty.wave index 66aebe59..384c6420 100644 --- a/std/sys/linux/riscv64/tty.wave +++ b/std/sys/linux/riscv64/tty.wave @@ -69,6 +69,7 @@ pub fun tty_getattr(fd: i32, t: ptr) -> i64 { } pub fun tty_setattr(fd: i32, action: i32, t: ptr) -> i64 { + if (action != TTY_TCSANOW && action != TTY_TCSADRAIN && action != TTY_TCSAFLUSH) { return -22; } var req: i64 = TTY_TCSETS; if (action == TTY_TCSADRAIN) { diff --git a/std/sys/macos/amd64/tty.wave b/std/sys/macos/amd64/tty.wave index 0d0b357a..f6233bdf 100644 --- a/std/sys/macos/amd64/tty.wave +++ b/std/sys/macos/amd64/tty.wave @@ -71,6 +71,7 @@ pub fun tty_getattr(fd: i32, t: ptr) -> i64 { } pub fun tty_setattr(fd: i32, action: i32, t: ptr) -> i64 { + if (action != TTY_TCSANOW && action != TTY_TCSADRAIN && action != TTY_TCSAFLUSH) { return -22; } var req: i64 = TTY_TCSETS; if (action == TTY_TCSADRAIN) { diff --git a/std/sys/macos/arm64/tty.wave b/std/sys/macos/arm64/tty.wave index deb125ed..e8fc83f3 100644 --- a/std/sys/macos/arm64/tty.wave +++ b/std/sys/macos/arm64/tty.wave @@ -70,6 +70,7 @@ pub fun tty_getattr(fd: i32, t: ptr) -> i64 { } pub fun tty_setattr(fd: i32, action: i32, t: ptr) -> i64 { + if (action != TTY_TCSANOW && action != TTY_TCSADRAIN && action != TTY_TCSAFLUSH) { return -22; } var req: i64 = TTY_TCSETS; if (action == TTY_TCSADRAIN) { diff --git a/tests/fixtures/stabilization_17/bytes.wave b/tests/fixtures/stabilization_17/bytes.wave new file mode 100644 index 00000000..bacba2ed --- /dev/null +++ b/tests/fixtures/stabilization_17/bytes.wave @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MPL-2.0 +import("std::bytes::types")::{Bytes, BytesMut, ByteReader, ByteWriter, bytes_view, bytes_view_mut, bytes_subview, bytes_subview_mut}; +import("std::bytes::cursor")::{bytes_reader, bytes_writer, bytes_reader_read_view}; +import("std::bytes::leb128")::{bytes_uleb128_u64_len, bytes_sleb128_i64_len, bytes_writer_write_uleb128_u64, bytes_writer_write_sleb128_i64}; +fun main() -> i32 { + var data: array = [1,2,3,4,5,6,7,8,9,10]; + var src: Bytes = bytes_view(&data[0], 10); + var view: Bytes = bytes_view(&data[0], 77); + if (bytes_subview(src, 2, 3, &view) != 0 || view.data != &data[2] || view.len != 3) { return 1; } + if (bytes_subview(src, 10, 0, &view) != 0 || view.len != 0) { return 2; } + if (bytes_subview(bytes_view(null, 0), 0, 0, &view) != 0 || view.data != null) { return 3; } + view = bytes_view(&data[0], 77); + if (bytes_subview(src, 1, 9223372036854775807, &view) == 0 || view.len != 77 || view.data != &data[0]) { return 4; } + if (bytes_subview(src, -1, 0, &view) == 0 || bytes_subview(src, 0, -1, &view) == 0 || bytes_subview(src, 0, 1, null) == 0 || bytes_subview(bytes_view(null, 1), 0, 0, &view) == 0) { return 5; } + var mutable: BytesMut = bytes_view_mut(&data[0], 77); + if (bytes_subview_mut(bytes_view_mut(&data[0], 10), 3, 2, &mutable) != 0 || mutable.data != &data[3]) { return 6; } + mutable.data[0] = 45; + if (data[3] != 45 || data[2] != 3 || data[4] != 5) { return 7; } + if (bytes_subview_mut(bytes_view_mut(&data[0], 10), 10, 1, &mutable) == 0 || mutable.len != 2) { return 8; } + var reader: ByteReader = bytes_reader(&data[0], 10); + if (bytes_reader_read_view(&reader, 4, &view) != 0 || reader.position != 4 || view.len != 4 || view.data != &data[0]) { return 9; } + if (bytes_reader_read_view(&reader, 7, &view) == 0 || reader.position != 4 || view.len != 4) { return 10; } + if (bytes_reader_read_view(&reader, 0, &view) != 0 || reader.position != 4 || view.len != 0 || view.data != &data[4]) { return 11; } + if (bytes_reader_read_view(&reader, 1, null) == 0 || reader.position != 4) { return 12; } + if (bytes_reader_read_view(&reader, 6, &view) != 0 || reader.position != 10) { return 13; } + var empty: ByteReader = bytes_reader(null, 0); + if (bytes_reader_read_view(&empty, 0, &view) != 0 || view.data != null || view.len != 0) { return 14; } + var writer: ByteWriter; + var bit: i32 = 0; + while (bit < 64) { + var bound: u64 = (1 as u64) << bit; + var values: array = [bound - 1, bound, bound + 1]; + var j: i32 = 0; + while (j < 3) { + writer = bytes_writer(&data[0], 10); + if (bytes_writer_write_uleb128_u64(&writer, values[j]) != 0 || writer.position != bytes_uleb128_u64_len(values[j])) { return 15; } + var signed: i64 = values[j] as i64; + writer = bytes_writer(&data[0], 10); + if (bytes_writer_write_sleb128_i64(&writer, signed) != 0 || writer.position != bytes_sleb128_i64_len(signed)) { return 16; } + if (signed >= 0) { + writer = bytes_writer(&data[0], 10); + if (bytes_writer_write_sleb128_i64(&writer, -signed) != 0 || writer.position != bytes_sleb128_i64_len(-signed)) { return 17; } + } + j += 1; + } + bit += 1; + } + if (bytes_uleb128_u64_len(18446744073709551615) != 10 || bytes_sleb128_i64_len(-9223372036854775808) != 10 || bytes_sleb128_i64_len(9223372036854775807) != 10) { return 18; } + return 0; +} diff --git a/tests/fixtures/stabilization_17/environment.wave b/tests/fixtures/stabilization_17/environment.wave new file mode 100644 index 00000000..8ff6e9c6 --- /dev/null +++ b/tests/fixtures/stabilization_17/environment.wave @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MPL-2.0 +// Appended to the production environ provider with only env_read substituted. +fun env_read(dst: ptr, cap: i64) -> i64 { + var blob: array = [69, 61, 0, 65, 61, 120, 0, 76, 61, 97, 98, 99, 100, 101, 102, 103, 104, 0]; + var i: i64 = 0; + while (i < 18) { dst[i] = blob[i]; i += 1; } + return 18; +} +fun main() -> i32 { + if (!env_exists("E") || !env_exists("A") || !env_exists("L") || env_exists("missing") || env_exists("")) { return 1; } + var value: i64 = 123; + if (!_env_parse_i64("9223372036854775807" as ptr, &value) || value != 9223372036854775807) { return 2; } + if (!_env_parse_i64("-9223372036854775808" as ptr, &value) || value != -9223372036854775808) { return 3; } + var invalid: array = ["9223372036854775808", "-9223372036854775809", "999999999999999999999999999999999999", "", "+", "-", "12x"]; + var i: i32 = 0; + while (i < 7) { + value = 123; + if (_env_parse_i64(invalid[i] as ptr, &value) || value != 123) { return 4; } + i += 1; + } + if (!_env_parse_i64("+42" as ptr, &value) || value != 42) { return 5; } + if (!_env_parse_i64("-0" as ptr, &value) || value != 0) { return 6; } + return 0; +} diff --git a/tests/fixtures/stabilization_17/numbers.wave b/tests/fixtures/stabilization_17/numbers.wave new file mode 100644 index 00000000..518d7328 --- /dev/null +++ b/tests/fixtures/stabilization_17/numbers.wave @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MPL-2.0 +const SMALL: i32 = -1; +const FLAG: bool = 2 as bool; +const NEG: i32 = -0.5 as i32; +fun main() -> i32 { + var x: u8 = 128; + var n: u64 = 1; + if ((x << n) != 0) { return 1; } + var signed: i8 = -128; + if ((signed >> n) != -64 || (signed << n) != 0) { return 2; } + if (!FLAG || NEG != 0 || !(2 as bool) || !(-2 as bool)) { return 3; } + if (!(18446744073709551616 as bool)) { return 12; } + var zero: f64 = 0.0; + var nan: f64 = zero / zero; + var inf: f64 = 1.0 / zero; + if (zero as bool || (-zero) as bool || !(nan as bool) || !(inf as bool) || !((-inf) as bool)) { return 4; } + var small: f64 = -0.5; + var edge: f64 = 255.9; + if ((small as u8) != 0 || (edge as u8) != 255) { return 5; } + var low: f64 = -128.9; + if ((low as i8) != -128) { return 6; } + var one: i32 = -1; + match (one) { -0b1 => {} _ => { return 7; } } + match (one) { -0o1 => {} _ => { return 8; } } + match (one) { -0x0_1 => {} _ => { return 9; } } + match (one) { -1 => {} _ => { return 10; } } + // A runtime binding must shadow a same-named constant during checking. + var SMALL: u64 = 1; + if ((x >> SMALL) != 64) { return 11; } + return 0; +} diff --git a/tests/fixtures/stabilization_17/tcp_zero.wave b/tests/fixtures/stabilization_17/tcp_zero.wave new file mode 100644 index 00000000..e8866966 --- /dev/null +++ b/tests/fixtures/stabilization_17/tcp_zero.wave @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MPL-2.0 +import("std::net::tcp")::{TcpListener, TcpStream, tcp_bind_loopback, tcp_listener_local_addr_v4, tcp_connect, tcp_accept, tcp_close, tcp_close_listener, tcp_write_all, tcp_read_result, tcp_shutdown_write}; +import("std::net::address")::{SocketAddrV4}; +import("std::net::error")::{NetResult, NetIoResult}; +import("std::net::vectored")::{NetIoSlice, net_recv_vectored}; +import("std::net::udp")::{UdpSocket, udp_bind_loopback, udp_local_addr_v4, udp_connect_v4, udp_send, udp_recv, udp_recv_vectored, udp_close}; +fun good_zero(result: NetIoResult) -> bool { return result.count == 0 && !result.eof && result.error.kind == 0; } +fun main() -> i32 { + var listener: NetResult = tcp_bind_loopback(0); + if (!listener.ok) { return 1; } + var address: NetResult = tcp_listener_local_addr_v4(listener.value); + if (!address.ok) { return 2; } + var client: NetResult = tcp_connect(address.value); + if (!client.ok) { return 3; } + var peer: NetResult = tcp_accept(listener.value); + if (!peer.ok) { return 4; } + tcp_close_listener(listener.value); + var empty: array = [NetIoSlice { data: null, length: 0 }, NetIoSlice { data: null, length: 0 }]; + // No data queued: zero-capacity reads must not wait for readability. + if (!good_zero(tcp_read_result(client.value, null, 0)) || !good_zero(net_recv_vectored(client.value.fd, &empty[0], 2, 0, true))) { return 5; } + var octet: u8 = 65; + if (tcp_write_all(peer.value, &octet, 1) != 1) { return 6; } + if (!good_zero(tcp_read_result(client.value, null, 0)) || !good_zero(net_recv_vectored(client.value.fd, &empty[0], 2, 0, true)) || !good_zero(net_recv_vectored(client.value.fd, null, 0, 0, true))) { return 7; } + var read: NetIoResult = tcp_read_result(client.value, &octet, 1); + if (read.count != 1 || read.eof || read.error.kind != 0 || octet != 65) { return 8; } + if (tcp_read_result(client.value, null, 1).error.kind == 0 || tcp_read_result(client.value, &octet, -1).error.kind == 0) { return 9; } + empty[1].length = -1; + if (net_recv_vectored(client.value.fd, &empty[0], 2, 0, true).error.kind == 0) { return 10; } + tcp_shutdown_write(peer.value); + read = tcp_read_result(client.value, &octet, 1); + if (!read.eof || read.count != 0) { return 11; } + tcp_close(peer.value); tcp_close(client.value); + var receiver: NetResult = udp_bind_loopback(0); + if (!receiver.ok) { return 12; } + var udp_address: NetResult = udp_local_addr_v4(receiver.value); + if (!udp_address.ok) { return 13; } + var sender: NetResult = udp_connect_v4(udp_address.value); + if (!sender.ok) { return 14; } + if (!good_zero(udp_send(sender.value, &octet, 0)) || udp_send(sender.value, &octet, 1).count != 1) { return 15; } + empty[1].length = 0; + if (!good_zero(udp_recv_vectored(receiver.value, &empty[0], 2))) { return 16; } + // The empty datagram was consumed, so the next receive sees the marker. + read = udp_recv(receiver.value, &octet, 1); + if (read.count != 1 || read.eof || octet != 65) { return 17; } + udp_close(sender.value); udp_close(receiver.value); + return 0; +} diff --git a/tests/fixtures/stabilization_17/wasm.cjs b/tests/fixtures/stabilization_17/wasm.cjs new file mode 100644 index 00000000..cf7e98bd --- /dev/null +++ b/tests/fixtures/stabilization_17/wasm.cjs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MPL-2.0 +const fs = require('node:fs'); +const assert = require('node:assert/strict'); +(async () => { + const {instance} = await WebAssembly.instantiate(fs.readFileSync(process.argv[2]), {}); + const f = instance.exports; + assert.equal(f.shift(1n, 63n), -(1n << 63n)); + assert.equal(f.signed_shift(-128n, 1), -64n); + for (const n of [64n, 256n, 1n << 32n, -1n]) assert.throws(() => f.shift(1n, n), WebAssembly.RuntimeError); + assert.throws(() => f.signed_shift(1n, -1), WebAssembly.RuntimeError); + assert.equal(f.signed_cast(-128.9), -128); + assert.equal(f.signed_cast(127.9), 127); + assert.equal(f.unsigned_cast(-0.5), 0); + assert.equal(f.unsigned_cast(255.9), 255); + for (const x of [NaN, Infinity, -Infinity, -129, 128]) assert.throws(() => f.signed_cast(x), WebAssembly.RuntimeError); + for (const x of [NaN, Infinity, -Infinity, -1, 256]) assert.throws(() => f.unsigned_cast(x), WebAssembly.RuntimeError); + for (const x of [0, -0]) assert.equal(f.truth(x), 0); + for (const x of [2, -0.5, NaN, Infinity, -Infinity]) assert.equal(f.truth(x), 1); + assert.equal(f.wide(3.75), 0); + assert.throws(() => f.wide(-1), WebAssembly.RuntimeError); + console.log('checked numeric boundaries and traps passed'); +})().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/tests/fixtures/stabilization_17/wasm.wave b/tests/fixtures/stabilization_17/wasm.wave new file mode 100644 index 00000000..a79ca13f --- /dev/null +++ b/tests/fixtures/stabilization_17/wasm.wave @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MPL-2.0 +export(c) fun shift(value: u64, count: u64) -> u64 { return value << count; } +export(c) fun signed_shift(value: i64, count: i8) -> i64 { return value >> count; } +export(c) fun signed_cast(value: f64) -> i32 { return (value as i8) as i32; } +export(c) fun unsigned_cast(value: f64) -> i32 { return (value as u8) as i32; } +export(c) fun truth(value: f64) -> i32 { return (value as bool) as i32; } +export(c) fun wide(value: f64) -> i32 { + var result: u1024 = value as u1024; + if (result == 3) { return 0; } + return 1; +} diff --git a/tests/io_constant_regressions.rs b/tests/io_constant_regressions.rs index 05a61498..3ffdce4d 100644 --- a/tests/io_constant_regressions.rs +++ b/tests/io_constant_regressions.rs @@ -218,7 +218,7 @@ const strings: array = ["one", "two"]; fun main() -> i32 { var local: u32 = floating as u32; var local_even: bool = 2 as bool; var local_odd: bool = 3 as bool; - if ((even as i32) != (local_even as i32) || (odd as i32) != (local_odd as i32) || nested_bool != 0) { return 3; } + if ((even as i32) != (local_even as i32) || (odd as i32) != (local_odd as i32) || nested_bool != 1 || !even || !odd) { return 3; } if (widened != 255 || implicit != 255 || signed_wide != -1 || wider != wide) { return 1; } if (unsigned_float != local || signed_float != -3 || to_float != 255.0) { return 2; } println("{} {c} {} {} {c} {} {}", data.text, data.letter, data.count, text, letter, strings[0], strings[1]); diff --git a/tests/stabilization_17.rs b/tests/stabilization_17.rs new file mode 100644 index 00000000..37fcf44a --- /dev/null +++ b/tests/stabilization_17.rs @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: MPL-2.0 +use std::{ + fs, + path::{Path, PathBuf}, + process::{Command, Output}, + sync::atomic::{AtomicU64, Ordering}, +}; +static NEXT: AtomicU64 = AtomicU64::new(0); +struct Case(PathBuf); +impl Case { + fn new() -> Self { + let p = std::env::temp_dir().join(format!( + "wave-stabilization-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&p).unwrap(); + Self(p) + } + fn command(&self) -> Command { + let mut c = Command::new(env!("CARGO_BIN_EXE_wavec")); + c.current_dir(&self.0) + .args(["--std-root"]) + .arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("std")); + c + } + fn source(&self, source: &str) { + fs::write(self.0.join("case.wave"), source).unwrap(); + } + fn build(&self, opt: &str) -> Output { + self.command() + .args(["build", "case.wave", opt, "--emit=ir,bin", "-o", "case.exe"]) + .output() + .unwrap() + } + fn run(&self, source: &str) { + self.source(source); + for opt in ["-O0", "-O2"] { + ok(self.build(opt)); + ok(Command::new(self.0.join("case.exe")).output().unwrap()); + } + } +} +impl Drop for Case { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} +fn ok(o: Output) { + assert!( + o.status.success(), + "{:?}\n{}\n{}", + o.status, + String::from_utf8_lossy(&o.stdout), + String::from_utf8_lossy(&o.stderr) + ); +} +fn native() -> bool { + let o = Command::new(env!("CARGO_BIN_EXE_wavec")) + .args(["print", "default-target"]) + .output() + .unwrap(); + llvm::codegen::target::target_spec_for_triple(String::from_utf8_lossy(&o.stdout).trim()) + .is_some() +} +#[test] +fn declaration_diagnostics_preserve_the_offending_token() { + let case = Case::new(); + for (source, token, message) in [ + ("fun f(x: i24) {}", "i24", "type"), + ("fun f() -> i24 {}", "i24", "type"), + ("fun f(x i32) {}", "i32", "':'"), + ("fun f(x: i32 y: i32) {}", "y:", "','"), + ("fun f(x: i32, x: i32) {}", "x: i32)", "duplicate parameter"), + ("import(\"./missing\" as );", ")", "identifier"), + ("pub import(\"./missing\")::{ };", "}", "identifier"), + ("import(\"./missing\")::{x y};", "y", "','"), + ("import(\"./missing\" as m)::{x};", "::", "aliases"), + ("import(123);", "123", "string literal"), + ("import(\"\\xFF\");", "\"\\xFF\"", "UTF-8"), + ("struct S { value: i24; }", "i24", "type"), + ("struct S { x i32; }", "i32", "':'"), + ("struct S { value: i32 other: i32; }", "other", "';'"), + ("struct S { 123: i32; }", "123", "identifier"), + ] { + case.source(source); + let column = source.find(token).unwrap() + 1; + for format in ["human", "json"] { + let o = case + .command() + .args(["check", "case.wave", &format!("--error-format={format}")]) + .output() + .unwrap(); + let err = String::from_utf8_lossy(&o.stderr); + assert!( + !o.status.success() && o.stdout.is_empty(), + "{source}: {o:?}" + ); + assert!(err.contains(message), "{source}: {err}"); + if format == "json" { + let json = utils::json::parse(err.trim()).unwrap(); + assert_eq!( + json.get("error").unwrap().get_num("column"), + Some(column as f64), + "{source}: {err}" + ); + } else { + assert!(err.contains(&format!(":1:{column}")), "{source}: {err}"); + } + } + } +} +#[test] +fn cli_rejects_invalid_debug_modes_and_empty_dependency_roots() { + let case = Case::new(); + case.source("fun main() {}"); + for args in [ + vec!["--debug-wave=toknes"], + vec!["--debug-wave", "ir,toknes"], + vec!["--debug-wave=tokens,toknes"], + vec!["--debug-wave", "toknes"], + vec!["--dep-root="], + vec!["--dep-root= "], + vec!["--dep-root", ""], + vec!["--dep-root", " "], + ] { + let o = case + .command() + .args(args) + .args(["check", "case.wave"]) + .output() + .unwrap(); + assert_eq!(o.status.code(), Some(2), "{o:?}"); + } + for args in [ + vec!["--debug-wave=ir"], + vec!["--debug-wave", "ir,mc"], + vec!["--debug-wave=all"], + vec!["--dep-root", "relative"], + vec!["--dep-root=/tmp"], + ] { + ok(case + .command() + .args(args) + .args(["check", "case.wave"]) + .output() + .unwrap()); + } +} +#[test] +fn numeric_constants_reject_invalid_shifts_and_casts() { + let case = Case::new(); + for source in [ + "struct S { value: f64; } const s: S = S { value: 1e300 }; fun main() -> i32 { return s.value as i32; }", + "const values: array = [1.0, 1e300]; fun main() -> i32 { return values[1] as i32; }", + "fun main() -> i32 { return 1 << 32; }", + "fun main() -> i32 { var x: u8 = 1; return (x << 4294967296) as i32; }", + "fun main() -> i32 { var x: u8 = 1; return (x >> -4294967296) as i32; }", + "fun main() -> i32 { return 1 >> -1; }", + "const N: u64 = 4294967296; fun main() -> i32 { var x: u8 = 1; return (x << N) as i32; }", + "fun main() -> i32 { var x: u8 = 1; return (x << (256 as u64)) as i32; }", + "fun main() -> i32 { return (1 << (16 + 16)); }", + "fun main() -> i32 { return (1.0 / 0.0) as i32; }", + "fun main() -> i32 { return (0.0 / 0.0) as i32; }", + "fun main() -> i32 { return 2147483648.0 as i32; }", + "fun main() -> i32 { var p: ptr = null; return (p as bool) as i32; }", + "fun main() -> i32 { return 1 << true; }", + "fun main() -> i32 { return 1 << 1.0; }", + "fun main() -> i32 { var x: i32 = 1; match(x) { - => {} _ => {} } return 0; }", + "const NEG: i32 = -1; fun main() -> i32 { var x: i32 = -1; match(x) { -1 => {} NEG => {} _ => {} } return 0; }", + ] { + case.source(source); + let o = case.command().args(["check", "case.wave", "--error-format=json"]).output().unwrap(); + assert!(!o.status.success(), "accepted {source}"); + utils::json::parse(String::from_utf8_lossy(&o.stderr).trim()).unwrap(); + } +} +#[test] +fn checked_numeric_runtime_and_byte_apis() { + if !native() { + return; + } + let case = Case::new(); + for name in ["numbers", "bytes", "environment"] { + let mut source = fs::read_to_string( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join(format!("tests/fixtures/stabilization_17/{name}.wave")), + ) + .unwrap(); + if name == "environment" { + let provider = fs::read_to_string( + Path::new(env!("CARGO_MANIFEST_DIR")).join("std/env/environ.wave"), + ) + .unwrap() + .replace("import(\"std::sys::env\")::{\n env_read,\n};", ""); + source = format!("{provider}\n{source}"); + } + case.run(&source); + } +} +#[test] +fn runtime_invalid_numeric_operations_trap_before_conversion() { + if !native() { + return; + } + let case = Case::new(); + for source in [ + "fun main() -> i32 { var x: u8 = 1; var n: u64 = 256; return (x << n) as i32; }", + "fun main() -> i32 { var x: u1024 = 1; var n: i8 = -1; return (x >> n) as i32; }", + "fun main() -> i32 { var x: f64 = 1e300; return x as i32; }", + "fun main() -> i32 { var x: f64 = -1.0; return (x as u64) as i32; }", + "fun main() -> i32 { var z: f64 = 0.0; var n: f64 = z / z; return (n as i64) as i32; }", + ] { + case.source(source); + for opt in ["-O0", "-O2"] { + ok(case.build(opt)); + let ir = fs::read_to_string(case.0.join("case.ll")).unwrap(); + assert!(ir.contains("llvm.trap"), "{ir}"); + let status = Command::new(case.0.join("case.exe")).status().unwrap(); + assert!(!status.success(), "{source} {opt}"); + #[cfg(unix)] + { + use std::os::unix::process::ExitStatusExt; + assert!(status.signal().is_some(), "{status}"); + } + } + } +} + +#[test] +fn shifts_and_float_conversions_cover_every_integer_width() { + if !native() { + return; + } + let case = Case::new(); + for bits in [8, 16, 32, 64, 128, 256, 512, 1024] { + for signed in [false, true] { + let ty = format!("{}{bits}", if signed { "i" } else { "u" }); + let top = 2f64.powi(bits - i32::from(signed)); + let below = if top.is_infinite() { + f64::MAX + } else { + f64::from_bits(top.to_bits() - 1).trunc() + }; + let negative = if signed { + "var neg: f64 = -1.9; if ((neg as TYPE) != -1) { return 4; }".replace("TYPE", &ty) + } else { + String::new() + }; + case.run(&format!("fun main() -> i32 {{ var x: {ty} = 1; var n: u64 = {}; var shifted: {ty} = x << n; var zero: u8 = 0; if ((shifted >> zero) != shifted) {{ return 1; }} var count: u8 = 1; var two: {ty} = x << count; if (two != 2) {{ return 2; }} var edge: f64 = {below:e}; var narrow: f32 = 3.9; if ((narrow as {ty}) != 3) {{ return 5; }} var converted: {ty} = edge as {ty}; if (converted != {below:.0}) {{ return 3; }} {negative} return 0; }}", bits - 1)); + } + } +} + +#[test] +fn webassembly_checked_numeric_runtime() { + if std::env::var_os("WAVE_RUN_WASM_RUNTIME_TESTS").is_none() { + return; + } + let case = Case::new(); + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/stabilization_17"); + for target in ["wasm32-unknown-unknown", "wasm64-unknown-unknown"] { + if llvm::codegen::target::target_spec_for_triple(target).is_none() { + continue; + } + for opt in ["-O0", "-O2"] { + let object = case.0.join("numeric.o"); + let module = case.0.join("numeric.wasm"); + ok(case + .command() + .arg("build") + .arg(root.join("wasm.wave")) + .args(["--target", target, opt, "--emit=obj", "-o"]) + .arg(&object) + .output() + .unwrap()); + let mut link = Command::new("wasm-ld"); + if target.starts_with("wasm64") { + link.arg("-mwasm64"); + } + link.arg("--no-entry"); + for name in [ + "shift", + "signed_shift", + "signed_cast", + "unsigned_cast", + "truth", + "wide", + ] { + link.arg(format!("--export={name}")); + } + ok(link.arg(object).arg("-o").arg(&module).output().unwrap()); + // An external deadline also bounds a broken target runtime. + ok(Command::new("python3").current_dir(env!("CARGO_MANIFEST_DIR")).args(["-c", "import subprocess,sys; subprocess.run(['node','--experimental-wasm-memory64',sys.argv[1],sys.argv[2]],check=True,timeout=20)"]).arg(root.join("wasm.cjs")).arg(module).output().unwrap()); + } + } +} diff --git a/tools/test_std_boundary_runtime.py b/tools/test_std_boundary_runtime.py index 1ff8cc3a..a2f57f22 100644 --- a/tools/test_std_boundary_runtime.py +++ b/tools/test_std_boundary_runtime.py @@ -34,3 +34,46 @@ def test_path_destinations_and_dot_components(self): @unittest.skipIf(os.name == "nt", "Windows socket handles are not POSIX descriptors") def test_single_descriptor_waits_and_disabled_poll_entries(self): self.run_case("poll") + + def test_zero_length_tcp_reads_preserve_connection_and_queued_data(self): + target = os.environ["WAVE_TEST_TARGET"] + with tempfile.TemporaryDirectory(prefix="wave-tcp-zero-") as temp: + for opt in ["-O0", "-O2"]: + binary = Path(temp) / "tcp.exe" + result = run_process([COMPILER, "--std-root", str(ROOT / "std"), "build", + str(ROOT / "tests/fixtures/stabilization_17/tcp_zero.wave"), "--target", target, + opt, "-o", str(binary)], timeout=60, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + result = run_process([str(binary)], timeout=10, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, f"{target} {opt}: {result.stdout} {result.stderr}") + + @unittest.skipIf(os.name == "nt", "POSIX terminal ioctl providers") + def test_invalid_terminal_actions_preserve_pty_settings(self): + import pty + import termios + master, slave = pty.openpty() + try: + initial = termios.tcgetattr(slave) + with tempfile.TemporaryDirectory(prefix="wave-tty-action-") as temp: + source = Path(temp) / "tty.wave" + source.write_text(f'''import("std::sys::tty")::{{Termios, tty_getattr, tty_setattr, TTY_ECHO, TTY_TCSANOW, TTY_TCSADRAIN, TTY_TCSAFLUSH}}; +fun main() -> i32 {{ + var term: Termios; + if (tty_getattr({slave}, &term) != 0) {{ return 1; }} + term.c_lflag = term.c_lflag ^ TTY_ECHO; + if (tty_setattr({slave}, -1, &term) != -22 || tty_setattr({slave}, 99, &term) != -22) {{ return 2; }} + if (tty_getattr({slave}, &term) != 0) {{ return 3; }} + if (tty_setattr({slave}, TTY_TCSANOW, &term) != 0 || tty_setattr({slave}, TTY_TCSADRAIN, &term) != 0 || tty_setattr({slave}, TTY_TCSAFLUSH, &term) != 0) {{ return 4; }} + return 0; +}}''') + for opt in ["-O0", "-O2"]: + binary = Path(temp) / "tty" + result = run_process([COMPILER, "--std-root", str(ROOT / "std"), "build", str(source), + "--target", os.environ["WAVE_TEST_TARGET"], opt, "-o", str(binary)], timeout=60, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + result = run_process([str(binary)], pass_fds=(slave,), timeout=10, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(termios.tcgetattr(slave), initial) + finally: + os.close(slave) + os.close(master) diff --git a/utils/src/const_int.rs b/utils/src/const_int.rs new file mode 100644 index 00000000..eef0671d --- /dev/null +++ b/utils/src/const_int.rs @@ -0,0 +1,523 @@ +// SPDX-License-Identifier: MPL-2.0 +//! Bounded exact integer arithmetic for compiler constant evaluation. +//! +//! Values use signed magnitude with 2048 bits of storage: enough for the full +//! product of two Wave 1024-bit operands before width normalization. Operations +//! report capacity overflow; truncation occurs only through `normalize`. +//! This module has no frontend, backend, or external crate dependencies. +use std::cmp::Ordering; + +const WORDS: usize = 32; +const CAPACITY: usize = WORDS * 64; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ConstInt { + negative: bool, + words: Box<[u64; WORDS]>, +} + +impl ConstInt { + pub fn zero() -> Self { + Self { + negative: false, + words: Box::new([0; WORDS]), + } + } + + pub fn from_u64(value: u64) -> Self { + let mut result = Self::zero(); + result.words[0] = value; + result + } + + /// Parse unsigned digits. Apply a source-level minus with `negated`. + /// Invalid digits, unsupported radices, and capacity overflow return None. + pub fn from_digits(digits: &str, radix: u32) -> Option { + if !matches!(radix, 2 | 8 | 10 | 16) || digits.is_empty() { + return None; + } + let mut result = Self::zero(); + for ch in digits.chars() { + if !ch.is_ascii() { + return None; + } + let mut carry = u128::from(ch.to_digit(radix)?); + for word in result.words.iter_mut() { + let next = u128::from(*word) * u128::from(radix) + carry; + *word = next as u64; + carry = next >> 64; + } + if carry != 0 { + return None; + } + } + Some(result) + } + + pub fn is_zero(&self) -> bool { + self.words.iter().all(|&w| w == 0) + } + pub fn is_negative(&self) -> bool { + self.negative + } + pub fn bits(&self) -> usize { + self.words + .iter() + .rposition(|&w| w != 0) + .map_or(0, |i| i * 64 + 64 - self.words[i].leading_zeros() as usize) + } + fn bit(&self, index: usize) -> bool { + index < CAPACITY && self.words[index / 64] & (1 << (index % 64)) != 0 + } + fn canonical(mut self) -> Self { + self.negative &= !self.is_zero(); + self + } + pub fn negated(mut self) -> Self { + self.negative = !self.negative && !self.is_zero(); + self + } + fn magnitude_cmp(&self, other: &Self) -> Ordering { + self.words.iter().rev().cmp(other.words.iter().rev()) + } + fn magnitude_add(&self, other: &Self) -> Option { + let mut result = Self::zero(); + let mut carry = 0u128; + for i in 0..WORDS { + let n = u128::from(self.words[i]) + u128::from(other.words[i]) + carry; + result.words[i] = n as u64; + carry = n >> 64; + } + (carry == 0).then_some(result) + } + fn magnitude_sub(&self, other: &Self) -> Self { + debug_assert!(self.magnitude_cmp(other) != Ordering::Less); + let mut result = Self::zero(); + let mut borrow = false; + for i in 0..WORDS { + let (n, a) = self.words[i].overflowing_sub(other.words[i]); + let (n, b) = n.overflowing_sub(u64::from(borrow)); + result.words[i] = n; + borrow = a || b; + } + debug_assert!(!borrow); + result + } + pub fn checked_add(&self, other: &Self) -> Option { + let (mut result, negative) = if self.negative == other.negative { + (self.magnitude_add(other)?, self.negative) + } else if self.magnitude_cmp(other) != Ordering::Less { + (self.magnitude_sub(other), self.negative) + } else { + (other.magnitude_sub(self), other.negative) + }; + result.negative = negative; + Some(result.canonical()) + } + pub fn checked_sub(&self, other: &Self) -> Option { + self.checked_add(&other.clone().negated()) + } + pub fn checked_mul(&self, other: &Self) -> Option { + let mut result = Self::zero(); + let right_words = other.bits().div_ceil(64); + for i in 0..self.bits().div_ceil(64) { + let mut carry = 0u128; + for j in 0..right_words { + let product = u128::from(self.words[i]) * u128::from(other.words[j]); + if i + j >= WORDS { + if product != 0 || carry != 0 { + return None; + } + continue; + } + let n = product + u128::from(result.words[i + j]) + carry; + result.words[i + j] = n as u64; + carry = n >> 64; + } + if carry != 0 { + let index = i + right_words; + if index >= WORDS { + return None; + } + // Earlier rows end before this word. + debug_assert_eq!(result.words[index], 0); + result.words[index] = carry as u64; + } + } + result.negative = self.negative != other.negative; + Some(result.canonical()) + } + /// Truncating division; the remainder has the dividend's sign. + pub fn div_rem(&self, other: &Self) -> Option<(Self, Self)> { + if other.is_zero() { + return None; + } + let mut quotient = Self::zero(); + let mut remainder = Self::zero(); + for bit in (0..self.bits()).rev() { + // Each prefix of the dividend fits the original capacity. + remainder = remainder.checked_shl(1)?; + remainder.words[0] |= u64::from(self.bit(bit)); + if remainder.magnitude_cmp(other) != Ordering::Less { + remainder = remainder.magnitude_sub(other); + quotient.words[bit / 64] |= 1 << (bit % 64); + } + } + quotient.negative = self.negative != other.negative; + remainder.negative = self.negative; + Some((quotient.canonical(), remainder.canonical())) + } + pub fn checked_shl(&self, shift: usize) -> Option { + if self.is_zero() { + return Some(Self::zero()); + } + if shift > CAPACITY - self.bits() { + return None; + } + let mut result = Self::zero(); + let (whole, part) = (shift / 64, shift % 64); + for i in 0..self.bits().div_ceil(64) { + result.words[i + whole] |= self.words[i] << part; + if part != 0 && i + whole + 1 < WORDS { + result.words[i + whole + 1] |= self.words[i] >> (64 - part); + } + } + result.negative = self.negative; + Some(result) + } + fn magnitude_shr(&self, shift: usize) -> Self { + let mut result = Self::zero(); + if shift >= CAPACITY { + return result; + } + let (whole, part) = (shift / 64, shift % 64); + for i in 0..WORDS - whole { + result.words[i] = self.words[i + whole] >> part; + if part != 0 && i + whole + 1 < WORDS { + result.words[i] |= self.words[i + whole + 1] << (64 - part); + } + } + result + } + /// Arithmetic shift: negative values round toward negative infinity. + pub fn shifted_right(&self, shift: usize) -> Self { + let mut result = self.magnitude_shr(shift); + if self.negative && (0..shift.min(CAPACITY)).any(|i| self.bit(i)) { + result = result.magnitude_add(&Self::from_u64(1)).unwrap(); + } + result.negative = self.negative; + result.canonical() + } + fn twos_complement(&self) -> [u64; WORDS + 1] { + let mut words = [0; WORDS + 1]; + words[..WORDS].copy_from_slice(self.words.as_ref()); + if self.negative { + negate_words(&mut words); + } + words + } + fn from_twos_complement(mut words: [u64; WORDS + 1]) -> Option { + let negative = words[WORDS] >> 63 != 0; + if negative { + negate_words(&mut words); + } + if words[WORDS] != 0 { + return None; + } + let mut result = Self::zero(); + result.words.copy_from_slice(&words[..WORDS]); + result.negative = negative; + Some(result.canonical()) + } + fn bitwise(&self, other: &Self, op: impl Fn(u64, u64) -> u64) -> Option { + let mut words = self.twos_complement(); + for (a, b) in words.iter_mut().zip(other.twos_complement()) { + *a = op(*a, b); + } + Self::from_twos_complement(words) + } + pub fn bitand(&self, other: &Self) -> Option { + self.bitwise(other, |a, b| a & b) + } + pub fn bitor(&self, other: &Self) -> Option { + self.bitwise(other, |a, b| a | b) + } + pub fn bitxor(&self, other: &Self) -> Option { + self.bitwise(other, |a, b| a ^ b) + } + pub fn checked_not(&self) -> Option { + Self::from_twos_complement(self.twos_complement().map(|w| !w)) + } + /// Apply modulo 2^bits, then interpret the result with the requested sign. + /// Wave widths 1..=1024 are supported, including bool and byte storage. + pub fn normalize(&self, bits: u16, signed: bool) -> Self { + assert!((1..=1024).contains(&bits), "unsupported Wave integer width"); + let bits = usize::from(bits); + let mut words = self.twos_complement(); + let negative = signed && words[(bits - 1) / 64] & (1 << ((bits - 1) % 64)) != 0; + let fill = if negative { u64::MAX } else { 0 }; + let (whole, part) = (bits / 64, bits % 64); + if part != 0 { + let mask = (1u64 << part) - 1; + words[whole] = (words[whole] & mask) | (fill & !mask); + } + words[bits.div_ceil(64)..].fill(fill); + Self::from_twos_complement(words).unwrap() + } + pub fn fits(&self, bits: u16, signed: bool) -> bool { + self == &self.normalize(bits, signed) + } + pub fn to_usize(&self) -> Option { + if self.negative || self.bits() > usize::BITS as usize { + return None; + } + Some(self.words[0] as usize) + } + /// Decode IEEE binary64 exactly, discarding fractional bits toward zero. + pub fn from_f64(value: f64) -> Option { + if !value.is_finite() { + return None; + } + let raw = value.to_bits(); + let exponent = ((raw >> 52) & 0x7ff) as i32 - 1023; + if exponent < 0 { + return Some(Self::zero()); + } + let significand = (raw & ((1u64 << 52) - 1)) | (1u64 << 52); + let value = Self::from_u64(significand); + let mut result = if exponent >= 52 { + value.checked_shl((exponent - 52) as usize)? + } else { + value.magnitude_shr((52 - exponent) as usize) + }; + result.negative = raw >> 63 != 0; + Some(result.canonical()) + } + fn rounded_significand(&self, precision: usize) -> (u64, i32) { + let shift = self.bits().saturating_sub(precision); + let mut significand = self.magnitude_shr(shift).words[0]; + // Round once, to nearest with ties to even; do not pass f32 through f64. + if shift != 0 + && self.bit(shift - 1) + && (significand & 1 != 0 || (0..shift - 1).any(|i| self.bit(i))) + { + significand += 1; + } + (significand, shift as i32) + } + pub fn to_f64(&self) -> f64 { + let (significand, shift) = self.rounded_significand(53); + let value = significand as f64 * 2f64.powi(shift); + if self.negative { + -value + } else { + value + } + } + pub fn to_f32(&self) -> f32 { + let (significand, shift) = self.rounded_significand(24); + let value = significand as f32 * 2f32.powi(shift); + if self.negative { + -value + } else { + value + } + } +} +fn negate_words(words: &mut [u64]) { + let mut carry = true; + for word in words { + let (next, overflow) = (!*word).overflowing_add(u64::from(carry)); + *word = next; + carry = overflow; + } +} +impl Ord for ConstInt { + fn cmp(&self, other: &Self) -> Ordering { + match (self.negative, other.negative) { + (true, false) => Ordering::Less, + (false, true) => Ordering::Greater, + (false, false) => self.magnitude_cmp(other), + (true, true) => self.magnitude_cmp(other).reverse(), + } + } +} +impl PartialOrd for ConstInt { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + fn int(n: i128) -> ConstInt { + let v = ConstInt::from_digits(&n.unsigned_abs().to_string(), 10).unwrap(); + if n < 0 { + v.negated() + } else { + v + } + } + #[test] + fn signed_arithmetic_matches_native_integers() { + for a in -32i128..=32 { + let x = int(a); + assert_eq!(x.checked_not().unwrap(), int(!a)); + for shift in [0, 1, 7, 63, 64, 127, 1024, usize::MAX] { + assert_eq!( + x.shifted_right(shift), + int(if shift < 128 { + a >> shift + } else { + -((a < 0) as i128) + }) + ); + } + for b in -32i128..=32 { + let y = int(b); + assert_eq!(x.cmp(&y), a.cmp(&b)); + assert_eq!(x.checked_add(&y).unwrap(), int(a + b)); + assert_eq!(x.checked_sub(&y).unwrap(), int(a - b)); + assert_eq!(x.checked_mul(&y).unwrap(), int(a * b)); + assert_eq!(x.bitand(&y).unwrap(), int(a & b)); + assert_eq!(x.bitor(&y).unwrap(), int(a | b)); + assert_eq!(x.bitxor(&y).unwrap(), int(a ^ b)); + if b != 0 { + assert_eq!(x.div_rem(&y).unwrap(), (int(a / b), int(a % b))); + } else { + assert!(x.div_rem(&y).is_none()); + } + } + } + } + #[test] + fn wide_arithmetic_matches_independent_reference_vectors() { + fn hex(raw: &str) -> ConstInt { + let value = ConstInt::from_digits(raw.trim_start_matches('-'), 16).unwrap(); + if raw.starts_with('-') { + value.negated() + } else { + value + } + } + for (index, line) in include_str!("../tests/fixtures/const_int.tsv") + .lines() + .enumerate() + { + if line.starts_with('#') { + continue; + } + let parts: Vec<_> = line.split_whitespace().collect(); + let (a, b, expected) = (hex(parts[1]), hex(parts[2]), hex(parts[3])); + let result = match parts[0] { + "add" => a.checked_add(&b), + "sub" => a.checked_sub(&b), + "mul" => a.checked_mul(&b), + "div" => a.div_rem(&b).map(|(q, _)| q), + "rem" => a.div_rem(&b).map(|(_, r)| r), + "and" => a.bitand(&b), + "or" => a.bitor(&b), + "xor" => a.bitxor(&b), + "shl" => a.checked_shl(b.to_usize().unwrap()), + "shr" => Some(a.shifted_right(b.to_usize().unwrap())), + _ => panic!("unknown operation"), + }; + assert_eq!(result, Some(expected), "reference vector {}", index + 1); + } + } + + #[test] + fn normalization_and_capacity_boundaries_are_explicit() { + for value in [i128::MIN, i128::MAX, -129, -128, -1, 0, 127, 128, 255, 256] { + let n = int(value); + assert_eq!(n.normalize(8, true), int((value as i8) as i128)); + assert_eq!(n.normalize(8, false), int((value as u8) as i128)); + assert_eq!(n.normalize(128, true), n); + } + let max = ConstInt::from_digits(&"f".repeat(256), 16).unwrap(); + assert!(max.fits(1024, false)); + assert!(!max.fits(1024, true)); + assert_eq!(max.normalize(1024, true), int(-1)); + let square = max.checked_mul(&max).unwrap(); + assert_eq!(square.bits(), 2048); + assert_eq!(square.div_rem(&max).unwrap(), (max.clone(), int(0))); + assert_eq!(square.normalize(1024, false), int(1)); + assert!(square.checked_mul(&int(2)).is_none()); + assert!(max.checked_shl(1025).is_none()); + let limit = ConstInt::from_digits(&"f".repeat(512), 16).unwrap(); + assert!(limit.checked_add(&int(1)).is_none()); + assert!(limit.checked_not().is_none()); + assert!(ConstInt::from_digits(&"f".repeat(513), 16).is_none()); + assert_eq!(int(0).checked_shl(usize::MAX), Some(int(0))); + for (digits, radix) in [("", 10), ("2", 2), ("1_0", 10), ("12", 10), ("1", 3)] { + assert!(ConstInt::from_digits(digits, radix).is_none()); + } + } + #[test] + fn floating_conversions_round_once_and_decode_exactly() { + for value in [ + i128::MIN, + i128::MAX, + -16777219, + -1, + 0, + 1, + 16777217, + 16777219, + ] { + assert_eq!(int(value).to_f64(), value as f64); + assert_eq!(int(value).to_f32(), value as f32); + } + // f64 intermediate rounding would erase the final +1 and choose the + // wrong side of an f32 tie. + for digits in [ + "1208925891672223212634113", + "9007199254740993", + "9007199254740995", + "340282356779733661637539395458142568448", + ] { + let n = ConstInt::from_digits(digits, 10).unwrap(); + assert_eq!( + n.to_f64().to_bits(), + digits.parse::().unwrap().to_bits() + ); + assert_eq!( + n.to_f32().to_bits(), + digits.parse::().unwrap().to_bits() + ); + assert_eq!(n.clone().negated().to_f32(), -n.to_f32()); + } + for value in [ + 0.0, + -0.0, + 0.99, + -0.99, + 1.99, + -1.99, + f64::MIN_POSITIVE, + f64::from_bits(1), + 2f64.powi(512), + f64::MAX, + -f64::MAX, + ] { + let n = ConstInt::from_f64(value).unwrap(); + assert_eq!(n.to_f64(), value.trunc()); + } + let mut seed = 0x5741_5645_1024_2048u64; + for _ in 0..4096 { + seed ^= seed << 13; + seed ^= seed >> 7; + seed ^= seed << 17; + let value = f64::from_bits(seed); + if value.is_finite() { + let integer = ConstInt::from_f64(value).unwrap(); + assert_eq!(integer.to_f64(), value.trunc(), "{value}"); + assert_eq!(integer.to_f32(), value.trunc() as f32, "{value}"); + } + } + for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] { + assert!(ConstInt::from_f64(value).is_none()); + } + } +} diff --git a/utils/src/lib.rs b/utils/src/lib.rs index b18599ea..203f1a0b 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -16,6 +16,7 @@ //! terminal color, and JSON helpers remain reusable at every compiler layer. pub mod colorex; +pub mod const_int; pub mod formatx; pub mod json; pub mod paths; diff --git a/utils/tests/fixtures/const_int.tsv b/utils/tests/fixtures/const_int.tsv new file mode 100644 index 00000000..f2fc35bf --- /dev/null +++ b/utils/tests/fixtures/const_int.tsv @@ -0,0 +1,101 @@ +# Python integer reference results. Columns: op, lhs, rhs, expected (signed hexadecimal). +add fdd6d7990539a6da -d44a5a919ef5a97 f09231efeb4a4c43 +sub fdd6d7990539a6da -d44a5a919ef5a97 10b1b7d421f290171 +mul fdd6d7990539a6da -d44a5a919ef5a97 -d27fa472f6a95a07a65212069300e96 +div fdd6d7990539a6da -d44a5a919ef5a97 -13 +rem fdd6d7990539a6da -d44a5a919ef5a97 1be8c0c1875eda5 +and fdd6d7990539a6da -d44a5a919ef5a97 f09252100410a448 +or fdd6d7990539a6da -d44a5a919ef5a97 -202018c65805 +xor fdd6d7990539a6da -d44a5a919ef5a97 -f09272301cd6fc4d +shl fdd6d7990539a6da 21 1fbadaf320a734db400000000 +shr fdd6d7990539a6da 21 7eeb6bcc +add -8b0609e81ca1ee1c e5318dbc07e16fb -7cb2f10c5c23d721 +sub -8b0609e81ca1ee1c e5318dbc07e16fb -995922c3dd200517 +mul -8b0609e81ca1ee1c e5318dbc07e16fb -7c774ffcf7360a7316a85462f02dd74 +div -8b0609e81ca1ee1c e5318dbc07e16fb -9 +rem -8b0609e81ca1ee1c e5318dbc07e16fb -a1a2a2e58331f49 +and -8b0609e81ca1ee1c e5318dbc07e16fb 4511013c05e10e0 +or -8b0609e81ca1ee1c e5318dbc07e16fb -810401201c81e801 +xor -8b0609e81ca1ee1c e5318dbc07e16fb -85551133dcdff8e1 +shl -8b0609e81ca1ee1c 21 -1160c13d03943dc3800000000 +shr -8b0609e81ca1ee1c 21 -458304f5 +add f9b2c4827335b8f96522588f42eda61d -71bbd69da69ab389d85655cc186e573 f297071898cc0dc0c79cf3328166c0aa +sub f9b2c4827335b8f96522588f42eda61d -71bbd69da69ab389d85655cc186e573 100ce81ec4d9f643202a7bdec04748b90 +mul f9b2c4827335b8f96522588f42eda61d -71bbd69da69ab389d85655cc186e573 -6eef1fa8558d3f4904a784d8618aa9989b2aec8a0fb96d16a1a454676879007 +div f9b2c4827335b8f96522588f42eda61d -71bbd69da69ab389d85655cc186e573 -23 +rem f9b2c4827335b8f96522588f42eda61d -71bbd69da69ab389d85655cc186e573 e7df0996c3503bdbe57ce0cd7c4764 +and f9b2c4827335b8f96522588f42eda61d -71bbd69da69ab389d85655cc186e573 f8a04082211410c1602218830269020d +or f9b2c4827335b8f96522588f42eda61d -71bbd69da69ab389d85655cc186e573 -6093969884803009885255081024163 +xor f9b2c4827335b8f96522588f42eda61d -71bbd69da69ab389d85655cc186e573 -fea979eba95c13c1f8a73dd3836b4370 +shl f9b2c4827335b8f96522588f42eda61d 41 1f3658904e66b71f2ca44b11e85db4c3a0000000000000000 +shr f9b2c4827335b8f96522588f42eda61d 41 7cd96241399adc7c +add -b91cc39cce28b9800a2426767a79dc08 40cf989307b3d179172e65db776eab9 -b50fca139dad7c6878b14018c302f14f +sub -b91cc39cce28b9800a2426767a79dc08 40cf989307b3d179172e65db776eab9 -bd29bd25fea3f6979b970cd431f0c6c1 +mul -b91cc39cce28b9800a2426767a79dc08 40cf989307b3d179172e65db776eab9 -2edd4d7cc4901eaeabeda6c22f5a863a95c288ee54c42a548f5c6ef08df51c8 +div -b91cc39cce28b9800a2426767a79dc08 40cf989307b3d179172e65db776eab9 -2d +rem -b91cc39cce28b9800a2426767a79dc08 40cf989307b3d179172e65db776eab9 -2d4e67f487efc5b78f1a7fd3a929983 +and -b91cc39cce28b9800a2426767a79dc08 40cf989307b3d179172e65db776eab9 4003801305304179152c009850622b8 +or -b91cc39cce28b9800a2426767a79dc08 40cf989307b3d179172e65db776eab9 -b9100214ce0080800a04002248091407 +xor -b91cc39cce28b9800a2426767a79dc08 40cf989307b3d179172e65db776eab9 -bd103a15fe5384979b56c02bcd0f36bf +shl -b91cc39cce28b9800a2426767a79dc08 41 -1723987399c51730014484cecf4f3b8100000000000000000 +shr -b91cc39cce28b9800a2426767a79dc08 41 -5c8e61ce67145cc1 +add c69f8ab75621833814f4d9f1fcf6ce258aac90cccdfa24f300c4fe1182c27c6d -71e6c360b18c0b00dc77bbac8078f3973d71be12e4400fc6d13129338f59843 bf811e814b08c288072d5e3734ef3eec16d574eb9fb623f693b1eb7e49cce42a +sub c69f8ab75621833814f4d9f1fcf6ce258aac90cccdfa24f300c4fe1182c27c6d -71e6c360b18c0b00dc77bbac8078f3973d71be12e4400fc6d13129338f59843 cdbdf6ed613a43e822bc55acc4fe5d5efe83acadfc3e25ef6dd810a4bbb814b0 +mul c69f8ab75621833814f4d9f1fcf6ce258aac90cccdfa24f300c4fe1182c27c6d -71e6c360b18c0b00dc77bbac8078f3973d71be12e4400fc6d13129338f59843 -585f772819facf159dfbfcea6224013cad05934d9897edb57e5d2a2ee7f80588a02c8f41c1efc943328ab2d3484f23d7f7636d2e87de61e8d49b0289f184887 +div c69f8ab75621833814f4d9f1fcf6ce258aac90cccdfa24f300c4fe1182c27c6d -71e6c360b18c0b00dc77bbac8078f3973d71be12e4400fc6d13129338f59843 -1b +rem c69f8ab75621833814f4d9f1fcf6ce258aac90cccdfa24f300c4fe1182c27c6d -71e6c360b18c0b00dc77bbac8078f3973d71be12e4400fc6d13129338f59843 66a21042a8530a6a0eacd3ee42ab31652fca00cecce0a537fc2088a80db6d5c +and c69f8ab75621833814f4d9f1fcf6ce258aac90cccdfa24f300c4fe1182c27c6d -71e6c360b18c0b00dc77bbac8078f3973d71be12e4400fc6d13129338f59843 c0818281542103081030804134f040048828800cc1ba240300c4ec008202642d +or c69f8ab75621833814f4d9f1fcf6ce258aac90cccdfa24f300c4fe1182c27c6d -71e6c360b18c0b00dc77bbac8078f3973d71be12e4400fc6d13129338f59843 -1006400091840800903220a0001011871530b212204000c6d13008238358003 +xor c69f8ab75621833814f4d9f1fcf6ce258aac90cccdfa24f300c4fe1182c27c6d -71e6c360b18c0b00dc77bbac8078f3973d71be12e4400fc6d13129338f59843 -c181e6815d3943881933a24b34f1411cf97b8b2de3be240f6dd7ec82ba37e430 +shl c69f8ab75621833814f4d9f1fcf6ce258aac90cccdfa24f300c4fe1182c27c6d 81 18d3f156eac43067029e9b3e3f9ed9c4b155921999bf449e60189fc230584f8da00000000000000000000000000000000 +shr c69f8ab75621833814f4d9f1fcf6ce258aac90cccdfa24f300c4fe1182c27c6d 81 634fc55bab10c19c0a7a6cf8fe7b6712 +add -c0c56c637c391b717f2dd1cef158698b56d97e5db355f8f98900b990bac46c81 1ebb296098da22f29793600e1218859ca948ac0b67411173952e41b8d968574d -a20a4302e35ef87ee79a71c0df3fe3eead90d2524c14e785f3d277d7e15c1534 +sub -c0c56c637c391b717f2dd1cef158698b56d97e5db355f8f98900b990bac46c81 1ebb296098da22f29793600e1218859ca948ac0b67411173952e41b8d968574d -df8095c415133e6416c131dd0370ef2800222a691a970a6d1e2efb49942cc3ce +mul -c0c56c637c391b717f2dd1cef158698b56d97e5db355f8f98900b990bac46c81 1ebb296098da22f29793600e1218859ca948ac0b67411173952e41b8d968574d -1724121230b87dec57418714dc7d98b98a7939842d220d4013478c4ac3c13855593dd710e0ebc9aea05b3c489cd0aa5d1d31afdd7485470c364d1d7e5b5c79cd +div -c0c56c637c391b717f2dd1cef158698b56d97e5db355f8f98900b990bac46c81 1ebb296098da22f29793600e1218859ca948ac0b67411173952e41b8d968574d -6 +rem -c0c56c637c391b717f2dd1cef158698b56d97e5db355f8f98900b990bac46c81 1ebb296098da22f29793600e1218859ca948ac0b67411173952e41b8d968574d -862741fe71c49c1f1b9917a84c547df5f25761947cf904409eb2f3ba25260b3 +and -c0c56c637c391b717f2dd1cef158698b56d97e5db355f8f98900b990bac46c81 1ebb296098da22f29793600e1218859ca948ac0b67411173952e41b8d968574d 1e3a010080c220828092200002008414a900800244000102142e40284128134d +or -c0c56c637c391b717f2dd1cef158698b56d97e5db355f8f98900b990bac46c81 1ebb296098da22f29793600e1218859ca948ac0b67411173952e41b8d968574d -c044440364211901682c91c0e1406803569152549014e8880800b80022842881 +xor -c0c56c637c391b717f2dd1cef158698b56d97e5db355f8f98900b990bac46c81 1ebb296098da22f29793600e1218859ca948ac0b67411173952e41b8d968574d -de7e4503e4e33983e8beb1c0e340ec17ff91d256d414e98a1c2ef82863ac3bce +shl -c0c56c637c391b717f2dd1cef158698b56d97e5db355f8f98900b990bac46c81 81 -1818ad8c6f87236e2fe5ba39de2b0d316adb2fcbb66abf1f3120173217588d90200000000000000000000000000000000 +shr -c0c56c637c391b717f2dd1cef158698b56d97e5db355f8f98900b990bac46c81 81 -6062b631be1c8db8bf96e8e778ac34c6 +add ae2a71adcb2d0e76e218e50e351799603ce349f92f1b6f39a692792ffb40f7f40427f687acfb250f75de5c10bee5f3d1a716cf818d0f0873a8d209e36009b7d1 -215ca14caf7183741aa79dfc217608b59f1ef5940794cdee54a453b24d0ac768163821bfa511aa67fe964892533a2cc534826cdea69bddf79cc6b6e581b99c7 ac14a7990035f63fa06e6b2e730038d4e2f15a9feea2225ac14833f4d6704b7d82c4746bb2aa0a68f5f4f78799b2510553cea8b3a2a54a942f059e7507ee1e0a +sub ae2a71adcb2d0e76e218e50e351799603ce349f92f1b6f39a692792ffb40f7f40427f687acfb250f75de5c10bee5f3d1a716cf818d0f0873a8d209e36009b7d1 -215ca14caf7183741aa79dfc217608b59f1ef5940794cdee54a453b24d0ac768163821bfa511aa67fe964892533a2cc534826cdea69bddf79cc6b6e581b99c7 b0403bc2962426ae23c35eedf72ef9eb96d539526f94bc188bdcbe6b2011a46a858b78a3a74c3fb5f5c7c099e419969dfa5ef64f7778c653229e7551b8255198 +mul ae2a71adcb2d0e76e218e50e351799603ce349f92f1b6f39a692792ffb40f7f40427f687acfb250f75de5c10bee5f3d1a716cf818d0f0873a8d209e36009b7d1 -215ca14caf7183741aa79dfc217608b59f1ef5940794cdee54a453b24d0ac768163821bfa511aa67fe964892533a2cc534826cdea69bddf79cc6b6e581b99c7 -16b27da51f1a1d4ede7bcc2c1284a4aef8cfb9a7b0d7f607dd16adf32b89b72c5d362ee05631c67a3d8b827eb124008b1c413d34258dec2b09de387f3562035c14b36fb05efcd1c4714e4c76e97a59984f4597447da9467d20648a36274310fde49963af01f75b8f2c0e8820fffaaf40f0287253ad487e18eba6329b174cc77 +div ae2a71adcb2d0e76e218e50e351799603ce349f92f1b6f39a692792ffb40f7f40427f687acfb250f75de5c10bee5f3d1a716cf818d0f0873a8d209e36009b7d1 -215ca14caf7183741aa79dfc217608b59f1ef5940794cdee54a453b24d0ac768163821bfa511aa67fe964892533a2cc534826cdea69bddf79cc6b6e581b99c7 -53 +rem ae2a71adcb2d0e76e218e50e351799603ce349f92f1b6f39a692792ffb40f7f40427f687acfb250f75de5c10bee5f3d1a716cf818d0f0873a8d209e36009b7d1 -215ca14caf7183741aa79dfc217608b59f1ef5940794cdee54a453b24d0ac768163821bfa511aa67fe964892533a2cc534826cdea69bddf79cc6b6e581b99c7 119eceffd10348c97d3618247834c321372b00947c782f54f7e07030b990d8810e4c77584af8113fd32c399af282b92a6b23abe8cc678ff2b8b351ccf16dc4c +and ae2a71adcb2d0e76e218e50e351799603ce349f92f1b6f39a692792ffb40f7f40427f687acfb250f75de5c10bee5f3d1a716cf818d0f0873a8d209e36009b7d1 -215ca14caf7183741aa79dfc217608b59f1ef5940794cdee54a453b24d0ac768163821bfa511aa67fe964892533a2cc534826cdea69bddf79cc6b6e581b99c7 ac2a31a901080640a210840035009960240200a02f02232102903804db0053800404748404aa2509001618109ac45111a416c900050600208012008120002611 +or ae2a71adcb2d0e76e218e50e351799603ce349f92f1b6f39a692792ffb40f7f40427f687acfb250f75de5c10bee5f3d1a716cf818d0f0873a8d209e36009b7d1 -215ca14caf7183741aa79dfc217608b59f1ef5940794cdee54a453b24d0ac768163821bfa511aa67fe964892533a2cc534826cdea69bddf79cc6b6e581b99c7 -158a1000d2100101a218d1c200608b4110a600406000c641480410049008028140001852001aa00a2120890112000c5048204c6260b58c510c620c18120807 +xor ae2a71adcb2d0e76e218e50e351799603ce349f92f1b6f39a692792ffb40f7f40427f687acfb250f75de5c10bee5f3d1a716cf818d0f0873a8d209e36009b7d1 -215ca14caf7183741aa79dfc217608b59f1ef5940794cdee54a453b24d0ac768163821bfa511aa67fe964892533a2cc534826cdea69bddf79cc6b6e581b99c7 -ac3fbbb901da1641a3b29cd1f700f9eb6512a6a06f6223e743d83c14df905b828544749c56aa3fa90a3738999bd6511df45ee94c6766b5acd11e628d38122e18 +shl ae2a71adcb2d0e76e218e50e351799603ce349f92f1b6f39a692792ffb40f7f40427f687acfb250f75de5c10bee5f3d1a716cf818d0f0873a8d209e36009b7d1 101 15c54e35b965a1cedc431ca1c6a2f32c079c693f25e36de734d24f25ff681efe8084fed0f59f64a1eebbcb8217dcbe7a34e2d9f031a1e10e751a413c6c0136fa20000000000000000000000000000000000000000000000000000000000000000 +shr ae2a71adcb2d0e76e218e50e351799603ce349f92f1b6f39a692792ffb40f7f40427f687acfb250f75de5c10bee5f3d1a716cf818d0f0873a8d209e36009b7d1 101 571538d6e596873b710c72871a8bccb01e71a4fc978db79cd3493c97fda07bfa +add -8f951eb148a05cb60980b1b8d0e2a63b6445e4c41db4200536ad88c25aed6b13df5d9f6156eda027053a57f4383e7ab924941ae14f84710867f3aa375f6f40c9 e7aa2bb7c029bce99a06211a6ba21cb824ff3f713376ed1b6d4d57bf243b675798b230e4286cb681b3314374108cc17614a792a0df255405a44d005fc44d01d -811a7bf5cc9dc0e76fe04fa72a28846fe1f5f0cd0a7cb1337fd8b34668a9b49e65d27c531466d4beea0743bcf735aea1c349a1b741921bc80daeda31632a70ac +sub -8f951eb148a05cb60980b1b8d0e2a63b6445e4c41db4200536ad88c25aed6b13df5d9f6156eda027053a57f4383e7ab924941ae14f84710867f3aa375f6f40c9 e7aa2bb7c029bce99a06211a6ba21cb824ff3f713376ed1b6d4d57bf243b675798b230e4286cb681b3314374108cc17614a792a0df255405a44d005fc44d01d -9e0fc16cc4a2f884a32113ca779cc806e695d8bb30eb8ed6ed825e3e4d31218958e8c26f99746b8f206d6c2b794746d085de940b5d76c648c2387a3d5bb410e6 +mul -8f951eb148a05cb60980b1b8d0e2a63b6445e4c41db4200536ad88c25aed6b13df5d9f6156eda027053a57f4383e7ab924941ae14f84710867f3aa375f6f40c9 e7aa2bb7c029bce99a06211a6ba21cb824ff3f713376ed1b6d4d57bf243b675798b230e4286cb681b3314374108cc17614a792a0df255405a44d005fc44d01d -81ef003d75cae5ef162d75fbd8b2d07871d278651d16117ed17e7edbdae0dda25f2d4691ab523d4540006d921c41fe16fe00cce4972c698c4c9cf98368009d11d900a2b2c854a24d175666de375d43914f2eac40d61b547a8405594c12a652bc926c58227755cd92dbb082336e0a1fa4a8b5abbda0dd0e49ac1b71045a1a6c5 +div -8f951eb148a05cb60980b1b8d0e2a63b6445e4c41db4200536ad88c25aed6b13df5d9f6156eda027053a57f4383e7ab924941ae14f84710867f3aa375f6f40c9 e7aa2bb7c029bce99a06211a6ba21cb824ff3f713376ed1b6d4d57bf243b675798b230e4286cb681b3314374108cc17614a792a0df255405a44d005fc44d01d -9 +rem -8f951eb148a05cb60980b1b8d0e2a63b6445e4c41db4200536ad88c25aed6b13df5d9f6156eda027053a57f4383e7ab924941ae14f84710867f3aa375f6f40c9 e7aa2bb7c029bce99a06211a6ba21cb824ff3f713376ed1b6d4d57bf243b675798b230e4286cb681b3314374108cc17614a792a0df255405a44d005fc44d01d -d456619ec88e272a2dd3f19f4577613cf76511470c13aa5c9320766d68c00f2997963e10030797e106ea202eeef4de6b8f5d866d1ff71c53b885a018103efc4 +and -8f951eb148a05cb60980b1b8d0e2a63b6445e4c41db4200536ad88c25aed6b13df5d9f6156eda027053a57f4383e7ab924941ae14f84710867f3aa375f6f40c9 e7aa2bb7c029bce99a06211a6ba21cb824ff3f713376ed1b6d4d57bf243b675798b230e4286cb681b3314374108cc17614a792a0df255405a44d005fc44d01d 6aa00a3402834890204201261801c0820a133302034ed080505539a00294642082200e00024b481a01000341008406414a610a0072044018045000a0009015 +or -8f951eb148a05cb60980b1b8d0e2a63b6445e4c41db4200536ad88c25aed6b13df5d9f6156eda027053a57f4383e7ab924941ae14f84710867f3aa375f6f40c9 e7aa2bb7c029bce99a06211a6ba21cb824ff3f713376ed1b6d4d57bf243b675798b230e4286cb681b3314374108cc17614a792a0df255405a44d005fc44d01d -81851c0000a04430000091a850408630640004000c8000040029088008ac490286549c6114692007040843c0383632a8049402c14204200825b32a32032b00c1 +xor -8f951eb148a05cb60980b1b8d0e2a63b6445e4c41db4200536ad88c25aed6b13df5d9f6156eda027053a57f4383e7ab924941ae14f84710867f3aa375f6f40c9 e7aa2bb7c029bce99a06211a6ba21cb824ff3f713376ed1b6d4d57bf243b675798b230e4286cb681b3314374108cc17614a792a0df255405a44d005fc44d01d -81efbc0a34a2c7789020d3a9765887f0e60a17330e834ed480795db9a8aedd66a6d6bc6f146b6b4f1e0943c37936b6ae45de63cb427624483db77a32a32b90d6 +shl -8f951eb148a05cb60980b1b8d0e2a63b6445e4c41db4200536ad88c25aed6b13df5d9f6156eda027053a57f4383e7ab924941ae14f84710867f3aa375f6f40c9 101 -11f2a3d629140b96c13016371a1c54c76c88bc9883b68400a6d5b1184b5dad627bebb3ec2addb404e0a74afe8707cf572492835c29f08e210cfe7546ebede81920000000000000000000000000000000000000000000000000000000000000000 +shr -8f951eb148a05cb60980b1b8d0e2a63b6445e4c41db4200536ad88c25aed6b13df5d9f6156eda027053a57f4383e7ab924941ae14f84710867f3aa375f6f40c9 101 -47ca8f58a4502e5b04c058dc6871531db222f2620eda10029b56c4612d76b58a +add e3347d436ad61c17cbdbf7c7a4b6aff9e6180cd3083aca654cb44969ec559c276cec733966bae9a7df4978c459b9caccc925af10e12b849d4df0089e0a9c1d6a06f87d122c7b562d8158cd52670312f5c7bcf820d5fa4faa91b7d4eb7a337e3f2f17422f67a92e7932bcc0d76b786392cda4fe54ca555b34e688d1485952eae4 -5ce277a4947030b490567ce66d3dfeeba25fe19542d3b774e0a583ccaaa7447dd2ee5ac98d6a8a7fe6c6d8332bcdeeaff5b510383fc641b916bed5361c9f5ec7cf86e01f7580272b1da6345451e23030cfb6aff0c288e1da4d235704f482633c8843ab240c69097031f6a7f102d63e76a08792000531248703783bcf6de03c7 dd6655c9218f190c82d68ff93de2d00b2bf20eb9b40d8eedfea9f12d21ab27df8fbd8d8ccde440ffe0dd0b4126fcebe1c9ca5e0d5d2f2081bc841b4aa8d2277d8a000f10352353bacf7e6a0d21e4eff2bac18d21c9d1c18cece59f7b2aeb580b6693077d26e29de22f9d56585b4affab639c8534ca0248ec76514d8b6274e71d +sub e3347d436ad61c17cbdbf7c7a4b6aff9e6180cd3083aca654cb44969ec559c276cec733966bae9a7df4978c459b9caccc925af10e12b849d4df0089e0a9c1d6a06f87d122c7b562d8158cd52670312f5c7bcf820d5fa4faa91b7d4eb7a337e3f2f17422f67a92e7932bcc0d76b786392cda4fe54ca555b34e688d1485952eae4 -5ce277a4947030b490567ce66d3dfeeba25fe19542d3b774e0a583ccaaa7447dd2ee5ac98d6a8a7fe6c6d8332bcdeeaff5b510383fc641b916bed5361c9f5ec7cf86e01f7580272b1da6345451e23030cfb6aff0c288e1da4d235704f482633c8843ab240c69097031f6a7f102d63e76a08792000531248703783bcf6de03c7 e902a4bdb41d1f2314e15f960b8a8fe8a03e0aec5c6805dc9abea1a6b700106f4a1b58e5ff91924fddb5e6478c76a9b7c88100146527e8b8df5bf5f16c66135683f0eb1423d358a033333097ac2135f8d4b8631fe222ddc8368a0a5bc97ba472f79b7ce1a86fbf1035dc2b567ba5c77a37ad7774caa86d7d56c055055030eeab +mul e3347d436ad61c17cbdbf7c7a4b6aff9e6180cd3083aca654cb44969ec559c276cec733966bae9a7df4978c459b9caccc925af10e12b849d4df0089e0a9c1d6a06f87d122c7b562d8158cd52670312f5c7bcf820d5fa4faa91b7d4eb7a337e3f2f17422f67a92e7932bcc0d76b786392cda4fe54ca555b34e688d1485952eae4 -5ce277a4947030b490567ce66d3dfeeba25fe19542d3b774e0a583ccaaa7447dd2ee5ac98d6a8a7fe6c6d8332bcdeeaff5b510383fc641b916bed5361c9f5ec7cf86e01f7580272b1da6345451e23030cfb6aff0c288e1da4d235704f482633c8843ab240c69097031f6a7f102d63e76a08792000531248703783bcf6de03c7 -526fdb8a4781c393323f9a899626118f4999cc079a43a4b9e8521fa92ee39d680f659825d47baf35d9232c2960f9f2d5053438dcdd347cf2044bedbe44d56fd5e04d62cd73a61e386e20f43a1e7392fc938f33f31f5980768bdd9e5c53656026da680af547b1ee0513022f090578b54fa897b8e73e6bb49221b8e0abb9b8cc501e1b911f7eb3f380324925c60dc472e23e235dd10ecbf704d5af60e2892905ddda77b264ba5f28de44015a37c62960daf50c703ef98625e743d02b1fa6cb7e3eb93bd8a390d9441747a81c7c443dd2c09ece0b8c195594afdd40473d918b2ed9e52b1ffb4f24580534ceb01394ab3583df0a6bb3ec62f1f0f74d75831ed433c +div e3347d436ad61c17cbdbf7c7a4b6aff9e6180cd3083aca654cb44969ec559c276cec733966bae9a7df4978c459b9caccc925af10e12b849d4df0089e0a9c1d6a06f87d122c7b562d8158cd52670312f5c7bcf820d5fa4faa91b7d4eb7a337e3f2f17422f67a92e7932bcc0d76b786392cda4fe54ca555b34e688d1485952eae4 -5ce277a4947030b490567ce66d3dfeeba25fe19542d3b774e0a583ccaaa7447dd2ee5ac98d6a8a7fe6c6d8332bcdeeaff5b510383fc641b916bed5361c9f5ec7cf86e01f7580272b1da6345451e23030cfb6aff0c288e1da4d235704f482633c8843ab240c69097031f6a7f102d63e76a08792000531248703783bcf6de03c7 -27 +rem e3347d436ad61c17cbdbf7c7a4b6aff9e6180cd3083aca654cb44969ec559c276cec733966bae9a7df4978c459b9caccc925af10e12b849d4df0089e0a9c1d6a06f87d122c7b562d8158cd52670312f5c7bcf820d5fa4faa91b7d4eb7a337e3f2f17422f67a92e7932bcc0d76b786392cda4fe54ca555b34e688d1485952eae4 -5ce277a4947030b490567ce66d3dfeeba25fe19542d3b774e0a583ccaaa7447dd2ee5ac98d6a8a7fe6c6d8332bcdeeaff5b510383fc641b916bed5361c9f5ec7cf86e01f7580272b1da6345451e23030cfb6aff0c288e1da4d235704f482633c8843ab240c69097031f6a7f102d63e76a08792000531248703783bcf6de03c7 cc79a24104a55fac092755fa6f929b8a4e56f73556bb386920d8270c5de534bac775ee1e0738101cc4c9c79ef3d4ffe23c5787c5b8446a267ee0ea24d7a662fd1fbac57e12f6b46913adc4df6bbd7ecd6fab45fbcca92675b1b0cf6635ac5ba2f2510789692777b8f3877bf48e2b51a65a8a74bdad922bce13bf7ebd805793 +and e3347d436ad61c17cbdbf7c7a4b6aff9e6180cd3083aca654cb44969ec559c276cec733966bae9a7df4978c459b9caccc925af10e12b849d4df0089e0a9c1d6a06f87d122c7b562d8158cd52670312f5c7bcf820d5fa4faa91b7d4eb7a337e3f2f17422f67a92e7932bcc0d76b786392cda4fe54ca555b34e688d1485952eae4 -5ce277a4947030b490567ce66d3dfeeba25fe19542d3b774e0a583ccaaa7447dd2ee5ac98d6a8a7fe6c6d8332bcdeeaff5b510383fc641b916bed5361c9f5ec7cf86e01f7580272b1da6345451e23030cfb6aff0c288e1da4d235704f482633c8843ab240c69097031f6a7f102d63e76a08792000531248703783bcf6de03c7 e230580122901c1482da900180242011441800c20812c00000b401412455882020c012116628410001011044490100040024ae10600380844c90008c0a140802020011120823540d00008c12220110f4c3049000d1d241a21125c08b3033580c2713400d27292e6830a080806b50001085a48654ca044934868850400900e820 +or e3347d436ad61c17cbdbf7c7a4b6aff9e6180cd3083aca654cb44969ec559c276cec733966bae9a7df4978c459b9caccc925af10e12b849d4df0089e0a9c1d6a06f87d122c7b562d8158cd52670312f5c7bcf820d5fa4faa91b7d4eb7a337e3f2f17422f67a92e7932bcc0d76b786392cda4fe54ca555b34e688d1485952eae4 -5ce277a4947030b490567ce66d3dfeeba25fe19542d3b774e0a583ccaaa7447dd2ee5ac98d6a8a7fe6c6d8332bcdeeaff5b510383fc641b916bed5361c9f5ec7cf86e01f7580272b1da6345451e23030cfb6aff0c288e1da4d235704f482633c8843ab240c69097031f6a7f102d63e76a08792000531248703783bcf6de03c7 -4ca02380101030800040008424150061825f20854053112020a101402aa604091028484984400002024050322041422365a500302d46002900be5416141e08478000201d300005230822205001c2102084302df080080152440211005480000c08038900046908601032a28100500652208012000020048103702b4a68c0103 +xor e3347d436ad61c17cbdbf7c7a4b6aff9e6180cd3083aca654cb44969ec559c276cec733966bae9a7df4978c459b9caccc925af10e12b849d4df0089e0a9c1d6a06f87d122c7b562d8158cd52670312f5c7bcf820d5fa4faa91b7d4eb7a337e3f2f17422f67a92e7932bcc0d76b786392cda4fe54ca555b34e688d1485952eae4 -5ce277a4947030b490567ce66d3dfeeba25fe19542d3b774e0a583ccaaa7447dd2ee5ac98d6a8a7fe6c6d8332bcdeeaff5b510383fc641b916bed5361c9f5ec7cf86e01f7580272b1da6345451e23030cfb6aff0c288e1da4d235704f482633c8843ab240c69097031f6a7f102d63e76a08792000531248703783bcf6de03c7 -e6fa5a3923911f1c82de9009c26570175c3df2ca5c17f11202be115526ffe860b1c29695fe6c4100212515476b051426367efe1362d7e086dc9be5cd6b55e8867a001313db23545f3082ae17221d31f6cb4792dfd9d2c1b73565e19b357b580ce793789d276fbeee31a3aaa87b550075a7ac8774ca06497c96bf52f4af8ce923 +shl e3347d436ad61c17cbdbf7c7a4b6aff9e6180cd3083aca654cb44969ec559c276cec733966bae9a7df4978c459b9caccc925af10e12b849d4df0089e0a9c1d6a06f87d122c7b562d8158cd52670312f5c7bcf820d5fa4faa91b7d4eb7a337e3f2f17422f67a92e7932bcc0d76b786392cda4fe54ca555b34e688d1485952eae4 201 1c668fa86d5ac382f97b7ef8f496d5ff3cc3019a6107594ca996892d3d8ab384ed9d8e672cd75d34fbe92f188b3739599924b5e21c257093a9be0113c15383ad40df0fa2458f6ac5b02b19aa4ce0625eb8f79f041abf49f55236fa9d6f466fc7e5e2e845ecf525cf2657981aed6f0c7259b49fca994aab669cd11a290b2a5d5c800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +shr e3347d436ad61c17cbdbf7c7a4b6aff9e6180cd3083aca654cb44969ec559c276cec733966bae9a7df4978c459b9caccc925af10e12b849d4df0089e0a9c1d6a06f87d122c7b562d8158cd52670312f5c7bcf820d5fa4faa91b7d4eb7a337e3f2f17422f67a92e7932bcc0d76b786392cda4fe54ca555b34e688d1485952eae4 201 719a3ea1b56b0e0be5edfbe3d25b57fcf30c0669841d6532a65a24b4f62ace13b676399cb35d74d3efa4bc622cdce5666492d7887095c24ea6f8044f054e0eb5 +add -fe92e0afe067b54283f97e454b1b42b05289f602ed1b89c2a06f300febcf753e02321511e5973914ca63f0001089e270bcb1f2dca5288fbfd29126aba84829a0b3cfebc6fb445588ccb160781fd035d58f08ab2614fd830a81a534df6ee1f7e6b5041c5e0e92c5d1a61da14c20145fd35e446f916f32d11745c4ebdf2d372cb4 fc1ccf03d3bfd72f72c1ee53f75f06da309e2c604262b931769267a8f00452c04584ea53a0ea556572f3a3c1fc25e64c2649375b4564aa23d8c6974abc5bb42dc775e821237cf222c87df887121eb767bdab9840603b8ec0333880c447d68019e91c39738f993ec19b21f4b254f173f628047a5ddf6825f36d6f204e4a616af -eed113bfa32bb7cf8ccd5f600ba55242af80133ce8f55e2f890609955ccf3011fdd9c66cab8893be7334b5c3f0c7840bfa4d5f66f0d2451d9504bd36fc826e5dd7588d44e90c8666a02980efaeae4a5f132df1a20ef9ca1e7e71acd32a648fe5167258c6d59931e58c6b8200fac54893fbc427eb913c4eb80eedf9da48911605 +sub -fe92e0afe067b54283f97e454b1b42b05289f602ed1b89c2a06f300febcf753e02321511e5973914ca63f0001089e270bcb1f2dca5288fbfd29126aba84829a0b3cfebc6fb445588ccb160781fd035d58f08ab2614fd830a81a534df6ee1f7e6b5041c5e0e92c5d1a61da14c20145fd35e446f916f32d11745c4ebdf2d372cb4 fc1ccf03d3bfd72f72c1ee53f75f06da309e2c604262b931769267a8f00452c04584ea53a0ea556572f3a3c1fc25e64c2649375b4564aa23d8c6974abc5bb42dc775e821237cf222c87df887121eb767bdab9840603b8ec0333880c447d68019e91c39738f993ec19b21f4b254f173f628047a5ddf6825f36d6f204e4a616af -10e54ada01da3b2b57b259d2a8a91331df593d8c8f141b555b7d8568a7acfba6a068a63b71fa5de6b21932a3c304c40d57f168652597eda62101d9020540de4e390474a490d7c24aaf939400090f2214c0ae364aa1b013bf684d8bcebb35f5fe85395dff5478c59bdbfcfc09745637712c0c4b7374d2953767c9bdde411dd4363 +mul -fe92e0afe067b54283f97e454b1b42b05289f602ed1b89c2a06f300febcf753e02321511e5973914ca63f0001089e270bcb1f2dca5288fbfd29126aba84829a0b3cfebc6fb445588ccb160781fd035d58f08ab2614fd830a81a534df6ee1f7e6b5041c5e0e92c5d1a61da14c20145fd35e446f916f32d11745c4ebdf2d372cb4 fc1ccf03d3bfd72f72c1ee53f75f06da309e2c604262b931769267a8f00452c04584ea53a0ea556572f3a3c1fc25e64c2649375b4564aa23d8c6974abc5bb42dc775e821237cf222c87df887121eb767bdab9840603b8ec0333880c447d68019e91c39738f993ec19b21f4b254f173f628047a5ddf6825f36d6f204e4a616af -fab53b1a461b4f0becef0f656d3cbc19cec2e71074bf1d4b1f066be5ddf60729557ed86b2b97827c3f36ffd83ad65ce05a22a69e7d63310eadb1793dbac3708b9673af7d03ac09dc49adf8243627808cd383578c69bf898a2be92b6872cb39ecf3eae79a76aceac87ae4d5af8e0b3a4aa9ca28771c31ae174388598165b9c7893643c32affec0eabfbc7f3a68fb211e0d5f0947137a22a55a29b36f3be4d662ee908610f75dce99b3e45f3cd2a45facb49f8fdacc9400b80401e39ae0fea289b37104b1561cb4c7b8e199860d8ddd3e730103cb8f5035ec92a6c2bd355c18664f8bcc6130a1de032735ae981bc0ec9b94a3ef3d543aa53f8875bbd9f347070c +div -fe92e0afe067b54283f97e454b1b42b05289f602ed1b89c2a06f300febcf753e02321511e5973914ca63f0001089e270bcb1f2dca5288fbfd29126aba84829a0b3cfebc6fb445588ccb160781fd035d58f08ab2614fd830a81a534df6ee1f7e6b5041c5e0e92c5d1a61da14c20145fd35e446f916f32d11745c4ebdf2d372cb4 fc1ccf03d3bfd72f72c1ee53f75f06da309e2c604262b931769267a8f00452c04584ea53a0ea556572f3a3c1fc25e64c2649375b4564aa23d8c6974abc5bb42dc775e821237cf222c87df887121eb767bdab9840603b8ec0333880c447d68019e91c39738f993ec19b21f4b254f173f628047a5ddf6825f36d6f204e4a616af -10 +rem -fe92e0afe067b54283f97e454b1b42b05289f602ed1b89c2a06f300febcf753e02321511e5973914ca63f0001089e270bcb1f2dca5288fbfd29126aba84829a0b3cfebc6fb445588ccb160781fd035d58f08ab2614fd830a81a534df6ee1f7e6b5041c5e0e92c5d1a61da14c20145fd35e446f916f32d11745c4ebdf2d372cb4 fc1ccf03d3bfd72f72c1ee53f75f06da309e2c604262b931769267a8f00452c04584ea53a0ea556572f3a3c1fc25e64c2649375b4564aa23d8c6974abc5bb42dc775e821237cf222c87df887121eb767bdab9840603b8ec0333880c447d68019e91c39738f993ec19b21f4b254f173f628047a5ddf6825f36d6f204e4a616af -27611ac0ca7de1311378ff153bc3bd621ebc9a2aab8d09129dcc866fbcb227dbcad2abe44ace3af57704c3e1463fc249668bb815fc3e59bf9ca8f60ebec7572ec5a03a5d7c76366043367f10db17e6dd15d12e5b4c1f44a4e6cb41b270b77cccbe7e2ea7ef987100afbac99cb22ebdd363ff5338fcaab23d855cb90e2d5c1c4 +and -fe92e0afe067b54283f97e454b1b42b05289f602ed1b89c2a06f300febcf753e02321511e5973914ca63f0001089e270bcb1f2dca5288fbfd29126aba84829a0b3cfebc6fb445588ccb160781fd035d58f08ab2614fd830a81a534df6ee1f7e6b5041c5e0e92c5d1a61da14c20145fd35e446f916f32d11745c4ebdf2d372cb4 fc1ccf03d3bfd72f72c1ee53f75f06da309e2c604262b931769267a8f00452c04584ea53a0ea556572f3a3c1fc25e64c2649375b4564aa23d8c6974abc5bb42dc775e821237cf222c87df887121eb767bdab9840603b8ec0333880c447d68019e91c39738f993ec19b21f4b254f173f628047a5ddf6825f36d6f204e4a616af 1410c501d184830740400a03464b04da10000c400242211170006700400000004484aa41a088442150c0a3c0f421c0442440121105640002d0c4954038592424c30140000338a2220069f806021ca2270d21080020238e402128800001c08010a91c3813069122c19a21e03054b002c2080002490c4024832121000c080120c +or -fe92e0afe067b54283f97e454b1b42b05289f602ed1b89c2a06f300febcf753e02321511e5973914ca63f0001089e270bcb1f2dca5288fbfd29126aba84829a0b3cfebc6fb445588ccb160781fd035d58f08ab2614fd830a81a534df6ee1f7e6b5041c5e0e92c5d1a61da14c20145fd35e446f916f32d11745c4ebdf2d372cb4 fc1ccf03d3bfd72f72c1ee53f75f06da309e2c604262b931769267a8f00452c04584ea53a0ea556572f3a3c1fc25e64c2649375b4564aa23d8c6974abc5bb42dc775e821237cf222c87df887121eb767bdab9840603b8ec0333880c447d68019e91c39738f993ec19b21f4b254f173f628047a5ddf6825f36d6f204e4a616af -f012200fc044000000d16000400a029050801400e9198040a006100560cf301202221110c59118008840c0000009a0103c9160880128851dc211068b000800a02388a144e9401088c03020700ed014818400022210fc0302808434d32a8097e621041c4806024411a60da004001048c01c44281022005100410009db09112811 +xor -fe92e0afe067b54283f97e454b1b42b05289f602ed1b89c2a06f300febcf753e02321511e5973914ca63f0001089e270bcb1f2dca5288fbfd29126aba84829a0b3cfebc6fb445588ccb160781fd035d58f08ab2614fd830a81a534df6ee1f7e6b5041c5e0e92c5d1a61da14c20145fd35e446f916f32d11745c4ebdf2d372cb4 fc1ccf03d3bfd72f72c1ee53f75f06da309e2c604262b931769267a8f00452c04584ea53a0ea556572f3a3c1fc25e64c2649375b4564aa23d8c6974abc5bb42dc775e821237cf222c87df887121eb767bdab9840603b8ec0333880c447d68019e91c39738f993ec19b21f4b254f173f628047a5ddf6825f36d6f204e4a616af -f1532c5fdd5c483074d560a0746eb2ddf18014c4e93da251b706167564cf3012066a5bb4df999c429d4cca3c0f4bbc147ed561a9117ec51def1d4fdf038d92e26fb8b544e9739aaae036bff06ef1dea3f4d212a212fe3be68296bcd32a9c9fe72b95dfc9366b563dbfafbe07055b48ec3cc42834b2c45348731219dbc9913a1d +shl -fe92e0afe067b54283f97e454b1b42b05289f602ed1b89c2a06f300febcf753e02321511e5973914ca63f0001089e270bcb1f2dca5288fbfd29126aba84829a0b3cfebc6fb445588ccb160781fd035d58f08ab2614fd830a81a534df6ee1f7e6b5041c5e0e92c5d1a61da14c20145fd35e446f916f32d11745c4ebdf2d372cb4 201 -1fd25c15fc0cf6a8507f2fc8a96368560a513ec05da37138540de601fd79eea7c04642a23cb2e722994c7e0002113c4e17963e5b94a511f7fa5224d5750905341679fd78df688ab119962c0f03fa06bab1e11564c29fb0615034a69beddc3efcd6a0838bc1d258ba34c3b42984028bfa6bc88df22de65a22e8b89d7be5a6e596800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +shr -fe92e0afe067b54283f97e454b1b42b05289f602ed1b89c2a06f300febcf753e02321511e5973914ca63f0001089e270bcb1f2dca5288fbfd29126aba84829a0b3cfebc6fb445588ccb160781fd035d58f08ab2614fd830a81a534df6ee1f7e6b5041c5e0e92c5d1a61da14c20145fd35e446f916f32d11745c4ebdf2d372cb4 201 -7f497057f033daa141fcbf22a58da1582944fb01768dc4e150379807f5e7ba9f01190a88f2cb9c8a6531f8000844f1385e58f96e529447dfe9489355d42414d1 From 75f8be1dd3ee7dcb9dd26986cd8353d9c534e70d Mon Sep 17 00:00:00 2001 From: LunaStev Date: Sun, 27 Sep 2026 13:10:19 +0900 Subject: [PATCH 2/3] Fix Darwin syscall outputs and portable regression fixtures Declare the Darwin secondary return register so optimized syscall wrappers do not reuse clobbered third arguments. Normalize provider fixture line endings, wait for UDP readiness before zero-capacity receive assertions, and add regression coverage and failure diagnostics. Signed-off-by: LunaStev --- std/sys/macos/amd64/syscall.wave | 17 +++++ tests/fixtures/stabilization_17/tcp_zero.wave | 6 +- tests/stabilization_17.rs | 70 ++++++++++++++++++- tools/test_std_boundary_runtime.py | 9 +-- 4 files changed, 94 insertions(+), 8 deletions(-) diff --git a/std/sys/macos/amd64/syscall.wave b/std/sys/macos/amd64/syscall.wave index 3d75fc54..0654fb22 100644 --- a/std/sys/macos/amd64/syscall.wave +++ b/std/sys/macos/amd64/syscall.wave @@ -21,6 +21,9 @@ // ======================================================= // // Raw syscall helpers for Darwin. +// XNU overwrites rdx with its secondary return value, including for calls +// that take their third argument in rdx. Declare that output so inlining +// cannot reuse an input register value after a syscall. // Return values follow kernel convention: // negative return value = -errno // ======================================================= @@ -29,29 +32,34 @@ pub const MACOS_SYSCALL_BASE: i64 = 0x2000000; pub fun syscall0(id: i64) -> i64 { var ret: i64; + var secondary: i64; var sysno: i64 = MACOS_SYSCALL_BASE + id; asm { "syscall\njnc 1f\nneg rax\n1:" in("rax") sysno out("rax") ret + out("rdx") secondary } return ret; } pub fun syscall1(id: i64, a1: i64) -> i64 { var ret: i64; + var secondary: i64; var sysno: i64 = MACOS_SYSCALL_BASE + id; asm { "syscall\njnc 1f\nneg rax\n1:" in("rax") sysno in("rdi") a1 out("rax") ret + out("rdx") secondary } return ret; } pub fun syscall2(id: i64, a1: i64, a2: i64) -> i64 { var ret: i64; + var secondary: i64; var sysno: i64 = MACOS_SYSCALL_BASE + id; asm { "syscall\njnc 1f\nneg rax\n1:" @@ -59,12 +67,14 @@ pub fun syscall2(id: i64, a1: i64, a2: i64) -> i64 { in("rdi") a1 in("rsi") a2 out("rax") ret + out("rdx") secondary } return ret; } pub fun syscall3(id: i64, a1: i64, a2: i64, a3: i64) -> i64 { var ret: i64; + var secondary: i64; var sysno: i64 = MACOS_SYSCALL_BASE + id; asm { "syscall\njnc 1f\nneg rax\n1:" @@ -73,12 +83,14 @@ pub fun syscall3(id: i64, a1: i64, a2: i64, a3: i64) -> i64 { in("rsi") a2 in("rdx") a3 out("rax") ret + out("rdx") secondary } return ret; } pub fun syscall4(id: i64, a1: i64, a2: i64, a3: i64, a4: i64) -> i64 { var ret: i64; + var secondary: i64; var sysno: i64 = MACOS_SYSCALL_BASE + id; asm { "syscall\njnc 1f\nneg rax\n1:" @@ -88,12 +100,14 @@ pub fun syscall4(id: i64, a1: i64, a2: i64, a3: i64, a4: i64) -> i64 { in("rdx") a3 in("r10") a4 out("rax") ret + out("rdx") secondary } return ret; } pub fun syscall5(id: i64, a1: i64, a2: i64, a3: i64, a4: i64, a5: i64) -> i64 { var ret: i64; + var secondary: i64; var sysno: i64 = MACOS_SYSCALL_BASE + id; asm { "syscall\njnc 1f\nneg rax\n1:" @@ -104,12 +118,14 @@ pub fun syscall5(id: i64, a1: i64, a2: i64, a3: i64, a4: i64, a5: i64) -> i64 { in("r10") a4 in("r8") a5 out("rax") ret + out("rdx") secondary } return ret; } pub fun syscall6(id: i64, a1: i64, a2: i64, a3: i64, a4: i64, a5: i64, a6: i64) -> i64 { var ret: i64; + var secondary: i64; var sysno: i64 = MACOS_SYSCALL_BASE + id; asm { "syscall\njnc 1f\nneg rax\n1:" @@ -121,6 +137,7 @@ pub fun syscall6(id: i64, a1: i64, a2: i64, a3: i64, a4: i64, a5: i64, a6: i64) in("r8") a5 in("r9") a6 out("rax") ret + out("rdx") secondary } return ret; } diff --git a/tests/fixtures/stabilization_17/tcp_zero.wave b/tests/fixtures/stabilization_17/tcp_zero.wave index e8866966..f2ae1e29 100644 --- a/tests/fixtures/stabilization_17/tcp_zero.wave +++ b/tests/fixtures/stabilization_17/tcp_zero.wave @@ -4,6 +4,7 @@ import("std::net::address")::{SocketAddrV4}; import("std::net::error")::{NetResult, NetIoResult}; import("std::net::vectored")::{NetIoSlice, net_recv_vectored}; import("std::net::udp")::{UdpSocket, udp_bind_loopback, udp_local_addr_v4, udp_connect_v4, udp_send, udp_recv, udp_recv_vectored, udp_close}; +import("std::net::poll")::{net_wait_readable}; fun good_zero(result: NetIoResult) -> bool { return result.count == 0 && !result.eof && result.error.kind == 0; } fun main() -> i32 { var listener: NetResult = tcp_bind_loopback(0); @@ -37,11 +38,14 @@ fun main() -> i32 { var sender: NetResult = udp_connect_v4(udp_address.value); if (!sender.ok) { return 14; } if (!good_zero(udp_send(sender.value, &octet, 0)) || udp_send(sender.value, &octet, 1).count != 1) { return 15; } + // A zero-capacity native receive may return before a datagram arrives. + // Wait for a queued message before asserting that this call consumes it. + if (net_wait_readable(receiver.value.fd, 1000) != 1) { return 18; } empty[1].length = 0; if (!good_zero(udp_recv_vectored(receiver.value, &empty[0], 2))) { return 16; } // The empty datagram was consumed, so the next receive sees the marker. read = udp_recv(receiver.value, &octet, 1); - if (read.count != 1 || read.eof || octet != 65) { return 17; } + if (read.count != 1 || read.eof || read.error.kind != 0 || octet != 65) { println("UDP marker: count={} eof={} error={} value={}", read.count, read.eof, read.error.kind, octet); return 17; } udp_close(sender.value); udp_close(receiver.value); return 0; } diff --git a/tests/stabilization_17.rs b/tests/stabilization_17.rs index 37fcf44a..0f57cc54 100644 --- a/tests/stabilization_17.rs +++ b/tests/stabilization_17.rs @@ -175,6 +175,42 @@ fn numeric_constants_reject_invalid_shifts_and_casts() { utils::json::parse(String::from_utf8_lossy(&o.stderr).trim()).unwrap(); } } +fn environment_fixture(provider: &str, fixture: &str) -> String { + // Git's Windows checkout uses CRLF; never silently leave the real import + // beside the mock declaration when replacing the provider boundary. + let provider = provider.replace("\r\n", "\n").replace('\r', "\n"); + let import = "import(\"std::sys::env\")::{\n env_read,\n};"; + assert_eq!( + provider.matches(import).count(), + 1, + "env_read provider import changed" + ); + format!("{}\n{fixture}", provider.replacen(import, "", 1)) +} + +#[test] +fn environment_fixture_replaces_the_provider_for_all_line_endings() { + let case = Case::new(); + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + let provider = fs::read_to_string(root.join("std/env/environ.wave")) + .unwrap() + .replace("\r\n", "\n") + .replace('\r', "\n"); + let fixture = + fs::read_to_string(root.join("tests/fixtures/stabilization_17/environment.wave")).unwrap(); + for newline in ["\n", "\r\n", "\r"] { + case.source(&environment_fixture( + &provider.replace('\n', newline), + &fixture, + )); + ok(case + .command() + .args(["check", "case.wave"]) + .output() + .unwrap()); + } +} + #[test] fn checked_numeric_runtime_and_byte_apis() { if !native() { @@ -191,9 +227,8 @@ fn checked_numeric_runtime_and_byte_apis() { let provider = fs::read_to_string( Path::new(env!("CARGO_MANIFEST_DIR")).join("std/env/environ.wave"), ) - .unwrap() - .replace("import(\"std::sys::env\")::{\n env_read,\n};", ""); - source = format!("{provider}\n{source}"); + .unwrap(); + source = environment_fixture(&provider, &source); } case.run(&source); } @@ -295,3 +330,32 @@ fn webassembly_checked_numeric_runtime() { } } } + +#[cfg(target_arch = "x86_64")] +#[test] +fn darwin_syscall_secondary_return_does_not_preserve_the_third_argument() { + if !native() { + return; + } + let provider = fs::read_to_string( + Path::new(env!("CARGO_MANIFEST_DIR")).join("std/sys/macos/amd64/syscall.wave"), + ) + .unwrap(); + assert_eq!(provider.matches("\"syscall\\n").count(), 7); + // Model the XNU register boundary without executing Darwin syscalls on + // another OS: echo the third input in rax, then overwrite rdx with zero. + let provider = provider.replace("\"syscall\\n", "\"mov rax, rdx\\nxor edx, edx\\n"); + let mut body = String::from("fun main() -> i32 {\n"); + for count in 3..=6 { + let mut args = vec![54, 1, 2, 7]; + args.extend(4..=count); + let args = args + .iter() + .map(ToString::to_string) + .collect::>() + .join(", "); + body.push_str(&format!("if (syscall{count}({args}) != 7 || syscall{count}({args}) != 7) {{ return {count}; }}\n")); + } + body.push_str("return 0; }"); + Case::new().run(&format!("{provider}\n{body}")); +} diff --git a/tools/test_std_boundary_runtime.py b/tools/test_std_boundary_runtime.py index a2f57f22..a8c387f1 100644 --- a/tools/test_std_boundary_runtime.py +++ b/tools/test_std_boundary_runtime.py @@ -43,7 +43,7 @@ def test_zero_length_tcp_reads_preserve_connection_and_queued_data(self): result = run_process([COMPILER, "--std-root", str(ROOT / "std"), "build", str(ROOT / "tests/fixtures/stabilization_17/tcp_zero.wave"), "--target", target, opt, "-o", str(binary)], timeout=60, capture_output=True, text=True) - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(result.returncode, 0, f"{opt}: {result.stdout} {result.stderr}") result = run_process([str(binary)], timeout=10, capture_output=True, text=True) self.assertEqual(result.returncode, 0, f"{target} {opt}: {result.stdout} {result.stderr}") @@ -62,7 +62,8 @@ def test_invalid_terminal_actions_preserve_pty_settings(self): if (tty_getattr({slave}, &term) != 0) {{ return 1; }} term.c_lflag = term.c_lflag ^ TTY_ECHO; if (tty_setattr({slave}, -1, &term) != -22 || tty_setattr({slave}, 99, &term) != -22) {{ return 2; }} - if (tty_getattr({slave}, &term) != 0) {{ return 3; }} + var status: i64 = tty_getattr({slave}, &term); + if (status != 0) {{ println("second tty_getattr: {{}}", status); return 3; }} if (tty_setattr({slave}, TTY_TCSANOW, &term) != 0 || tty_setattr({slave}, TTY_TCSADRAIN, &term) != 0 || tty_setattr({slave}, TTY_TCSAFLUSH, &term) != 0) {{ return 4; }} return 0; }}''') @@ -70,9 +71,9 @@ def test_invalid_terminal_actions_preserve_pty_settings(self): binary = Path(temp) / "tty" result = run_process([COMPILER, "--std-root", str(ROOT / "std"), "build", str(source), "--target", os.environ["WAVE_TEST_TARGET"], opt, "-o", str(binary)], timeout=60, capture_output=True, text=True) - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(result.returncode, 0, f"{opt}: {result.stdout} {result.stderr}") result = run_process([str(binary)], pass_fds=(slave,), timeout=10, capture_output=True, text=True) - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(result.returncode, 0, f"{opt}: {result.stdout} {result.stderr}") self.assertEqual(termios.tcgetattr(slave), initial) finally: os.close(slave) From 871035d73d607252633270ef2c597844c8bc0f40 Mon Sep 17 00:00:00 2001 From: LunaStev Date: Sun, 27 Sep 2026 13:41:49 +0900 Subject: [PATCH 3/3] Fix FreeBSD serial prompt detection with interleaved kernel output Signed-off-by: LunaStev --- tools/check_freebsd_sys.py | 9 +++-- tools/test_freebsd_runtime_reporting.py | 52 +++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/tools/check_freebsd_sys.py b/tools/check_freebsd_sys.py index 0ee0f56f..8e42f24f 100644 --- a/tools/check_freebsd_sys.py +++ b/tools/check_freebsd_sys.py @@ -19,6 +19,9 @@ ROOT = Path(__file__).resolve().parents[1] ARCHES = {"amd64": "x86_64", "arm64": "aarch64", "riscv64": "riscv64"} +# Kernel device messages can interleave with init's prompt (even inside +# '/bin/sh:'). Accept its prefix, then require the actual root shell prompt. +SINGLE_USER_PROMPT = r"Enter full pathname of shell" class Commands: @@ -182,10 +185,10 @@ def execute(args, report): if args.arch == "riscv64": # Direct boot preserves QEMU's FDT for FreeBSD 14.3. A kernel # without loader-provided root metadata asks for its root disk. - prompt = console.expect(r"mountroot> |RETURN for /bin/sh:") + prompt = console.expect(r"mountroot> |" + SINGLE_USER_PROMPT) if prompt.group(0).startswith("mountroot>"): console.send("ufs:/dev/vtbd0p3\n") - console.expect("RETURN for /bin/sh:") + console.expect(SINGLE_USER_PROMPT) else: console.expect("Autoboot in") console.send("3", paced=True) @@ -194,7 +197,7 @@ def execute(args, report): console.send("set console=comconsole\r", paced=True) console.expect(r"OK ") console.send("boot -s\r", paced=True) - console.expect("RETURN for /bin/sh:") + console.expect(SINGLE_USER_PROMPT) console.send("\r") console.expect(r"root@[^\r\n]*# ") console.send("mount -uw /\nmount -t cd9660 /dev/vtbd1 /mnt\nsh /mnt/run.sh\n") diff --git a/tools/test_freebsd_runtime_reporting.py b/tools/test_freebsd_runtime_reporting.py index b4dcf26e..08a6816e 100644 --- a/tools/test_freebsd_runtime_reporting.py +++ b/tools/test_freebsd_runtime_reporting.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: MPL-2.0 """Failure-path tests for the FreeBSD CI runner; no VM or network required.""" import argparse +import io import json from pathlib import Path import re @@ -94,6 +95,57 @@ def test_boot_failure_preserves_phase_and_stops_guest(self): self.assertTrue(all(c["status"] == "not_run" for c in report["cases"])) process.terminate.assert_called_once() + def replay_serial_boot(self, prompt, shell_ready=True): + # Exercise the real serial parser and boot sequence without a VM. + chunks = [b"Autoboot in 10 seconds. ", b"OK ", b"OK ", + prompt[:12], prompt[12:37], prompt[37:]] + if shell_ready: + chunks += [b"root@:/ # ", + b"WAVE-RESULT test1-O0 0\r\nWAVE-RESULT test1-O2 0\r\n" + b"WAVE-RESULT test2-O0 0\r\nWAVE-RESULT test2-O2 0\r\n"] + chunks = iter(chunks) + process = Mock() + process.stdin = io.BytesIO() + process.wait.return_value = 0 + process.poll.return_value = None + report = {"commands": [], "cases": []} + with patch.object(runner, "ROOT", self.root), \ + patch.object(runner.Commands, "run"), \ + patch.object(runner.subprocess, "Popen", return_value=process), \ + patch.object(runner.select, "select", return_value=([process.stdout], [], [])), \ + patch.object(runner.os, "read", side_effect=lambda *_: next(chunks, b"")), \ + patch.object(runner.time, "sleep"): + try: + runner.execute(self.args, report) + error = None + except Exception as exc: + error = exc + return report, process, error + + def test_serial_prompt_accepts_interleaved_device_output(self): + # Captured from PR #802's FreeBSD job 108548040634: cd0 output + # split the shell path and postponed the final colon to another line. + interleaved = ( + b"Enter full pathname of shell or RETURN for c/bin/shd0 at ata1 bus 0 scbus1 target 0 lun 0\r\n" + b"cd0: Removable CD-ROM SCSI device\r\n" + b"cd0: Attempt to query device size failed: NOT READY, Medium not present\r\n: ") + for prompt in [b"Enter full pathname of shell or RETURN for /bin/sh: ", interleaved]: + with self.subTest(prompt=prompt): + report, process, error = self.replay_serial_boot(prompt) + self.assertIsNone(error) + self.assertTrue(all(c["status"] == "pass" for c in report["cases"])) + self.assertIn(b"boot -s\r\rmount -uw /\n", process.stdin.getvalue()) + self.assertIn(prompt, (Path(report["work_dir"]) / "guest.log").read_bytes()) + + def test_serial_prompt_still_requires_root_shell_before_cases(self): + report, process, error = self.replay_serial_boot( + b"Enter full pathname of shell or RETURN for /bin/sh: ", shell_ready=False) + self.assertIsInstance(error, RuntimeError) + self.assertEqual(report["phase"], "guest_boot") + self.assertTrue(all(c["status"] == "not_run" for c in report["cases"])) + self.assertNotIn(b"sh /mnt/run.sh", process.stdin.getvalue()) + process.terminate.assert_called_once() + def test_command_failure_and_timeout_are_recorded(self): report = {"commands": []} command = runner.Commands(report, self.root / "build.log", 3)