This commit is contained in:
Nilton Constantino 2026-01-27 10:55:10 +00:00
parent d62503eb5a
commit b5cafe1a4a
No known key found for this signature in database
7 changed files with 181 additions and 34 deletions

14
Cargo.lock generated
View File

@ -1923,6 +1923,7 @@ dependencies = [
"prometeu-core",
"serde",
"serde_json",
"tempfile",
]
[[package]]
@ -2320,6 +2321,19 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "tempfile"
version = "3.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
dependencies = [
"fastrand",
"getrandom",
"once_cell",
"rustix 1.1.3",
"windows-sys 0.61.2",
]
[[package]]
name = "termcolor"
version = "1.4.1"

View File

@ -26,3 +26,6 @@ clap = { version = "4.5.54", features = ["derive"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
anyhow = "1.0.100"
[dev-dependencies]
tempfile = "3.10.1"

View File

@ -0,0 +1,43 @@
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use anyhow::Result;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ProjectConfig {
pub script_fe: String,
pub entry: PathBuf,
}
impl ProjectConfig {
pub fn load(project_dir: &Path) -> Result<Self> {
let config_path = project_dir.join("prometeu.json");
let content = std::fs::read_to_string(config_path)?;
let config: ProjectConfig = serde_json::from_str(&content)?;
Ok(config)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_load_valid_config() {
let dir = tempdir().unwrap();
let config_path = dir.path().join("prometeu.json");
fs::write(
config_path,
r#"{
"script_fe": "pbs",
"entry": "main.pbs"
}"#,
)
.unwrap();
let config = ProjectConfig::load(dir.path()).unwrap();
assert_eq!(config.script_fe, "pbs");
assert_eq!(config.entry, PathBuf::from("main.pbs"));
}
}

View File

@ -2,3 +2,4 @@ pub mod diagnostics;
pub mod spans;
pub mod files;
pub mod symbols;
pub mod config;

View File

@ -4,6 +4,7 @@
//! It handles the transition between different compiler phases: Frontend -> IR -> Backend.
use crate::backend;
use crate::common::config::ProjectConfig;
use crate::common::files::FileManager;
use crate::common::symbols::Symbol;
use crate::frontends::Frontend;
@ -13,6 +14,7 @@ use std::path::Path;
/// The result of a successful compilation process.
/// It contains the final binary and the metadata needed for debugging.
#[derive(Debug)]
pub struct CompilationUnit {
/// The raw binary data formatted as Prometeu ByteCode (PBC).
/// This is what gets written to a `.pbc` file.
@ -51,35 +53,87 @@ impl CompilationUnit {
/// # Example
/// ```no_run
/// use std::path::Path;
/// let entry = Path::new("src/main.ts");
/// let unit = prometeu_compiler::compiler::compile(entry).expect("Failed to compile");
/// let project_dir = Path::new(".");
/// let unit = prometeu_compiler::compiler::compile(project_dir).expect("Failed to compile");
/// unit.export(Path::new("build/program.pbc"), true, true).unwrap();
/// ```
pub fn compile(entry: &Path) -> Result<CompilationUnit> {
let mut file_manager = FileManager::new();
pub fn compile(project_dir: &Path) -> Result<CompilationUnit> {
let config = ProjectConfig::load(project_dir)?;
// 1. Select Frontend
// The _frontend is responsible for parsing source code and producing the IR.
let _frontend: Box<dyn Frontend> = match config.script_fe.as_str() {
"pbs" => anyhow::bail!("Frontend 'pbs' not yet implemented"),
_ => anyhow::bail!("Invalid frontend: {}", config.script_fe),
};
// 1. Select Frontend (Currently only TS is supported)
// The frontend is responsible for parsing source code and producing the IR.
let frontend = /** ??? **/;
// 2. Compile to IR (Intermediate Representation)
// This step abstracts away source-specific syntax (like TypeScript) into a
// generic set of instructions that the backend can understand.
let ir_module = frontend.compile_to_ir(entry, &mut file_manager)
.map_err(|bundle| anyhow::anyhow!("Compilation failed with {} errors", bundle.diagnostics.len()))?;
#[allow(unreachable_code, unused_variables, unused_mut)]
{
let entry = project_dir.join(&config.entry);
let mut file_manager = FileManager::new();
// 2. Compile to IR (Intermediate Representation)
// This step abstracts away source-specific syntax (like TypeScript) into a
// generic set of instructions that the backend can understand.
let ir_module = _frontend.compile_to_ir(&entry, &mut file_manager)
.map_err(|bundle| anyhow::anyhow!("Compilation failed with {} errors", bundle.diagnostics.len()))?;
// 3. IR Validation
// Ensures the generated IR is sound and doesn't violate any VM constraints
// before we spend time generating bytecode.
ir::validate::validate_module(&ir_module)
.map_err(|bundle| anyhow::anyhow!("IR Validation failed: {:?}", bundle))?;
// 4. Emit Bytecode
// The backend takes the validated IR and produces the final binary executable.
let result = backend::emit_module(&ir_module, &file_manager)?;
// 3. IR Validation
// Ensures the generated IR is sound and doesn't violate any VM constraints
// before we spend time generating bytecode.
ir::validate::validate_module(&ir_module)
.map_err(|bundle| anyhow::anyhow!("IR Validation failed: {:?}", bundle))?;
// 4. Emit Bytecode
// The backend takes the validated IR and produces the final binary executable.
let result = backend::emit_module(&ir_module, &file_manager)?;
Ok(CompilationUnit {
rom: result.rom,
symbols: result.symbols,
})
Ok(CompilationUnit {
rom: result.rom,
symbols: result.symbols,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_invalid_frontend() {
let dir = tempdir().unwrap();
let config_path = dir.path().join("prometeu.json");
fs::write(
config_path,
r#"{
"script_fe": "invalid",
"entry": "main.pbs"
}"#,
)
.unwrap();
let result = compile(dir.path());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Invalid frontend: invalid"));
}
#[test]
fn test_frontend_pbs_not_implemented() {
let dir = tempdir().unwrap();
let config_path = dir.path().join("prometeu.json");
fs::write(
config_path,
r#"{
"script_fe": "pbs",
"entry": "main.pbs"
}"#,
)
.unwrap();
let result = compile(dir.path());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Frontend 'pbs' not yet implemented"));
}
}

View File

@ -96,12 +96,11 @@ pub fn run() -> Result<()> {
match cli.command {
Commands::Build {
project_dir,
entry,
out,
emit_disasm,
emit_symbols,
..
} => {
let entry = entry.unwrap_or_else(|| project_dir.join("src/main.ts"));
let build_dir = project_dir.join("build");
let out = out.unwrap_or_else(|| build_dir.join("program.pbc"));
@ -110,18 +109,15 @@ pub fn run() -> Result<()> {
}
println!("Building project at {:?}", project_dir);
println!("Entry: {:?}", entry);
println!("Output: {:?}", out);
let compilation_unit = compiler::compile(&entry)?;
let compilation_unit = compiler::compile(&project_dir)?;
compilation_unit.export(&out, emit_disasm, emit_symbols)?;
}
Commands::Verify { project_dir } => {
let entry = project_dir.join("src/main.ts");
println!("Verifying project at {:?}", project_dir);
println!("Entry: {:?}", entry);
compiler::compile(&entry)?;
compiler::compile(&project_dir)?;
println!("Project is valid!");
}
}

View File

@ -0,0 +1,36 @@
use std::fs;
use tempfile::tempdir;
use prometeu_compiler::compiler;
#[test]
fn test_project_root_and_entry_resolution() {
let dir = tempdir().unwrap();
let project_dir = dir.path();
// Create prometeu.json
fs::write(
project_dir.join("prometeu.json"),
r#"{
"script_fe": "pbs",
"entry": "src/main.pbs"
}"#,
).unwrap();
// Create src directory and main.pbs
fs::create_dir(project_dir.join("src")).unwrap();
fs::write(project_dir.join("src/main.pbs"), "").unwrap();
// Call compile
let result = compiler::compile(project_dir);
// It should fail with "Frontend 'pbs' not yet implemented"
// but ONLY after successfully loading the config and resolving the entry.
match result {
Err(e) => {
let msg = e.to_string();
assert!(msg.contains("Frontend 'pbs' not yet implemented"), "Unexpected error: {}", msg);
}
Ok(_) => panic!("Should have failed as pbs is not implemented yet"),
}
}