refactor: restructure CLI into library crate and add semantic analysis phase
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
This commit is contained in:
@@ -5,14 +5,16 @@ edition.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "nxc"
|
name = "nxc"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "nexacore"
|
name = "nexacore"
|
||||||
path = "src/main.rs"
|
path = "src/bin/nexacore.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nxc-driver = { path = "../nxc-driver" }
|
nxc-driver = { path = "../nxc-driver" }
|
||||||
|
|
||||||
|
|||||||
3
crates/nxc-cli/src/bin/nexacore.rs
Normal file
3
crates/nxc-cli/src/bin/nexacore.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() -> std::process::ExitCode {
|
||||||
|
nxc_cli::main_entry()
|
||||||
|
}
|
||||||
86
crates/nxc-cli/src/lib.rs
Normal file
86
crates/nxc-cli/src/lib.rs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::ExitCode;
|
||||||
|
|
||||||
|
pub fn main_entry() -> ExitCode {
|
||||||
|
match run() {
|
||||||
|
Ok(()) => ExitCode::SUCCESS,
|
||||||
|
Err(message) => {
|
||||||
|
eprintln!("{message}");
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run() -> Result<(), String> {
|
||||||
|
let mut args = env::args().skip(1);
|
||||||
|
let Some(command) = args.next() else {
|
||||||
|
print_help();
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
match command.as_str() {
|
||||||
|
"check" | "build" => {
|
||||||
|
let Some(path) = args.next() else {
|
||||||
|
return Err(format!("usage: {} {command} <file.nx>", executable_name()));
|
||||||
|
};
|
||||||
|
|
||||||
|
let output =
|
||||||
|
nxc_driver::check_file(Path::new(&path)).map_err(format_driver_error)?;
|
||||||
|
|
||||||
|
if output.has_errors() {
|
||||||
|
eprintln!("{}", output.render_diagnostics());
|
||||||
|
return Err(format!(
|
||||||
|
"check failed with {} diagnostic(s)",
|
||||||
|
output.diagnostics.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let summary = output.summary();
|
||||||
|
println!("checked {}", output.path.display());
|
||||||
|
println!("tokens: {}", output.tokens.len());
|
||||||
|
println!("items: {}", summary.items);
|
||||||
|
println!("functions: {}", summary.functions);
|
||||||
|
println!("structs: {}", summary.structs);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
"run" => Err("runtime execution is not implemented yet".to_string()),
|
||||||
|
"new" => Err("project scaffolding is not implemented yet".to_string()),
|
||||||
|
"test" => Err("test runner is not implemented yet".to_string()),
|
||||||
|
"fmt" => Err("formatter is not implemented yet".to_string()),
|
||||||
|
"add" => Err("package manager is not implemented yet".to_string()),
|
||||||
|
"doc" => Err("docs generator is not implemented yet".to_string()),
|
||||||
|
_ => Err(format!("unknown command: {command}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn executable_name() -> String {
|
||||||
|
env::args()
|
||||||
|
.next()
|
||||||
|
.and_then(|path| {
|
||||||
|
Path::new(&path)
|
||||||
|
.file_name()
|
||||||
|
.map(|name| name.to_string_lossy().to_string())
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "nxc".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_driver_error(error: nxc_driver::DriverError) -> String {
|
||||||
|
match error {
|
||||||
|
nxc_driver::DriverError::Io(io) => format!("io error: {io}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_help() {
|
||||||
|
let name = executable_name();
|
||||||
|
println!("NexaCore CLI");
|
||||||
|
println!("usage:");
|
||||||
|
println!(" {name} check <file.nx>");
|
||||||
|
println!(" {name} build <file.nx>");
|
||||||
|
println!(" {name} run <file.nx>");
|
||||||
|
println!(" {name} new <name>");
|
||||||
|
println!(" {name} test");
|
||||||
|
println!(" {name} fmt");
|
||||||
|
println!(" {name} add <package>");
|
||||||
|
println!(" {name} doc");
|
||||||
|
}
|
||||||
@@ -1,87 +1,4 @@
|
|||||||
use std::env;
|
fn main() -> std::process::ExitCode {
|
||||||
use std::path::Path;
|
nxc_cli::main_entry()
|
||||||
use std::process::ExitCode;
|
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
|
||||||
match run() {
|
|
||||||
Ok(()) => ExitCode::SUCCESS,
|
|
||||||
Err(message) => {
|
|
||||||
eprintln!("{message}");
|
|
||||||
ExitCode::FAILURE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run() -> Result<(), String> {
|
|
||||||
let mut args = env::args().skip(1);
|
|
||||||
let Some(command) = args.next() else {
|
|
||||||
print_help();
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
match command.as_str() {
|
|
||||||
"check" | "build" => {
|
|
||||||
let Some(path) = args.next() else {
|
|
||||||
return Err(format!("usage: {} {command} <file.nx>", executable_name()));
|
|
||||||
};
|
|
||||||
|
|
||||||
let output =
|
|
||||||
nxc_driver::check_file(Path::new(&path)).map_err(format_driver_error)?;
|
|
||||||
|
|
||||||
if output.has_errors() {
|
|
||||||
eprintln!("{}", output.render_diagnostics());
|
|
||||||
return Err(format!(
|
|
||||||
"check failed with {} diagnostic(s)",
|
|
||||||
output.diagnostics.len()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let summary = output.summary();
|
|
||||||
println!("checked {}", output.path.display());
|
|
||||||
println!("tokens: {}", output.tokens.len());
|
|
||||||
println!("items: {}", summary.items);
|
|
||||||
println!("functions: {}", summary.functions);
|
|
||||||
println!("structs: {}", summary.structs);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
"run" => Err("runtime execution is not implemented yet".to_string()),
|
|
||||||
"new" => Err("project scaffolding is not implemented yet".to_string()),
|
|
||||||
"test" => Err("test runner is not implemented yet".to_string()),
|
|
||||||
"fmt" => Err("formatter is not implemented yet".to_string()),
|
|
||||||
"add" => Err("package manager is not implemented yet".to_string()),
|
|
||||||
"doc" => Err("docs generator is not implemented yet".to_string()),
|
|
||||||
_ => Err(format!("unknown command: {command}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn executable_name() -> String {
|
|
||||||
env::args()
|
|
||||||
.next()
|
|
||||||
.and_then(|path| {
|
|
||||||
Path::new(&path)
|
|
||||||
.file_name()
|
|
||||||
.map(|name| name.to_string_lossy().to_string())
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| "nxc".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_driver_error(error: nxc_driver::DriverError) -> String {
|
|
||||||
match error {
|
|
||||||
nxc_driver::DriverError::Io(io) => format!("io error: {io}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_help() {
|
|
||||||
let name = executable_name();
|
|
||||||
println!("NexaCore CLI");
|
|
||||||
println!("usage:");
|
|
||||||
println!(" {name} check <file.nx>");
|
|
||||||
println!(" {name} build <file.nx>");
|
|
||||||
println!(" {name} run <file.nx>");
|
|
||||||
println!(" {name} new <name>");
|
|
||||||
println!(" {name} test");
|
|
||||||
println!(" {name} fmt");
|
|
||||||
println!(" {name} add <package>");
|
|
||||||
println!(" {name} doc");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ use std::fs;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use nxc_frontend::{
|
use nxc_frontend::{
|
||||||
has_errors, Diagnostic, Item, LexResult, Lexer, Module, ParseResult, Parser, Token,
|
analyze, has_errors, Diagnostic, Item, LexResult, Lexer, Module, ParseResult, Parser, Token,
|
||||||
|
TypedModule,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -12,6 +13,7 @@ pub struct FrontendOutput {
|
|||||||
pub source: String,
|
pub source: String,
|
||||||
pub tokens: Vec<Token>,
|
pub tokens: Vec<Token>,
|
||||||
pub module: Module,
|
pub module: Module,
|
||||||
|
pub typed_module: Option<TypedModule>,
|
||||||
pub diagnostics: Vec<Diagnostic>,
|
pub diagnostics: Vec<Diagnostic>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,13 +88,20 @@ pub fn check_source(path: PathBuf, source: String) -> FrontendOutput {
|
|||||||
} = Parser::new(tokens.clone()).parse_module();
|
} = Parser::new(tokens.clone()).parse_module();
|
||||||
|
|
||||||
lexer_diagnostics.extend(parser_diagnostics);
|
lexer_diagnostics.extend(parser_diagnostics);
|
||||||
|
let mut typed_module = None;
|
||||||
|
|
||||||
|
if !has_errors(&lexer_diagnostics) {
|
||||||
|
let semantic = analyze(&module);
|
||||||
|
lexer_diagnostics.extend(semantic.diagnostics);
|
||||||
|
typed_module = Some(semantic.module);
|
||||||
|
}
|
||||||
|
|
||||||
FrontendOutput {
|
FrontendOutput {
|
||||||
path,
|
path,
|
||||||
source,
|
source,
|
||||||
tokens,
|
tokens,
|
||||||
module,
|
module,
|
||||||
|
typed_module,
|
||||||
diagnostics: lexer_diagnostics,
|
diagnostics: lexer_diagnostics,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ pub mod ast;
|
|||||||
pub mod diagnostics;
|
pub mod diagnostics;
|
||||||
pub mod lexer;
|
pub mod lexer;
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
|
pub mod semantic;
|
||||||
pub mod token;
|
pub mod token;
|
||||||
|
|
||||||
pub use ast::{
|
pub use ast::{
|
||||||
@@ -11,5 +12,8 @@ pub use ast::{
|
|||||||
pub use diagnostics::{has_errors, Diagnostic, Severity};
|
pub use diagnostics::{has_errors, Diagnostic, Severity};
|
||||||
pub use lexer::{LexResult, Lexer};
|
pub use lexer::{LexResult, Lexer};
|
||||||
pub use parser::{ParseResult, Parser};
|
pub use parser::{ParseResult, Parser};
|
||||||
|
pub use semantic::{
|
||||||
|
analyze, FunctionId, LocalId, SemanticResult, StructId, Type, TypedBlock, TypedExpr,
|
||||||
|
TypedExprKind, TypedFunction, TypedIfStmt, TypedModule, TypedStmt, TypedStmtKind, TypedStruct,
|
||||||
|
};
|
||||||
pub use token::{Keyword, Span, Token, TokenKind};
|
pub use token::{Keyword, Span, Token, TokenKind};
|
||||||
|
|
||||||
|
|||||||
1009
crates/nxc-frontend/src/semantic.rs
Normal file
1009
crates/nxc-frontend/src/semantic.rs
Normal file
File diff suppressed because it is too large
Load Diff
223
crates/nxc-frontend/tests/semantic_tests.rs
Normal file
223
crates/nxc-frontend/tests/semantic_tests.rs
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user