Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions src/men-sharp-compiler/tests/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:?}");
}
155 changes: 86 additions & 69 deletions src/men-sharp-semantics/src/semantics/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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<NamespaceScope<'ast>>,
type_stack: Vec<SymbolId>,
Expand Down Expand Up @@ -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
}
}
Loading
Loading