Restructure nxc-cli crate: - Extract main logic into lib.rs with main_entry() function - Move nexacore binary to separate bin/nexacore.rs file - Keep nxc binary in main.rs as thin wrapper Add semantic analysis to compilation pipeline: - Implement semantic analyzer with type checking and name resolution - Add Type enum with Int, Float, Bool, String, Void, Struct, Function, Error variants - Add typed AST nodes (TypedModule, TypedFunction
224 lines
4.2 KiB
Rust
224 lines
4.2 KiB
Rust
use nxc_frontend::{analyze, Lexer, Parser};
|
|
|
|
fn analyze_source(source: &str) -> nxc_frontend::SemanticResult {
|
|
let lexed = Lexer::new(source).lex();
|
|
assert!(
|
|
lexed.diagnostics.is_empty(),
|
|
"unexpected lexer diagnostics: {:?}",
|
|
lexed.diagnostics
|
|
);
|
|
|
|
let parsed = Parser::new(lexed.tokens).parse_module();
|
|
assert!(
|
|
parsed.diagnostics.is_empty(),
|
|
"unexpected parser diagnostics: {:?}",
|
|
parsed.diagnostics
|
|
);
|
|
|
|
analyze(&parsed.module)
|
|
}
|
|
|
|
fn messages(result: &nxc_frontend::SemanticResult) -> Vec<String> {
|
|
result
|
|
.diagnostics
|
|
.iter()
|
|
.map(|diagnostic| diagnostic.message.clone())
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_duplicate_function_names() {
|
|
let result = analyze_source(
|
|
"\
|
|
fn main() -> Int:
|
|
return 0
|
|
|
|
fn main() -> Int:
|
|
return 1
|
|
",
|
|
);
|
|
|
|
assert!(messages(&result).iter().any(|msg| msg.contains("duplicate function `main`")));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_duplicate_parameter_names() {
|
|
let result = analyze_source(
|
|
"\
|
|
fn main(value: Int, value: Int) -> Int:
|
|
return value
|
|
",
|
|
);
|
|
|
|
assert!(messages(&result)
|
|
.iter()
|
|
.any(|msg| msg.contains("duplicate parameter `value`")));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_duplicate_local_bindings_in_same_scope() {
|
|
let result = analyze_source(
|
|
"\
|
|
fn main() -> Int:
|
|
let value = 1
|
|
let value = 2
|
|
return value
|
|
",
|
|
);
|
|
|
|
assert!(messages(&result)
|
|
.iter()
|
|
.any(|msg| msg.contains("duplicate binding `value`")));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_unknown_identifiers() {
|
|
let result = analyze_source(
|
|
"\
|
|
fn main() -> Int:
|
|
return missing
|
|
",
|
|
);
|
|
|
|
assert!(messages(&result)
|
|
.iter()
|
|
.any(|msg| msg.contains("unknown identifier `missing`")));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_unknown_function_calls() {
|
|
let result = analyze_source(
|
|
"\
|
|
fn main() -> Int:
|
|
return missing(1)
|
|
",
|
|
);
|
|
|
|
assert!(messages(&result)
|
|
.iter()
|
|
.any(|msg| msg.contains("unknown function `missing`")));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_invalid_unary_operator_types() {
|
|
let result = analyze_source(
|
|
"\
|
|
fn main() -> Int:
|
|
return -true
|
|
",
|
|
);
|
|
|
|
assert!(messages(&result)
|
|
.iter()
|
|
.any(|msg| msg.contains("unary `-` expects `Int` or `Float`")));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_invalid_binary_operator_types() {
|
|
let result = analyze_source(
|
|
"\
|
|
fn main() -> Int:
|
|
return true + 1
|
|
",
|
|
);
|
|
|
|
assert!(messages(&result)
|
|
.iter()
|
|
.any(|msg| msg.contains("operator `+` expects numeric operands")));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_invalid_if_condition_types() {
|
|
let result = analyze_source(
|
|
"\
|
|
fn main() -> Int:
|
|
if 1:
|
|
return 0
|
|
else:
|
|
return 1
|
|
",
|
|
);
|
|
|
|
assert!(messages(&result)
|
|
.iter()
|
|
.any(|msg| msg.contains("if condition must be `Bool`")));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_wrong_call_arity() {
|
|
let result = analyze_source(
|
|
"\
|
|
fn add(a: Int, b: Int) -> Int:
|
|
return a + b
|
|
|
|
fn main() -> Int:
|
|
return add(1)
|
|
",
|
|
);
|
|
|
|
assert!(messages(&result)
|
|
.iter()
|
|
.any(|msg| msg.contains("expects 2 argument(s), found 1")));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_wrong_call_argument_types() {
|
|
let result = analyze_source(
|
|
"\
|
|
fn negate(value: Int) -> Int:
|
|
return -value
|
|
|
|
fn main() -> Int:
|
|
return negate(true)
|
|
",
|
|
);
|
|
|
|
assert!(messages(&result)
|
|
.iter()
|
|
.any(|msg| msg.contains("parameter `value` expects `Int`, found `Bool`")));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_return_type_mismatch() {
|
|
let result = analyze_source(
|
|
"\
|
|
fn main() -> Int:
|
|
return true
|
|
",
|
|
);
|
|
|
|
assert!(messages(&result)
|
|
.iter()
|
|
.any(|msg| msg.contains("return type mismatch: expected `Int`, found `Bool`")));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_missing_return_in_non_void_function() {
|
|
let result = analyze_source(
|
|
"\
|
|
fn main() -> Int:
|
|
let value = 1
|
|
",
|
|
);
|
|
|
|
assert!(messages(&result)
|
|
.iter()
|
|
.any(|msg| msg.contains("may exit without returning `Int`")));
|
|
}
|
|
|
|
#[test]
|
|
fn allows_shadowing_in_nested_scopes() {
|
|
let result = analyze_source(
|
|
"\
|
|
fn main(value: Int) -> Int:
|
|
if true:
|
|
let value = 2
|
|
return value
|
|
else:
|
|
return value
|
|
",
|
|
);
|
|
|
|
assert!(result.diagnostics.is_empty(), "{:?}", result.diagnostics);
|
|
}
|