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:
2026-04-06 17:13:34 +02:00
parent dfd2f10234
commit 9304b6bcaa
8 changed files with 1343 additions and 90 deletions

View File

@@ -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" }

View File

@@ -0,0 +1,3 @@
fn main() -> std::process::ExitCode {
nxc_cli::main_entry()
}

86
crates/nxc-cli/src/lib.rs Normal file
View 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");
}

View File

@@ -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");
} }

View File

@@ -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,
} }
} }

View File

@@ -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};

File diff suppressed because it is too large Load Diff

View 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);
}