From ac0fede6e163bd82503c5769ab1d980bfd00b4b8 Mon Sep 17 00:00:00 2001 From: ureishi <57707826+ureishi@users.noreply.github.com> Date: Sat, 26 Sep 2026 02:45:41 +0900 Subject: [PATCH] fix constant expression evaluation Bind const field initializers in their declaring file, namespace and type before evaluating an initializer that has not yet been checked. Reuse the normal expression checker instead of guessing types and resolving names from syntax, so declaration order does not change compound assignment constant conversions. Preserve uint and ulong values when evaluating shifts and complements, and parse integral literals through i128 so ulong literals above i64::MAX remain available. Evaluate integral conditional expressions with constant boolean conditions, including const locals, without accepting bool as an integral conversion. Require both branches to be constant and bound recursive field binding as well as evaluation. Add regression coverage for all six affected integral target types, declaration order, qualified constants, sizeof, shifts, complements, conditional expressions, cross-file aliases and nested types. Check that out-of-range values, nonconstants, bool operands and cycles are rejected. --- src/men-sharp-compiler/tests/codegen.rs | 151 +++++++++ .../src/semantics/check.rs | 155 +++++----- .../src/semantics/check/checker.rs | 290 +++++------------- .../src/semantics/check/declarations.rs | 55 +++- .../src/semantics/check/exhaustive.rs | 13 +- .../src/semantics/check/statements.rs | 4 +- 6 files changed, 378 insertions(+), 290 deletions(-) diff --git a/src/men-sharp-compiler/tests/codegen.rs b/src/men-sharp-compiler/tests/codegen.rs index bfb7bd5..ac8649c 100644 --- a/src/men-sharp-compiler/tests/codegen.rs +++ b/src/men-sharp-compiler/tests/codegen.rs @@ -16065,3 +16065,154 @@ fn static_field_initializers_are_part_of_their_classs_initialization() { }; assert_eq!(int_of(&emulator, "x"), 11); } + +#[test] +fn compound_constants_keep_types_and_declaration_scope() { + // The RHS must be evaluated in its declaration's scope, with its real + // signedness and width, even before that initializer has been checked. + for expression in [ + "Other.N + 1", + "sizeof(System.Int32)", + "(int)(0xffffffffu >> 31)", + "(int)(0xffffffffffffffffUL >> 63)", + "(int)(((long)1 << 32) >> 32)", + "(int)(~0xfffffffeu)", + "(int)(~0xfffffffffffffffeUL)", + "true ? 1 : 2", + "Other.Flag && Other.N == 2 ? 1 : 2", + ] { + for target in ["sbyte", "byte", "short", "ushort", "uint", "ulong"] { + for before in [true, false] { + let constant = format!("const int K = {expression};"); + let method = format!("public void Interact() {{ {target} b = 1; b += K; }}"); + let body = if before { + format!("{constant} {method}") + } else { + format!("{method} {constant}") + }; + let source = format!( + "using MenSharp; public class Probe : MenSharpBehaviour {{ {body} }} + public class Other {{ public const int N = 2; public const bool Flag = true; }}" + ); + let Some(messages) = codegen_messages(&source, "Probe") else { + return; + }; + assert!( + messages.is_empty(), + "{target}: {expression}, before={before}: {messages:?}" + ); + } + } + } +} + +#[test] +fn compound_constants_resolve_aliases_in_the_declaring_file() { + // The caller has a different alias and a shadowing constant: neither + // may change a constant declared in another file or a nested type. + let mut sources = vec![ + SourceCode::new( + "caller.cs", + r#" + using MenSharp; + using Number = System.Int64; + public class Probe : MenSharpBehaviour { + const int N = 1000; + public void Interact() { byte b = 0; b += Values.Outer.Inner.K; } + } + "#, + ), + SourceCode::new( + "values.cs", + r#" + namespace Values { + using Number = System.Int32; + public class Outer { + public const int N = 2; + public class Inner { public const int K = Outer.N + sizeof(Number); } + } + } + "#, + ), + ]; + sources.extend(Compiler::corlib_sources()); + let Some(dir) = dotnet_shared_dir() else { + return; + }; + let compiler = Compiler::new(CompilerSettings::default()).unwrap(); + let bytes = vec![std::fs::read(dir.join("System.Private.CoreLib.dll")).unwrap()]; + let references = compiler.load_references(&bytes).unwrap(); + let files = compiler.parse(sources); + let declarations = compiler.collect_declarations(&files); + let signatures = compiler.resolve_signatures(&declarations, &references); + let bodies = compiler.check_bodies(&declarations, &signatures, &references); + assert!(bodies.errors.is_empty(), "{:#?}", bodies.errors); +} + +#[test] +fn compound_constants_do_not_admit_nonconstants_or_out_of_range_values() { + // A selected branch fitting byte does not make a nonconstant expression + // constant. Cycles must also terminate instead of recursively rebinding. + for declaration in [ + "const int K = true ? 256 : 1;", + "const int K = false ? 1 : -1;", + "const int K = (int)(0xffffffffu >> 23);", + "static readonly int K = 1;", + "const bool K = true;", + "static int X = 1; const int K = true ? 1 : X;", + "const int K = L; const int L = K;", + ] { + let mut sources = vec![SourceCode::new( + "test.cs", + format!( + "using MenSharp; public class Probe : MenSharpBehaviour {{ + public void Interact() {{ byte b = 0; b += K; }} {declaration} + }}" + ), + )]; + sources.extend(Compiler::corlib_sources()); + let Some(dir) = dotnet_shared_dir() else { + return; + }; + let compiler = Compiler::new(CompilerSettings::default()).unwrap(); + let bytes = vec![std::fs::read(dir.join("System.Private.CoreLib.dll")).unwrap()]; + let references = compiler.load_references(&bytes).unwrap(); + let files = compiler.parse(sources); + let declarations = compiler.collect_declarations(&files); + let signatures = compiler.resolve_signatures(&declarations, &references); + let bodies = compiler.check_bodies(&declarations, &signatures, &references); + assert!( + bodies.errors.iter().any(|error| matches!( + error.kind, + men_sharp_semantics::SemanticErrorKind::TypeMismatch { .. } + | men_sharp_semantics::SemanticErrorKind::InvalidOperator { .. } + )), + "{declaration}: {:#?}", + bodies.errors + ); + } +} + +#[test] +fn compound_constants_use_boolean_locals_without_converting_bool_to_int() { + // A const bool may select an integral constant, but the bool itself + // must never be accepted as an integral constant conversion. + let Some(messages) = codegen_messages( + r#" + using MenSharp; + public class Probe : MenSharpBehaviour { + public void Interact() { + const bool flag = true; + const bool other = !flag; + const int step = flag && !other ? 1 : 2; + byte b = 0; + b += step; + } + } + "#, + "Probe", + ) else { + return; + }; + assert!(messages.is_empty(), "{messages:?}"); +} diff --git a/src/men-sharp-semantics/src/semantics/check.rs b/src/men-sharp-semantics/src/semantics/check.rs index 5eafe31..a413812 100644 --- a/src/men-sharp-semantics/src/semantics/check.rs +++ b/src/men-sharp-semantics/src/semantics/check.rs @@ -337,75 +337,7 @@ pub fn check_file( ) -> BodyCheck { let file = &declarations.files[file_index]; - let mut checker = Checker { - resolver: Resolver { - declarations, - external, - file: file.file, - out: Signatures::default(), - }, - signatures, - scopes: Vec::new(), - type_stack: Vec::new(), - locals: Vec::new(), - this_type: None, - static_context: true, - return_type: Type::Void, - lambda_probe_returns: None, - lambda_stack: Vec::new(), - current_member: None, - captures: HashMap::default(), - captured_locals: HashMap::default(), - capture_types: HashMap::default(), - local_functions: HashMap::default(), - local_calls: HashMap::default(), - declared_names: HashMap::default(), - static_local_functions: HashSet::default(), - local_order: 0, - local_function_order: HashMap::default(), - expression_types: HashMap::default(), - numeric_promotions: HashMap::default(), - attribute_types: HashMap::default(), - targets: HashMap::default(), - pattern_inputs: HashMap::default(), - enumerations: HashMap::default(), - disposals: HashMap::default(), - constructor_chains: HashMap::default(), - catch_depth: 0, - in_async: false, - awaits: HashMap::default(), - iterator_element: None, - yield_seen: false, - value_return_seen: false, - guarded_depth: 0, - catch_depth_for_yield: 0, - finally_depth: 0, - iterators: HashSet::default(), - }; - - // rebuild the same file scope signature resolution used - checker.scopes.push(NamespaceScope { - path: Vec::new(), - symbol: Some(declarations.table.root()), - usings: Vec::new(), - }); - let mut root_usings = Vec::new(); - for (_, using) in declarations.global_usings() { - if let Some(resolved) = checker.resolve_using(using) { - root_usings.push(resolved); - } - } - for using in &file.usings { - if using.global.is_none() - && let Some(resolved) = checker.resolve_using(using) - { - root_usings.push(resolved); - } - } - // using-target resolution errors were already reported by the resolve phase; - // this rebuild must not duplicate them - checker.resolver.out.errors.clear(); - checker.scopes[0].usings = root_usings; + let mut checker = Checker::for_file(declarations, signatures, external, file_index); checker.walk_nodes(&file.members); checker.close_captures(); @@ -576,6 +508,8 @@ struct LocalFunctionEntry<'ast> { struct Checker<'a, 'ast> { resolver: Resolver<'a, 'ast>, + /// Bounds recursive const-field binding as well as value evaluation. + constant_depth: usize, signatures: &'a Signatures, scopes: Vec>, type_stack: Vec, @@ -686,3 +620,86 @@ fn type_parameter_leaves(ty: &Type) -> usize { _ => 0, } } + +impl<'a, 'ast> Checker<'a, 'ast> { + fn for_file( + declarations: &'a Declarations<'ast>, + signatures: &'a Signatures, + external: &'a dyn ExternalTypes, + file_index: usize, + ) -> Self { + let file = &declarations.files[file_index]; + let mut checker = Checker { + resolver: Resolver { + declarations, + external, + file: file.file, + out: Signatures::default(), + }, + signatures, + constant_depth: 64, + scopes: Vec::new(), + type_stack: Vec::new(), + locals: Vec::new(), + this_type: None, + static_context: true, + return_type: Type::Void, + lambda_probe_returns: None, + lambda_stack: Vec::new(), + current_member: None, + captures: HashMap::default(), + captured_locals: HashMap::default(), + capture_types: HashMap::default(), + local_functions: HashMap::default(), + local_calls: HashMap::default(), + declared_names: HashMap::default(), + static_local_functions: HashSet::default(), + local_order: 0, + local_function_order: HashMap::default(), + expression_types: HashMap::default(), + numeric_promotions: HashMap::default(), + attribute_types: HashMap::default(), + targets: HashMap::default(), + pattern_inputs: HashMap::default(), + enumerations: HashMap::default(), + disposals: HashMap::default(), + constructor_chains: HashMap::default(), + catch_depth: 0, + in_async: false, + awaits: HashMap::default(), + iterator_element: None, + yield_seen: false, + value_return_seen: false, + guarded_depth: 0, + catch_depth_for_yield: 0, + finally_depth: 0, + iterators: HashSet::default(), + }; + + // rebuild the same file scope signature resolution used + checker.scopes.push(NamespaceScope { + path: Vec::new(), + symbol: Some(declarations.table.root()), + usings: Vec::new(), + }); + let mut root_usings = Vec::new(); + for (_, using) in declarations.global_usings() { + if let Some(resolved) = checker.resolve_using(using) { + root_usings.push(resolved); + } + } + for using in &file.usings { + if using.global.is_none() + && let Some(resolved) = checker.resolve_using(using) + { + root_usings.push(resolved); + } + } + // using-target resolution errors were already reported by the resolve phase; + // this rebuild must not duplicate them + checker.resolver.out.errors.clear(); + checker.scopes[0].usings = root_usings; + + checker + } +} diff --git a/src/men-sharp-semantics/src/semantics/check/checker.rs b/src/men-sharp-semantics/src/semantics/check/checker.rs index de60c6e..9af0e1b 100644 --- a/src/men-sharp-semantics/src/semantics/check/checker.rs +++ b/src/men-sharp-semantics/src/semantics/check/checker.rs @@ -303,10 +303,14 @@ impl<'a, 'ast> Checker<'a, 'ast> { /// Keep values only for const locals, never for ordinary variables whose /// initializer happens to be a literal. pub(super) fn integer_constant(&self, expression: &Expression<'ast, 'ast>) -> Option { - self.integer_constant_inner(expression, 64) + let ty = self.expression_types.get(&EntityID::from(expression))?; + self.system() + .numeric_kind(ty) + .filter(|kind| kind.is_integral())?; + self.constant_value_inner(expression, self.constant_depth) } - fn integer_constant_inner( + pub(super) fn constant_value_inner( &self, expression: &Expression<'ast, 'ast>, depth: usize, @@ -315,11 +319,13 @@ impl<'a, 'ast> Checker<'a, 'ast> { if depth == 0 { return None; } - let Some(ty) = self.expression_types.get(&EntityID::from(expression)) else { - return super::exhaustive::integer_literal_value(expression).map(i128::from); - }; - let kind = self.system().numeric_kind(ty)?; - if !kind.is_integral() { + let ty = self.expression_types.get(&EntityID::from(expression))?; + let boolean = *ty == self.corlib("Boolean"); + let kind = self + .system() + .numeric_kind(ty) + .filter(|kind| kind.is_integral()); + if kind.is_none() && !boolean { return None; } let value = match expression { @@ -348,6 +354,8 @@ impl<'a, 'ast> Checker<'a, 'ast> { match member.constant.as_ref()? { crate::ExternalConstant::Int(v) => i128::from(*v), crate::ExternalConstant::UInt(v) => i128::from(*v), + crate::ExternalConstant::Boolean(v) => i128::from(*v), + crate::ExternalConstant::Char(v) => i128::from(*v as u32), _ => return None, } } @@ -359,7 +367,7 @@ impl<'a, 'ast> Checker<'a, 'ast> { } Expression::Primary(primary) if primary.chain.is_empty() => match &primary.left { PrimaryLeft::Parenthesized { expression, .. } => { - self.integer_constant_inner(expression, depth - 1)? + self.constant_value_inner(expression, depth - 1)? } PrimaryLeft::Identifier { name, .. } => { self.locals @@ -372,23 +380,30 @@ impl<'a, 'ast> Checker<'a, 'ast> { target_type: Ok(target_type), .. } => i128::from(self.sizeof_constant(target_type)?), - _ => i128::from(super::exhaustive::integer_literal_value(expression)?), + PrimaryLeft::Literal(LiteralExpression::True(_)) => 1, + PrimaryLeft::Literal(LiteralExpression::False(_)) => 0, + _ => super::exhaustive::wide_integer_literal_value(expression)?, }, Expression::Cast(cast) => { - self.integer_constant_inner(cast.value.as_ref().ok()?, depth - 1)? + self.constant_value_inner(cast.value.as_ref().ok()?, depth - 1)? } Expression::Unary(unary) => { - let value = self.integer_constant_inner(unary.operand.as_ref().ok()?, depth - 1)?; + let value = self.constant_value_inner(unary.operand.as_ref().ok()?, depth - 1)?; match unary.operator.value { UnaryOperator::Plus => value, UnaryOperator::Minus => value.checked_neg()?, - UnaryOperator::BitwiseNot => !value, + UnaryOperator::BitwiseNot => match kind? { + UInt32 => i128::from(!(value as u32)), + UInt64 => i128::from(!(value as u64)), + _ => !value, + }, + UnaryOperator::Not => i128::from(value == 0), _ => return None, } } Expression::Binary(binary) => { - let left = self.integer_constant_inner(&binary.left, depth - 1)?; - let right = self.integer_constant_inner(binary.right.as_ref().ok()?, depth - 1)?; + let left = self.constant_value_inner(&binary.left, depth - 1)?; + let right = self.constant_value_inner(binary.right.as_ref().ok()?, depth - 1)?; match binary.operator.value { BinaryOperator::Add => left.checked_add(right)?, BinaryOperator::Subtract => left.checked_sub(right)?, @@ -398,9 +413,18 @@ impl<'a, 'ast> Checker<'a, 'ast> { BinaryOperator::BitwiseAnd => left & right, BinaryOperator::BitwiseOr => left | right, BinaryOperator::BitwiseXor => left ^ right, + BinaryOperator::Equal => i128::from(left == right), + BinaryOperator::NotEqual => i128::from(left != right), + BinaryOperator::LessThan => i128::from(left < right), + BinaryOperator::LessThanEqual => i128::from(left <= right), + BinaryOperator::GreaterThan => i128::from(left > right), + BinaryOperator::GreaterThanEqual => i128::from(left >= right), + BinaryOperator::LogicalAnd => i128::from(left != 0 && right != 0), + BinaryOperator::LogicalOr => i128::from(left != 0 || right != 0), BinaryOperator::LeftShift | BinaryOperator::RightShift | BinaryOperator::UnsignedRightShift => { + let kind = kind?; let bits = if matches!(kind, Int64 | UInt64) { 64 } else { @@ -428,9 +452,22 @@ impl<'a, 'ast> Checker<'a, 'ast> { _ => return None, } } + Expression::Conditional(conditional) => { + // Both branches must be constant, including the unselected one. + let condition = self.constant_value_inner(&conditional.condition, depth - 1)?; + let yes = + self.constant_value_inner(conditional.then_value.as_ref().ok()?, depth - 1)?; + let no = + self.constant_value_inner(conditional.else_value.as_ref().ok()?, depth - 1)?; + if condition != 0 { yes } else { no } + } _ => return None, }; - integer_fits(kind, value).then_some(value) + if boolean { + matches!(value, 0 | 1).then_some(value) + } else { + integer_fits(kind?, value).then_some(value) + } } /// The initializer expression of a source `const` field, or `None` for @@ -452,215 +489,38 @@ impl<'a, 'ast> Checker<'a, 'ast> { }) } - /// The value of a source `const` field. Its initializer may not have - /// been checked yet — it sits below the use, or in another class — so - /// an unchecked one is evaluated structurally against the field's - /// declared type rather than through the types recorded for it (which - /// made the answer depend on declaration order). + /// Bind an as-yet unchecked initializer in its own file/type scope. + /// Reusing the normal checker preserves aliases, qualified names and + /// integer promotion; a structural evaluator used to guess these from + /// syntax and made constant conversion depend on declaration order. fn source_const_value(&self, field: SymbolId, depth: usize) -> Option { - let value = self.const_initializer(field)?; - if self.expression_types.contains_key(&EntityID::from(value)) { - return self.integer_constant_inner(value, depth); - } - let kind = match self.signatures.members.get(&field) { - Some(crate::types::MemberSignature::Field(ty)) => self.system().numeric_kind(ty)?, - _ => return None, - }; - let owner = self.resolver.declarations.table.symbol(field).parent; - let value = self.untyped_integer_constant(value, owner, depth)?; - integer_fits(kind, value).then_some(value) - } - - /// An integer constant expression no type was recorded for: literals, - /// parentheses, unary and binary operators, casts (their value; C# - /// rejects an out-of-range constant cast without `unchecked` anyway), - /// and names of `const` fields of `owner` or a type enclosing it. - fn untyped_integer_constant( - &self, - expression: &Expression<'ast, 'ast>, - owner: Option, - depth: usize, - ) -> Option { if depth == 0 { return None; } - match expression { - Expression::Primary(primary) if primary.chain.is_empty() => match &primary.left { - PrimaryLeft::Parenthesized { expression, .. } => { - self.untyped_integer_constant(expression, owner, depth - 1) - } - PrimaryLeft::Identifier { - name, - generics: None, - .. - } => { - let field = self.const_field_named(owner, name.value)?; - self.source_const_value(field, depth - 1) - } - PrimaryLeft::Literal(_) => { - super::exhaustive::integer_literal_value(expression).map(i128::from) - } - PrimaryLeft::Sizeof { - target_type: Ok(target_type), - .. - } => self.sizeof_constant(target_type).map(i128::from), - _ => None, - }, - Expression::Cast(cast) => { - self.untyped_integer_constant(cast.value.as_ref().ok()?, owner, depth - 1) - } - Expression::Unary(unary) => { - let value = - self.untyped_integer_constant(unary.operand.as_ref().ok()?, owner, depth - 1)?; - match unary.operator.value { - UnaryOperator::Plus => Some(value), - UnaryOperator::Minus => value.checked_neg(), - UnaryOperator::BitwiseNot => Some(!value), - _ => None, - } - } - Expression::Binary(binary) => { - let right_expression = binary.right.as_ref().ok()?; - let left = self.untyped_integer_constant(&binary.left, owner, depth - 1)?; - let right = self.untyped_integer_constant(right_expression, owner, depth - 1)?; - match binary.operator.value { - BinaryOperator::Add => left.checked_add(right), - BinaryOperator::Subtract => left.checked_sub(right), - BinaryOperator::Multiply => left.checked_mul(right), - BinaryOperator::Divide => left.checked_div(right), - BinaryOperator::Modulo => left.checked_rem(right), - BinaryOperator::BitwiseAnd => Some(left & right), - BinaryOperator::BitwiseOr => Some(left | right), - BinaryOperator::BitwiseXor => Some(left ^ right), - operator @ (BinaryOperator::LeftShift - | BinaryOperator::RightShift - | BinaryOperator::UnsignedRightShift) => { - // the left operand's own type sets the width, as in - // C#: `1 << 33` is an int shift by 1, `1L << 33` not - let wide = self.untyped_is_long(&binary.left, owner, depth - 1); - let bits: u32 = if wide { 64 } else { 32 }; - let count = (right as u32) & (bits - 1); - Some(match (operator, wide) { - (BinaryOperator::LeftShift, false) => { - i128::from((left as i32).wrapping_shl(count)) - } - (BinaryOperator::LeftShift, true) => { - i128::from((left as i64).wrapping_shl(count)) - } - (BinaryOperator::UnsignedRightShift, false) => { - i128::from((left as u32) >> count) - } - (BinaryOperator::UnsignedRightShift, true) => { - i128::from((left as u64) >> count) - } - (_, false) => i128::from((left as i32) >> count), - (_, true) => i128::from((left as i64) >> count), - }) - } - _ => None, - } - } - _ => None, - } - } - - /// Is an unchecked integer constant expression a `long`/`ulong` rather - /// than an `int`/`uint`? A suffixed or out-of-uint-range literal, or a - /// const field declared so; for an operator, either operand. - fn untyped_is_long( - &self, - expression: &Expression<'ast, 'ast>, - owner: Option, - depth: usize, - ) -> bool { - use crate::types::conversions::NumericKind::{Int64, UInt64}; - if depth == 0 { - return false; - } - match expression { - Expression::Primary(primary) if primary.chain.is_empty() => match &primary.left { - PrimaryLeft::Parenthesized { expression, .. } => { - self.untyped_is_long(expression, owner, depth - 1) - } - PrimaryLeft::Literal(LiteralExpression::Integer(text)) => { - let suffixed = text.value.to_ascii_lowercase().contains('l'); - suffixed - || super::exhaustive::integer_literal_value(expression) - .is_none_or(|value| u32::try_from(value).is_err()) - } - PrimaryLeft::Identifier { name, .. } => self - .const_field_named(owner, name.value) - .and_then(|field| match self.signatures.members.get(&field) { - Some(crate::types::MemberSignature::Field(ty)) => { - self.system().numeric_kind(ty) - } - _ => None, - }) - .is_some_and(|kind| matches!(kind, Int64 | UInt64)), - _ => false, - }, - Expression::Unary(unary) => unary - .operand - .as_ref() - .is_ok_and(|operand| self.untyped_is_long(operand, owner, depth - 1)), - Expression::Binary(binary) => { - let shift = matches!( - binary.operator.value, - BinaryOperator::LeftShift - | BinaryOperator::RightShift - | BinaryOperator::UnsignedRightShift - ); - self.untyped_is_long(&binary.left, owner, depth - 1) - || (!shift - && binary - .right - .as_ref() - .is_ok_and(|right| self.untyped_is_long(right, owner, depth - 1))) - } - _ => false, - } - } - - /// The value of `sizeof(T)`: from the type the checker resolved for it, - /// or — in an initializer not checked yet — from the keyword spelled. - fn sizeof_constant(&self, target: &men_sharp_parser::ast::TypeRef<'ast, 'ast>) -> Option { - use men_sharp_parser::ast::{PredefinedType as P, TypeRefBase}; - if let Some(ty) = self.resolver.out.type_of.get(&EntityID::from(target)) { - return self.system().sizeof_value(ty); + let value = self.const_initializer(field)?; + if self.expression_types.contains_key(&EntityID::from(value)) { + return self.constant_value_inner(value, depth); } - let TypeRefBase::Predefined(predefined) = &target.base else { - return None; - }; - if !target.suffixes.is_empty() { + let declarations = self.resolver.declarations; + let file = declarations.table.symbol(field).declarations.first()?.file; + let index = declarations + .files + .iter() + .position(|candidate| candidate.file == file)?; + let mut checker = + Self::for_file(declarations, self.signatures, self.resolver.external, index); + checker.constant_depth = depth - 1; + if !checker.bind_constant_nodes(&declarations.files[index].members, field) + || !checker.resolver.out.errors.is_empty() + { return None; } - Some(match predefined.value { - P::Bool | P::Byte | P::Sbyte => 1, - P::Char | P::Short | P::Ushort => 2, - P::Int | P::Uint | P::Float => 4, - P::Long | P::Ulong | P::Double => 8, - P::Decimal => 16, - _ => return None, - }) + checker.constant_value_inner(value, depth - 1) } - /// A `const` field called `name` on `owner` or a type enclosing it. - fn const_field_named(&self, owner: Option, name: &str) -> Option { - let table = &self.resolver.declarations.table; - let mut current = owner; - while let Some(ty) = current { - let symbol = table.symbol(ty); - if let Some(field) = symbol - .members_named(name) - .iter() - .copied() - .find(|&member| self.const_initializer(member).is_some()) - { - return Some(field); - } - current = symbol.parent; - } - None + fn sizeof_constant(&self, target: &men_sharp_parser::ast::TypeRef<'ast, 'ast>) -> Option { + let ty = self.resolver.out.type_of.get(&EntityID::from(target))?; + self.system().sizeof_value(ty) } /// The constant-expression allowance, without evaluating: an integer diff --git a/src/men-sharp-semantics/src/semantics/check/declarations.rs b/src/men-sharp-semantics/src/semantics/check/declarations.rs index b75122d..03a708f 100644 --- a/src/men-sharp-semantics/src/semantics/check/declarations.rs +++ b/src/men-sharp-semantics/src/semantics/check/declarations.rs @@ -27,7 +27,7 @@ impl<'a, 'ast> Checker<'a, 'ast> { } } - fn walk_namespace(&mut self, node: &NamespaceNode<'ast>) { + fn enter_namespace(&mut self, node: &NamespaceNode<'ast>) -> usize { let pushed = node.name.len(); for segment in node.name { @@ -65,12 +65,65 @@ impl<'a, 'ast> Checker<'a, 'ast> { scope.usings = usings; } + pushed + } + + fn walk_namespace(&mut self, node: &NamespaceNode<'ast>) { + let pushed = self.enter_namespace(node); self.walk_nodes(&node.members); let keep = self.scopes.len() - pushed; self.scopes.truncate(keep); } + pub(super) fn bind_constant_nodes( + &mut self, + nodes: &[DeclarationNode<'ast>], + field: SymbolId, + ) -> bool { + for node in nodes { + let found = match node { + DeclarationNode::Namespace(namespace) => { + let pushed = self.enter_namespace(namespace); + let found = self.bind_constant_nodes(&namespace.members, field); + self.scopes.truncate(self.scopes.len() - pushed); + found + } + DeclarationNode::Type(node) => self.bind_constant_type(node, field), + }; + if found { + return true; + } + } + false + } + + fn bind_constant_type(&mut self, node: &TypeNode<'ast>, field: SymbolId) -> bool { + let Some(symbol) = self + .resolver + .declarations + .symbol_of(node.syntax.entity_id()) + else { + return false; + }; + self.type_stack.push(symbol); + let found = if let Some(member) = node.members.iter().find(|member| { + self.resolver + .declarations + .symbol_of(member.syntax.entity_id()) + == Some(field) + }) { + self.check_member(member); + true + } else { + node.nested + .iter() + .any(|nested| self.bind_constant_type(nested, field)) + }; + self.type_stack.pop(); + found + } + fn check_type_declaration(&mut self, node: &TypeNode<'ast>) { let Some(symbol) = self .resolver diff --git a/src/men-sharp-semantics/src/semantics/check/exhaustive.rs b/src/men-sharp-semantics/src/semantics/check/exhaustive.rs index 0aa0014..f6d0035 100644 --- a/src/men-sharp-semantics/src/semantics/check/exhaustive.rs +++ b/src/men-sharp-semantics/src/semantics/check/exhaustive.rs @@ -90,8 +90,13 @@ fn bool_literal(expression: &Expression) -> Option { /// allowed to be. Anything else is `None`: the member is then a case of /// its own, which is right unless it aliases another. pub(super) fn integer_literal_value(expression: &Expression) -> Option { + i64::try_from(wide_integer_literal_value(expression)?).ok() +} + +/// Constant binding also needs unsigned literals above i64::MAX. +pub(super) fn wide_integer_literal_value(expression: &Expression) -> Option { if let Expression::Unary(unary) = expression { - let value = integer_literal_value(unary.operand.as_ref().ok()?)?; + let value = wide_integer_literal_value(unary.operand.as_ref().ok()?)?; return match unary.operator.value { men_sharp_parser::ast::UnaryOperator::Plus => Some(value), men_sharp_parser::ast::UnaryOperator::Minus => value.checked_neg(), @@ -105,7 +110,7 @@ pub(super) fn integer_literal_value(expression: &Expression) -> Option { return None; } if let PrimaryLeft::Parenthesized { expression, .. } = &primary.left { - return integer_literal_value(expression); + return wide_integer_literal_value(expression); } let PrimaryLeft::Literal(LiteralExpression::Integer(text)) = &primary.left else { return None; @@ -116,12 +121,12 @@ pub(super) fn integer_literal_value(expression: &Expression) -> Option { .strip_prefix("0x") .or_else(|| digits.strip_prefix("0X")) { - i64::from_str_radix(hex, 16).ok() + i128::from_str_radix(hex, 16).ok() } else if let Some(bits) = digits .strip_prefix("0b") .or_else(|| digits.strip_prefix("0B")) { - i64::from_str_radix(bits, 2).ok() + i128::from_str_radix(bits, 2).ok() } else { digits.parse().ok() } diff --git a/src/men-sharp-semantics/src/semantics/check/statements.rs b/src/men-sharp-semantics/src/semantics/check/statements.rs index 83ac3f0..7483033 100644 --- a/src/men-sharp-semantics/src/semantics/check/statements.rs +++ b/src/men-sharp-semantics/src/semantics/check/statements.rs @@ -365,7 +365,9 @@ impl<'a, 'ast> Checker<'a, 'ast> { let constant = if declaration.const_keyword.is_some() { match &declarator.initializer { - Some(InitializerValue::Expression(value)) => self.integer_constant(value), + Some(InitializerValue::Expression(value)) => { + self.constant_value_inner(value, self.constant_depth) + } _ => None, } } else {