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/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/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..f2ae1e29 --- /dev/null +++ b/tests/fixtures/stabilization_17/tcp_zero.wave @@ -0,0 +1,51 @@ +// 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}; +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); + 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; } + // 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 || 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/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..0f57cc54 --- /dev/null +++ b/tests/stabilization_17.rs @@ -0,0 +1,361 @@ +// 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(); + } +} +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() { + 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(); + source = environment_fixture(&provider, &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()); + } + } +} + +#[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/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) diff --git a/tools/test_std_boundary_runtime.py b/tools/test_std_boundary_runtime.py index 1ff8cc3a..a8c387f1 100644 --- a/tools/test_std_boundary_runtime.py +++ b/tools/test_std_boundary_runtime.py @@ -34,3 +34,47 @@ 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, 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}") + + @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; }} + 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; +}}''') + 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, 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, f"{opt}: {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