diff --git a/crates/prometeu-compiler/src/backend/artifacts.rs b/crates/prometeu-compiler/src/backend/artifacts.rs new file mode 100644 index 00000000..2e096a59 --- /dev/null +++ b/crates/prometeu-compiler/src/backend/artifacts.rs @@ -0,0 +1,63 @@ +use crate::common::symbols::Symbol; +use anyhow::{anyhow, Context, Result}; +use prometeu_bytecode::disasm::disasm; +use std::fs; +use std::path::Path; + +pub struct Artifacts { + pub rom: Vec, + pub symbols: Vec, +} + +impl Artifacts { + pub fn new(rom: Vec, symbols: Vec) -> Self { + Self { rom, symbols } + } + + pub fn export(&self, out: &Path, emit_disasm: bool, emit_symbols: bool) -> Result<()> { + // 1. Save the main binary + fs::write(out, &self.rom).with_context(|| format!("Failed to write PBC to {:?}", out))?; + + // 2. Export symbols for the HostDebugger + if emit_symbols { + let symbols_path = out.with_file_name("symbols.json"); + let symbols_json = serde_json::to_string_pretty(&self.symbols)?; + fs::write(&symbols_path, symbols_json)?; + } + + // 3. Export human-readable disassembly for developer inspection + if emit_disasm { + let disasm_path = out.with_extension("disasm.txt"); + + // Extract the actual bytecode (stripping the PBC header if present) + let rom_to_disasm = if let Ok(pbc) = prometeu_bytecode::pbc::parse_pbc(&self.rom) { + pbc.rom + } else { + self.rom.clone() + }; + + let instructions = disasm(&rom_to_disasm).map_err(|e| anyhow!("Disassembly failed: {}", e))?; + + let mut disasm_text = String::new(); + for instr in instructions { + // Find a matching symbol to show which source line generated this instruction + let symbol = self.symbols.iter().find(|s| s.pc == instr.pc); + let comment = if let Some(s) = symbol { + format!(" ; {}:{}", s.file, s.line) + } else { + "".to_string() + }; + + let operands_str = instr.operands.iter() + .map(|o| format!("{:?}", o)) + .collect::>() + .join(" "); + + disasm_text.push_str(&format!("{:08X} {:?} {}{}\n", instr.pc, instr.opcode, operands_str, comment)); + } + fs::write(disasm_path, disasm_text)?; + } + + Ok(()) + } +} diff --git a/crates/prometeu-compiler/src/backend/disasm.rs b/crates/prometeu-compiler/src/backend/disasm.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/prometeu-compiler/src/backend/emit_bytecode.rs b/crates/prometeu-compiler/src/backend/emit_bytecode.rs new file mode 100644 index 00000000..6becf040 --- /dev/null +++ b/crates/prometeu-compiler/src/backend/emit_bytecode.rs @@ -0,0 +1,181 @@ +use crate::backend::syscall_registry; +use crate::common::files::FileManager; +use crate::common::symbols::Symbol; +use crate::ir; +use crate::ir::instr::InstrKind; +use anyhow::{anyhow, Result}; +use prometeu_bytecode::asm::{assemble, update_pc_by_operand, Asm, Operand}; +use prometeu_bytecode::opcode::OpCode; +use prometeu_bytecode::pbc::{write_pbc, ConstantPoolEntry, PbcFile}; + +pub struct EmitResult { + pub rom: Vec, + pub symbols: Vec, +} + +pub fn emit_module(module: &ir::Module, file_manager: &FileManager) -> Result { + let mut emitter = BytecodeEmitter::new(file_manager); + emitter.emit(module) +} + +struct BytecodeEmitter<'a> { + constant_pool: Vec, + file_manager: &'a FileManager, +} + +impl<'a> BytecodeEmitter<'a> { + fn new(file_manager: &'a FileManager) -> Self { + Self { + constant_pool: vec![ConstantPoolEntry::Null], + file_manager, + } + } + + fn add_constant(&mut self, entry: ConstantPoolEntry) -> u32 { + if let Some(pos) = self.constant_pool.iter().position(|e| e == &entry) { + pos as u32 + } else { + let id = self.constant_pool.len() as u32; + self.constant_pool.push(entry); + id + } + } + + fn emit(&mut self, module: &ir::Module) -> Result { + let mut asm_instrs = Vec::new(); + let mut ir_instr_map = Vec::new(); // Maps Asm index to IR instruction (for symbols) + + for function in &module.functions { + asm_instrs.push(Asm::Label(function.name.clone())); + ir_instr_map.push(None); + + for instr in &function.body { + let start_idx = asm_instrs.len(); + match &instr.kind { + InstrKind::Nop => asm_instrs.push(Asm::Op(OpCode::Nop, vec![])), + InstrKind::Halt => asm_instrs.push(Asm::Op(OpCode::Halt, vec![])), + InstrKind::PushInt(v) => { + asm_instrs.push(Asm::Op(OpCode::PushI64, vec![Operand::I64(*v)])); + } + InstrKind::PushFloat(v) => { + asm_instrs.push(Asm::Op(OpCode::PushF64, vec![Operand::F64(*v)])); + } + InstrKind::PushBool(v) => { + asm_instrs.push(Asm::Op(OpCode::PushBool, vec![Operand::Bool(*v)])); + } + InstrKind::PushString(s) => { + let id = self.add_constant(ConstantPoolEntry::String(s.clone())); + asm_instrs.push(Asm::Op(OpCode::PushConst, vec![Operand::U32(id)])); + } + InstrKind::PushNull => { + asm_instrs.push(Asm::Op(OpCode::PushConst, vec![Operand::U32(0)])); + } + InstrKind::Pop => asm_instrs.push(Asm::Op(OpCode::Pop, vec![])), + InstrKind::Dup => asm_instrs.push(Asm::Op(OpCode::Dup, vec![])), + InstrKind::Swap => asm_instrs.push(Asm::Op(OpCode::Swap, vec![])), + InstrKind::Add => asm_instrs.push(Asm::Op(OpCode::Add, vec![])), + InstrKind::Sub => asm_instrs.push(Asm::Op(OpCode::Sub, vec![])), + InstrKind::Mul => asm_instrs.push(Asm::Op(OpCode::Mul, vec![])), + InstrKind::Div => asm_instrs.push(Asm::Op(OpCode::Div, vec![])), + InstrKind::Neg => asm_instrs.push(Asm::Op(OpCode::Neg, vec![])), + InstrKind::Eq => asm_instrs.push(Asm::Op(OpCode::Eq, vec![])), + InstrKind::Neq => asm_instrs.push(Asm::Op(OpCode::Neq, vec![])), + InstrKind::Lt => asm_instrs.push(Asm::Op(OpCode::Lt, vec![])), + InstrKind::Gt => asm_instrs.push(Asm::Op(OpCode::Gt, vec![])), + InstrKind::Lte => asm_instrs.push(Asm::Op(OpCode::Lte, vec![])), + InstrKind::Gte => asm_instrs.push(Asm::Op(OpCode::Gte, vec![])), + InstrKind::And => asm_instrs.push(Asm::Op(OpCode::And, vec![])), + InstrKind::Or => asm_instrs.push(Asm::Op(OpCode::Or, vec![])), + InstrKind::Not => asm_instrs.push(Asm::Op(OpCode::Not, vec![])), + InstrKind::BitAnd => asm_instrs.push(Asm::Op(OpCode::BitAnd, vec![])), + InstrKind::BitOr => asm_instrs.push(Asm::Op(OpCode::BitOr, vec![])), + InstrKind::BitXor => asm_instrs.push(Asm::Op(OpCode::BitXor, vec![])), + InstrKind::Shl => asm_instrs.push(Asm::Op(OpCode::Shl, vec![])), + InstrKind::Shr => asm_instrs.push(Asm::Op(OpCode::Shr, vec![])), + InstrKind::GetLocal(slot) => { + asm_instrs.push(Asm::Op(OpCode::GetLocal, vec![Operand::U32(*slot)])); + } + InstrKind::SetLocal(slot) => { + asm_instrs.push(Asm::Op(OpCode::SetLocal, vec![Operand::U32(*slot)])); + } + InstrKind::GetGlobal(slot) => { + asm_instrs.push(Asm::Op(OpCode::GetGlobal, vec![Operand::U32(*slot)])); + } + InstrKind::SetGlobal(slot) => { + asm_instrs.push(Asm::Op(OpCode::SetGlobal, vec![Operand::U32(*slot)])); + } + InstrKind::Jmp(label) => { + asm_instrs.push(Asm::Op(OpCode::Jmp, vec![Operand::Label(label.0.clone())])); + } + InstrKind::JmpIfFalse(label) => { + asm_instrs.push(Asm::Op(OpCode::JmpIfFalse, vec![Operand::Label(label.0.clone())])); + } + InstrKind::Label(label) => { + asm_instrs.push(Asm::Label(label.0.clone())); + } + InstrKind::Call { name, arg_count } => { + asm_instrs.push(Asm::Op(OpCode::Call, vec![Operand::Label(name.clone()), Operand::U32(*arg_count)])); + } + InstrKind::Ret => asm_instrs.push(Asm::Op(OpCode::Ret, vec![])), + InstrKind::Syscall(name) => { + if let Some(id) = syscall_registry::map_syscall(name) { + asm_instrs.push(Asm::Op(OpCode::Syscall, vec![Operand::U32(id)])); + } else { + return Err(anyhow!("Unknown syscall: {}", name)); + } + } + InstrKind::FrameSync => asm_instrs.push(Asm::Op(OpCode::FrameSync, vec![])), + InstrKind::PushScope => asm_instrs.push(Asm::Op(OpCode::PushScope, vec![])), + InstrKind::PopScope => asm_instrs.push(Asm::Op(OpCode::PopScope, vec![])), + } + + let end_idx = asm_instrs.len(); + for _ in start_idx..end_idx { + ir_instr_map.push(Some(instr)); + } + } + } + + let bytecode = assemble(&asm_instrs).map_err(|e| anyhow!(e))?; + + // Resolve symbols + let mut symbols = Vec::new(); + let mut current_pc = 0u32; + for (i, asm) in asm_instrs.iter().enumerate() { + if let Some(ir_instr) = ir_instr_map[i] { + if let Some(span) = ir_instr.span { + let (line, col) = self.file_manager.lookup_pos(span.file_id, span.start); + let file_path = self.file_manager.get_path(span.file_id) + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_default(); + + symbols.push(Symbol { + pc: current_pc, + file: file_path, + line, + col, + }); + } + } + + match asm { + Asm::Label(_) => {} + Asm::Op(_opcode, operands) => { + current_pc += 2; + current_pc = update_pc_by_operand(current_pc, operands); + } + } + } + + let pbc = PbcFile { + cp: self.constant_pool.clone(), + rom: bytecode, + }; + + let out = write_pbc(&pbc).map_err(|e| anyhow!(e))?; + Ok(EmitResult { + rom: out, + symbols, + }) + } +} diff --git a/crates/prometeu-compiler/src/backend/lowering.rs b/crates/prometeu-compiler/src/backend/lowering.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/prometeu-compiler/src/backend/mod.rs b/crates/prometeu-compiler/src/backend/mod.rs new file mode 100644 index 00000000..90038f0f --- /dev/null +++ b/crates/prometeu-compiler/src/backend/mod.rs @@ -0,0 +1,7 @@ +pub mod syscall_registry; +pub mod lowering; +pub mod emit_bytecode; +pub mod disasm; +pub mod artifacts; + +pub use emit_bytecode::emit_module; diff --git a/crates/prometeu-compiler/src/backend/syscall_registry.rs b/crates/prometeu-compiler/src/backend/syscall_registry.rs new file mode 100644 index 00000000..33de992c --- /dev/null +++ b/crates/prometeu-compiler/src/backend/syscall_registry.rs @@ -0,0 +1,26 @@ +use prometeu_core::hardware::Syscall; + +/// Maps a High-Level function name to its corresponding Virtual Machine Syscall ID. +pub fn map_syscall(name: &str) -> Option { + // Check if the name matches a standard syscall defined in the core firmware + if let Some(syscall) = Syscall::from_name(name) { + return Some(syscall as u32); + } + + // Handle special compiler intrinsics that don't map 1:1 to a single syscall ID. + match name { + // Color.rgb(r,g,b) is expanded to bitwise logic by the codegen + "Color.rgb" | "color.rgb" => Some(0xFFFF_FFFF), + _ => None, + } +} + +pub fn get_syscall_name(id: u32) -> Option { + if id == 0xFFFF_FFFF { + return Some("Color.rgb".to_string()); + } + + // This is a bit hacky because Syscall is an enum, we might need a better way in prometeu-core + // For now, let's just assume we can find it. + None // TODO: implement reverse mapping if needed +} diff --git a/crates/prometeu-compiler/src/codegen/codegen.rs b/crates/prometeu-compiler/src/codegen/codegen.rs index edeec2bd..2f853d1a 100644 --- a/crates/prometeu-compiler/src/codegen/codegen.rs +++ b/crates/prometeu-compiler/src/codegen/codegen.rs @@ -5,8 +5,8 @@ use anyhow::{anyhow, Result}; use oxc_allocator::Vec as OXCVec; use oxc_ast::ast::*; use oxc_ast_visit::{walk, Visit}; -use oxc_syntax::scope::ScopeFlags; use oxc_span::{GetSpan, Span}; +use oxc_syntax::scope::ScopeFlags; use prometeu_bytecode::asm; use prometeu_bytecode::asm::{assemble, Asm, Operand}; use prometeu_bytecode::opcode::OpCode; diff --git a/crates/prometeu-compiler/src/codegen/validator.rs b/crates/prometeu-compiler/src/codegen/validator.rs index d51741a6..05921ccb 100644 --- a/crates/prometeu-compiler/src/codegen/validator.rs +++ b/crates/prometeu-compiler/src/codegen/validator.rs @@ -3,8 +3,8 @@ use crate::codegen::syscall_map; use anyhow::{anyhow, Result}; use oxc_ast::ast::*; use oxc_ast_visit::{walk, Visit}; -use oxc_syntax::scope::ScopeFlags; use oxc_span::GetSpan; +use oxc_syntax::scope::ScopeFlags; /// AST Visitor that ensures the source code follows the Prometeu subset of JS/TS. /// diff --git a/crates/prometeu-compiler/src/common/diagnostics.rs b/crates/prometeu-compiler/src/common/diagnostics.rs new file mode 100644 index 00000000..e2515d8f --- /dev/null +++ b/crates/prometeu-compiler/src/common/diagnostics.rs @@ -0,0 +1,55 @@ +use crate::common::spans::Span; + +#[derive(Debug, Clone)] +pub enum DiagnosticLevel { + Error, + Warning, +} + +#[derive(Debug, Clone)] +pub struct Diagnostic { + pub level: DiagnosticLevel, + pub message: String, + pub span: Option, +} + +#[derive(Debug, Clone)] +pub struct DiagnosticBundle { + pub diagnostics: Vec, +} + +impl DiagnosticBundle { + pub fn new() -> Self { + Self { + diagnostics: Vec::new(), + } + } + + pub fn push(&mut self, diagnostic: Diagnostic) { + self.diagnostics.push(diagnostic); + } + + pub fn error(message: String, span: Option) -> Self { + let mut bundle = Self::new(); + bundle.push(Diagnostic { + level: DiagnosticLevel::Error, + message, + span, + }); + bundle + } + + pub fn has_errors(&self) -> bool { + self.diagnostics + .iter() + .any(|d| matches!(d.level, DiagnosticLevel::Error)) + } +} + +impl From for DiagnosticBundle { + fn from(diagnostic: Diagnostic) -> Self { + let mut bundle = Self::new(); + bundle.push(diagnostic); + bundle + } +} diff --git a/crates/prometeu-compiler/src/common/files.rs b/crates/prometeu-compiler/src/common/files.rs new file mode 100644 index 00000000..7ea479a5 --- /dev/null +++ b/crates/prometeu-compiler/src/common/files.rs @@ -0,0 +1,59 @@ +use std::path::PathBuf; +use std::sync::Arc; + +pub struct SourceFile { + pub id: usize, + pub path: PathBuf, + pub source: Arc, +} + +pub struct FileManager { + files: Vec, +} + +impl FileManager { + pub fn new() -> Self { + Self { files: Vec::new() } + } + + pub fn add(&mut self, path: PathBuf, source: String) -> usize { + let id = self.files.len(); + self.files.push(SourceFile { + id, + path, + source: Arc::from(source), + }); + id + } + + pub fn get_file(&self, id: usize) -> Option<&SourceFile> { + self.files.get(id) + } + + pub fn get_path(&self, id: usize) -> Option { + self.files.get(id).map(|f| f.path.clone()) + } + + pub fn lookup_pos(&self, file_id: usize, pos: u32) -> (usize, usize) { + let file = if let Some(f) = self.files.get(file_id) { + f + } else { + return (0, 0); + }; + + let mut line = 1; + let mut col = 1; + for (i, c) in file.source.char_indices() { + if i as u32 == pos { + break; + } + if c == '\n' { + line += 1; + col = 1; + } else { + col += 1; + } + } + (line, col) + } +} diff --git a/crates/prometeu-compiler/src/common/mod.rs b/crates/prometeu-compiler/src/common/mod.rs new file mode 100644 index 00000000..696f8745 --- /dev/null +++ b/crates/prometeu-compiler/src/common/mod.rs @@ -0,0 +1,5 @@ +pub mod diagnostics; +pub mod spans; +pub mod files; +pub mod symbols; +pub mod sourcemap; diff --git a/crates/prometeu-compiler/src/common/sourcemap.rs b/crates/prometeu-compiler/src/common/sourcemap.rs new file mode 100644 index 00000000..3aee0198 --- /dev/null +++ b/crates/prometeu-compiler/src/common/sourcemap.rs @@ -0,0 +1 @@ +// PC <-> Span mapping logic diff --git a/crates/prometeu-compiler/src/common/spans.rs b/crates/prometeu-compiler/src/common/spans.rs new file mode 100644 index 00000000..41f60b16 --- /dev/null +++ b/crates/prometeu-compiler/src/common/spans.rs @@ -0,0 +1,16 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Span { + pub file_id: usize, + pub start: u32, + pub end: u32, +} + +impl Span { + pub fn new(file_id: usize, start: u32, end: u32) -> Self { + Self { + file_id, + start, + end, + } + } +} diff --git a/crates/prometeu-compiler/src/common/symbols.rs b/crates/prometeu-compiler/src/common/symbols.rs new file mode 100644 index 00000000..42484dcd --- /dev/null +++ b/crates/prometeu-compiler/src/common/symbols.rs @@ -0,0 +1,9 @@ +use serde::Serialize; + +#[derive(Serialize, Debug, Clone)] +pub struct Symbol { + pub pc: u32, + pub file: String, + pub line: usize, + pub col: usize, +} diff --git a/crates/prometeu-compiler/src/compiler.rs b/crates/prometeu-compiler/src/compiler.rs index 95abcea1..95b0e66f 100644 --- a/crates/prometeu-compiler/src/compiler.rs +++ b/crates/prometeu-compiler/src/compiler.rs @@ -1,29 +1,11 @@ -use crate::codegen::validator::Validator; -use crate::codegen::Codegen; -use anyhow::{anyhow, Context, Result}; -use oxc_allocator::Allocator; -use oxc_ast::ast::*; -use oxc_parser::Parser; -use oxc_span::SourceType; -use prometeu_bytecode::disasm::disasm; -use serde::Serialize; -use std::collections::{HashMap, VecDeque}; -use std::fs; -use std::path::{Path, PathBuf}; - -/// Represents a debug symbol that maps a Program Counter (PC) to a specific -/// source code location. -#[derive(Serialize)] -pub struct Symbol { - /// The absolute address in the bytecode. - pub pc: u32, - /// The original source file path. - pub file: String, - /// 1-based line number. - pub line: usize, - /// 1-based column number. - pub col: usize, -} +use crate::backend; +use crate::common::files::FileManager; +use crate::common::symbols::Symbol; +use crate::frontends::ts::TypescriptFrontend; +use crate::frontends::Frontend; +use crate::ir; +use anyhow::Result; +use std::path::Path; /// The result of a successful compilation process. /// It contains the final binary and the metadata needed for debugging. @@ -36,156 +18,32 @@ pub struct CompilationUnit { impl CompilationUnit { /// Writes the compilation results to the disk. - /// - /// # Arguments - /// * `out` - The target path for the .pbc file. - /// * `emit_disasm` - If true, generates a .disasm.txt file next to the output. - /// * `emit_symbols` - If true, generates a symbols.json file for debuggers. pub fn export(&self, out: &Path, emit_disasm: bool, emit_symbols: bool) -> Result<()> { - // 1. Save the main binary - fs::write(out, &self.rom).with_context(|| format!("Failed to write PBC to {:?}", out))?; - - // 2. Export symbols for the HostDebugger - if emit_symbols { - let symbols_path = out.with_file_name("symbols.json"); - let symbols_json = serde_json::to_string_pretty(&self.symbols)?; - fs::write(&symbols_path, symbols_json)?; - } - - // 3. Export human-readable disassembly for developer inspection - if emit_disasm { - let disasm_path = out.with_extension("disasm.txt"); - - // Extract the actual bytecode (stripping the PBC header if present) - let rom_to_disasm = if let Ok(pbc) = prometeu_bytecode::pbc::parse_pbc(&self.rom) { - pbc.rom - } else { - self.rom.clone() - }; - - let instructions = disasm(&rom_to_disasm).map_err(|e| anyhow::anyhow!("Disassembly failed: {}", e))?; - - let mut disasm_text = String::new(); - for instr in instructions { - // Find a matching symbol to show which source line generated this instruction - let symbol = self.symbols.iter().find(|s| s.pc == instr.pc); - let comment = if let Some(s) = symbol { - format!(" ; {}:{}", s.file, s.line) - } else { - "".to_string() - }; - - let operands_str = instr.operands.iter() - .map(|o| format!("{:?}", o)) - .collect::>() - .join(" "); - - disasm_text.push_str(&format!("{:08X} {:?} {}{}\n", instr.pc, instr.opcode, operands_str, comment)); - } - fs::write(disasm_path, disasm_text)?; - } - - Ok(()) + let artifacts = backend::artifacts::Artifacts::new(self.rom.clone(), self.symbols.clone()); + artifacts.export(out, emit_disasm, emit_symbols) } } -/// Helper to resolve import paths (e.g., converting './utils' to './utils.ts'). -fn resolve_import(base_path: &Path, import_str: &str) -> Result { - let mut path = base_path.parent().unwrap().join(import_str); - - // Auto-append extensions if missing - if !path.exists() { - if path.with_extension("ts").exists() { - path.set_extension("ts"); - } else if path.with_extension("js").exists() { - path.set_extension("js"); - } - } - - if !path.exists() { - return Err(anyhow!("Cannot resolve import '{}' from {:?}", import_str, base_path)); - } - - Ok(path.canonicalize()?) -} - /// Orchestrates the compilation of a Prometeu project starting from an entry file. -/// -/// This function: -/// 1. Crawls the import graph to find all dependencies. -/// 2. Parses each file using `oxc`. -/// 3. Validates that the code adheres to Prometeu's restricted JS/TS subset. -/// 4. Hands over all programs to the `Codegen` for binary generation. pub fn compile(entry: &Path) -> Result { - let allocator = Allocator::default(); - let mut modules = HashMap::new(); - let mut queue = VecDeque::new(); + let mut file_manager = FileManager::new(); - // Start with the entry point - let entry_abs = entry.canonicalize() - .with_context(|| format!("Failed to canonicalize entry path: {:?}", entry))?; - queue.push_back(entry_abs.clone()); - - // --- PHASE 1: Dependency Resolution and Parsing --- - while let Some(path) = queue.pop_front() { - let path_str = path.to_string_lossy().to_string(); - if modules.contains_key(&path_str) { - continue; // Already processed - } - - let source_text = fs::read_to_string(&path) - .with_context(|| format!("Failed to read file: {:?}", path))?; + // 1. Select Frontend (Currently only TS is supported) + let frontend = TypescriptFrontend; + + // 2. Compile to IR + let ir_module = frontend.compile_to_ir(entry, &mut file_manager) + .map_err(|bundle| anyhow::anyhow!("Compilation failed with {} errors", bundle.diagnostics.len()))?; - // We use an Allocator to manage the lifetime of AST nodes and source strings efficiently - let source_text_ptr: &str = allocator.alloc_str(&source_text); + // 3. IR Validation + ir::validate::validate_module(&ir_module) + .map_err(|bundle| anyhow::anyhow!("IR Validation failed: {:?}", bundle))?; - let source_type = SourceType::from_path(&path).unwrap_or_default(); - let parser_ret = Parser::new(&allocator, source_text_ptr, source_type).parse(); - - // Handle syntax errors immediately - if !parser_ret.errors.is_empty() { - for error in parser_ret.errors { - eprintln!("{:?}", error); - } - return Err(anyhow!("Failed to parse module: {:?}", path)); - } - - // --- PHASE 2: Individual Module Validation --- - // Checks for things like 'class' or 'async' which are not supported by PVM. - Validator::validate(&parser_ret.program)?; - - // Discover new imports to crawl - for item in &parser_ret.program.body { - if let Statement::ImportDeclaration(decl) = item { - let import_path = decl.source.value.as_str(); - let resolved = resolve_import(&path, import_path)?; - queue.push_back(resolved); - } - } - - modules.insert(path_str, (source_text_ptr, parser_ret.program)); - } - - // --- PHASE 3: Code Generation --- - let entry_str = entry_abs.to_string_lossy().to_string(); - let mut program_list = Vec::new(); + // 4. Emit Bytecode + let result = backend::emit_module(&ir_module, &file_manager)?; - // Ensure the entry module (containing 'frame()') is processed first - let entry_data = modules.get(&entry_str).ok_or_else(|| anyhow!("Entry module not found after loading"))?; - program_list.push((entry_str.clone(), entry_data.0.to_string(), &entry_data.1)); - - // Collect all other modules - for (path, (source, program)) in &modules { - if path != &entry_str { - program_list.push((path.clone(), source.to_string(), program)); - } - } - - let mut codegen = Codegen::new(entry_str.clone(), entry_data.0.to_string()); - let rom = codegen.compile_programs(program_list)?; - Ok(CompilationUnit { - rom, - symbols: codegen.symbols, + rom: result.rom, + symbols: result.symbols, }) } diff --git a/crates/prometeu-compiler/src/frontends/mod.rs b/crates/prometeu-compiler/src/frontends/mod.rs new file mode 100644 index 00000000..0afe5723 --- /dev/null +++ b/crates/prometeu-compiler/src/frontends/mod.rs @@ -0,0 +1,17 @@ +use crate::common::diagnostics::DiagnosticBundle; +use crate::ir; +use std::path::Path; + +pub mod ts; + +use crate::common::files::FileManager; + +pub trait Frontend { + fn language(&self) -> &'static str; + + fn compile_to_ir( + &self, + entry: &Path, + file_manager: &mut FileManager, + ) -> Result; +} diff --git a/crates/prometeu-compiler/src/codegen/ast_util.rs b/crates/prometeu-compiler/src/frontends/ts/ast_util.rs similarity index 100% rename from crates/prometeu-compiler/src/codegen/ast_util.rs rename to crates/prometeu-compiler/src/frontends/ts/ast_util.rs diff --git a/crates/prometeu-compiler/src/codegen/input_map.rs b/crates/prometeu-compiler/src/frontends/ts/input_map.rs similarity index 100% rename from crates/prometeu-compiler/src/codegen/input_map.rs rename to crates/prometeu-compiler/src/frontends/ts/input_map.rs diff --git a/crates/prometeu-compiler/src/frontends/ts/mod.rs b/crates/prometeu-compiler/src/frontends/ts/mod.rs new file mode 100644 index 00000000..b1d285d3 --- /dev/null +++ b/crates/prometeu-compiler/src/frontends/ts/mod.rs @@ -0,0 +1,82 @@ +pub mod parse; +pub mod resolve; +pub mod validate; +pub mod to_ir; +pub mod ast_util; +pub mod input_map; + +use crate::common::diagnostics::DiagnosticBundle; +use crate::frontends::Frontend; +use crate::ir; +use oxc_allocator::Allocator; +use std::collections::{HashMap, VecDeque}; +use std::fs; +use std::path::Path; + +pub struct TypescriptFrontend; + +impl Frontend for TypescriptFrontend { + fn language(&self) -> &'static str { + "typescript" + } + + fn compile_to_ir( + &self, + entry: &Path, + file_manager: &mut crate::common::files::FileManager, + ) -> Result { + let allocator = Allocator::default(); + let mut modules = HashMap::new(); + let mut queue = VecDeque::new(); + + // Start with the entry point + let entry_abs = entry.canonicalize().map_err(|e| DiagnosticBundle::error(format!("Failed to canonicalize entry path: {}", e), None))?; + queue.push_back(entry_abs.clone()); + + // --- PHASE 1: Dependency Resolution and Parsing --- + while let Some(path) = queue.pop_front() { + let path_str: String = path.to_string_lossy().into_owned(); + if modules.contains_key(&path_str) { + continue; + } + + let source_text = fs::read_to_string(&path).map_err(|e| DiagnosticBundle::error(format!("Failed to read file: {}", e), None))?; + let file_id = file_manager.add(path.clone(), source_text.clone()); + let source_text_ptr = allocator.alloc_str(&source_text); + + let program = parse::parse_file(&allocator, &path).map_err(|e| DiagnosticBundle::error(format!("Failed to parse module: {}", e), None))?; + + // --- PHASE 2: Individual Module Validation --- + validate::Validator::validate(&program).map_err(|e| DiagnosticBundle::error(format!("Validation error: {}", e), None))?; + + // Discover new imports + for item in &program.body { + if let oxc_ast::ast::Statement::ImportDeclaration(decl) = item { + let import_path = decl.source.value.as_str(); + let resolved = resolve::resolve_import(&path, import_path).map_err(|e| DiagnosticBundle::error(format!("Resolve error: {}", e), None))?; + queue.push_back(resolved); + } + } + + modules.insert(path_str, (file_id, source_text_ptr, program)); + } + + // --- PHASE 3: To IR --- + let entry_str = entry_abs.to_string_lossy().to_string(); + let mut program_list = Vec::new(); + + let entry_data = modules.get(&entry_str).ok_or_else(|| DiagnosticBundle::error("Entry module not found".into(), None))?; + program_list.push((entry_data.0, entry_str.clone(), entry_data.1.to_string(), &entry_data.2)); + + for (path, (file_id, source, program)) in &modules { + if path != &entry_str { + program_list.push((*file_id, path.clone(), source.to_string(), program)); + } + } + + let mut to_ir_engine = to_ir::ToIR::new(entry_str.clone(), entry_data.1.to_string()); + let module = to_ir_engine.compile_to_ir(program_list).map_err(|e| DiagnosticBundle::error(format!("IR Generation error: {}", e), None))?; + + Ok(module) + } +} diff --git a/crates/prometeu-compiler/src/frontends/ts/parse.rs b/crates/prometeu-compiler/src/frontends/ts/parse.rs new file mode 100644 index 00000000..862429ae --- /dev/null +++ b/crates/prometeu-compiler/src/frontends/ts/parse.rs @@ -0,0 +1,25 @@ +use anyhow::{anyhow, Context, Result}; +use oxc_allocator::Allocator; +use oxc_ast::ast::Program; +use oxc_parser::Parser; +use oxc_span::SourceType; +use std::fs; +use std::path::Path; + +pub fn parse_file<'a>(allocator: &'a Allocator, path: &Path) -> Result> { + let source_text = fs::read_to_string(path) + .with_context(|| format!("Failed to read file: {:?}", path))?; + + let source_text_ptr = allocator.alloc_str(&source_text); + let source_type = SourceType::from_path(path).unwrap_or_default(); + let parser_ret = Parser::new(allocator, source_text_ptr, source_type).parse(); + + if !parser_ret.errors.is_empty() { + for error in parser_ret.errors { + eprintln!("{:?}", error); + } + return Err(anyhow!("Failed to parse module: {:?}", path)); + } + + Ok(parser_ret.program) +} diff --git a/crates/prometeu-compiler/src/frontends/ts/resolve.rs b/crates/prometeu-compiler/src/frontends/ts/resolve.rs new file mode 100644 index 00000000..fbbf645f --- /dev/null +++ b/crates/prometeu-compiler/src/frontends/ts/resolve.rs @@ -0,0 +1,22 @@ +use anyhow::{anyhow, Result}; +use std::path::{Path, PathBuf}; + +/// Helper to resolve import paths (e.g., converting './utils' to './utils.ts'). +pub fn resolve_import(base_path: &Path, import_str: &str) -> Result { + let mut path = base_path.parent().unwrap().join(import_str); + + // Auto-append extensions if missing + if !path.exists() { + if path.with_extension("ts").exists() { + path.set_extension("ts"); + } else if path.with_extension("js").exists() { + path.set_extension("js"); + } + } + + if !path.exists() { + return Err(anyhow!("Cannot resolve import '{}' from {:?}", import_str, base_path)); + } + + Ok(path.canonicalize()?) +} diff --git a/crates/prometeu-compiler/src/frontends/ts/to_ir.rs b/crates/prometeu-compiler/src/frontends/ts/to_ir.rs new file mode 100644 index 00000000..4bae6cf7 --- /dev/null +++ b/crates/prometeu-compiler/src/frontends/ts/to_ir.rs @@ -0,0 +1,770 @@ +use crate::backend::syscall_registry as syscall_map; +use crate::common::spans::Span as IRSpan; +use crate::frontends::ts::ast_util; +use crate::frontends::ts::input_map; +use crate::ir; +use crate::ir::instr::{InstrKind, Instruction as IRInstruction, Label as IRLabel}; +use anyhow::{anyhow, Result}; +use oxc_allocator::Vec as OXCVec; +use oxc_ast::ast::*; +use oxc_ast_visit::{walk, Visit}; +use oxc_span::{GetSpan, Span}; +use oxc_syntax::scope::ScopeFlags; +use prometeu_core::model::Color; +use std::collections::HashMap; + +/// Helper to count local variables and hoisted functions in a function body. +struct LocalCounter { + count: u32, +} + +impl<'a> Visit<'a> for LocalCounter { + fn visit_statement(&mut self, stmt: &Statement<'a>) { + match stmt { + Statement::FunctionDeclaration(f) => { + self.visit_function(f, ScopeFlags::empty()); + } + _ => walk::walk_statement(self, stmt), + } + } + + fn visit_variable_declaration(&mut self, decl: &VariableDeclaration<'a>) { + self.count += decl.declarations.len() as u32; + walk::walk_variable_declaration(self, decl); + } + + fn visit_function(&mut self, f: &Function<'a>, _flags: ScopeFlags) { + if f.id.is_some() { + self.count += 1; + } + // Stop recursion: nested functions have their own frames and locals. + } +} + +/// Metadata for a symbol (variable or function) in the symbol table. +#[allow(dead_code)] +struct SymbolEntry { + name: String, + slot_index: u32, + scope_depth: usize, + is_const: bool, + is_initialized: bool, + // PC of the instruction where the symbol was declared/initialized. + // Useful for debug symbols and potentially for more advanced TDZ checks. + declared_at_pc: u32, + initialized_at_pc: u32, +} + +/// The TS AST to Prometeu IR translator. +pub struct ToIR { + /// Name of the file being compiled (used for debug symbols). + file_name: String, + /// Full source code of the file (used for position lookup). + source_text: String, + /// ID of the file being compiled. + current_file_id: usize, + /// The stream of generated IR instructions. + pub instructions: Vec, + /// Scoped symbol table. Each element is a scope level containing a map of symbols. + symbol_table: Vec>, + /// Current depth of the scope (0 is global/function top-level). + scope_depth: usize, + /// Mapping of global variable names to their slots in the VM's global memory. + globals: HashMap, + /// Counter for the next available local variable ID. + next_local: u32, + /// Counter for the next available global variable ID. + next_global: u32, + /// Counter for generating unique labels (e.g., for 'if' or 'while' blocks). + label_count: u32, +} + +impl ToIR { + /// Creates a new ToIR instance for a specific file. + pub fn new(file_name: String, source_text: String) -> Self { + Self { + file_name, + source_text, + current_file_id: 0, + instructions: Vec::new(), + symbol_table: Vec::new(), + scope_depth: 0, + globals: HashMap::new(), + next_local: 0, + next_global: 0, + label_count: 0, + } + } + + /// Enters a new scope level. + fn enter_scope(&mut self) { + self.symbol_table.push(HashMap::new()); + self.scope_depth += 1; + } + + /// Exits the current scope level. + fn exit_scope(&mut self) { + self.symbol_table.pop(); + self.scope_depth -= 1; + } + + /// Declares a new symbol in the current scope. + fn declare_symbol(&mut self, name: String, is_const: bool, is_initialized: bool, span: Span) -> Result<&mut SymbolEntry> { + let current_scope = self.symbol_table.last_mut().ok_or_else(|| anyhow!("No active scope"))?; + + if current_scope.contains_key(&name) { + return Err(anyhow!("Variable '{}' already declared in this scope at {:?}", name, span)); + } + + let slot_index = self.next_local; + self.next_local += 1; + + let entry = SymbolEntry { + name: name.clone(), + slot_index, + scope_depth: self.scope_depth, + is_const, + is_initialized, + declared_at_pc: 0, // Will be filled if needed + initialized_at_pc: 0, + }; + + current_scope.insert(name.clone(), entry); + Ok(self.symbol_table.last_mut().unwrap().get_mut(&name).unwrap()) + } + + /// Marks a symbol as initialized. + fn initialize_symbol(&mut self, name: &str) { + for scope in self.symbol_table.iter_mut().rev() { + if let Some(entry) = scope.get_mut(name) { + entry.is_initialized = true; + return; + } + } + } + + /// Resolves a symbol name to its entry, searching from inner to outer scopes. + fn resolve_symbol(&self, name: &str) -> Option<&SymbolEntry> { + for scope in self.symbol_table.iter().rev() { + if let Some(entry) = scope.get(name) { + return Some(entry); + } + } + None + } + + /// Discovers all function declarations in a program, including nested ones. + fn discover_functions<'a>( + &self, + file_id: usize, + file: String, + source: String, + program: &'a Program<'a>, + all_functions: &mut Vec<(usize, String, String, &'a Function<'a>)>, + ) { + struct Collector<'a, 'b> { + file_id: usize, + file: String, + source: String, + functions: &'b mut Vec<(usize, String, String, &'a Function<'a>)>, + } + + impl<'a, 'b> Visit<'a> for Collector<'a, 'b> { + fn visit_function(&mut self, f: &Function<'a>, flags: ScopeFlags) { + // Safety: The program AST lives long enough as it's owned by the caller + // of compile_programs and outlives the compilation process. + let f_ref = unsafe { std::mem::transmute::<&Function<'a>, &'a Function<'a>>(f) }; + self.functions.push((self.file_id, self.file.clone(), self.source.clone(), f_ref)); + walk::walk_function(self, f, flags); + } + } + + let mut collector = Collector { + file_id, + file, + source, + functions: all_functions, + }; + collector.visit_program(program); + } + + + + /// Compiles multiple programs (files) into a single Prometeu IR Module. + pub fn compile_to_ir(&mut self, programs: Vec<(usize, String, String, &Program)>) -> Result { + // --- FIRST PASS: Global Functions and Variables Collection --- + let mut all_functions_ast = Vec::new(); + + for (file_id, file, source, program) in &programs { + for item in &program.body { + match item { + Statement::ExportNamedDeclaration(decl) => { + if let Some(Declaration::VariableDeclaration(var)) = &decl.declaration { + self.export_global_variable_declarations(&var); + } + } + Statement::VariableDeclaration(var) => { + self.export_global_variable_declarations(&var); + } + _ => {} + } + } + + self.discover_functions(*file_id, file.clone(), source.clone(), program, &mut all_functions_ast); + } + + // --- ENTRY POINT VERIFICATION --- + let mut frame_fn_name = None; + if let Some((_, _, _, entry_program)) = programs.first() { + for item in &entry_program.body { + let f_opt = match item { + Statement::FunctionDeclaration(f) => Some(f.as_ref()), + Statement::ExportNamedDeclaration(decl) => { + if let Some(Declaration::FunctionDeclaration(f)) = &decl.declaration { + Some(f.as_ref()) + } else { + None + } + } + _ => None, + }; + + if let Some(f) = f_opt { + if let Some(ident) = &f.id { + if ident.name == "frame" { + frame_fn_name = Some(ident.name.to_string()); + break; + } + } + } + } + } + + let frame_fn_name = frame_fn_name.ok_or_else(|| anyhow!("export function frame() not found in entry file"))?; + + let mut module = ir::Module::new("main".to_string()); + + // Populate globals in IR + for (name, slot) in &self.globals { + module.globals.push(ir::module::Global { + name: name.clone(), + r#type: ir::types::Type::Any, + slot: *slot, + }); + } + + // --- GLOBAL INITIALIZATION AND MAIN LOOP (__init) --- + self.instructions.clear(); + for (file_id, file, source, program) in &programs { + self.file_name = file.clone(); + self.source_text = source.clone(); + self.current_file_id = *file_id; + for item in &program.body { + let var_opt = match item { + Statement::VariableDeclaration(var) => Some(var.as_ref()), + Statement::ExportNamedDeclaration(decl) => { + if let Some(Declaration::VariableDeclaration(var)) = &decl.declaration { + Some(var.as_ref()) + } else { + None + } + } + _ => None, + }; + + if let Some(var) = var_opt { + for decl in &var.declarations { + if let BindingPattern::BindingIdentifier(ident) = &decl.id { + let name = ident.name.to_string(); + let id = *self.globals.get(&name).unwrap(); + if let Some(init) = &decl.init { + self.compile_expr(init)?; + } else { + self.emit_instr(InstrKind::PushInt(0), decl.span); + } + self.emit_instr(InstrKind::SetGlobal(id), decl.span); + } + } + } + } + } + + self.emit_label("entry".to_string()); + self.emit_instr(InstrKind::Call { name: frame_fn_name, arg_count: 0 }, Span::default()); + self.emit_instr(InstrKind::Pop, Span::default()); + self.emit_instr(InstrKind::FrameSync, Span::default()); + self.emit_instr(InstrKind::Jmp(IRLabel("entry".to_string())), Span::default()); + + module.functions.push(ir::module::Function { + name: "__init".to_string(), + params: Vec::new(), + return_type: ir::types::Type::Void, + body: self.instructions.clone(), + }); + + // --- FUNCTION COMPILATION --- + for (file_id, file, source, f) in all_functions_ast { + self.instructions.clear(); + self.file_name = file; + self.source_text = source; + self.current_file_id = file_id; + if let Some(ident) = &f.id { + let name = ident.name.to_string(); + self.compile_function(f)?; + self.emit_instr(InstrKind::Ret, Span::default()); + + module.functions.push(ir::module::Function { + name, + params: Vec::new(), // TODO: map parameters to IR + return_type: ir::types::Type::Any, + body: self.instructions.clone(), + }); + } + } + + Ok(module) + } + + /// Registers a global variable in the symbol table. + /// Global variables are accessible from any function and persist between frames. + fn export_global_variable_declarations(&mut self, var: &VariableDeclaration) { + for decl in &var.declarations { + if let BindingPattern::BindingIdentifier(ident) = &decl.id { + let name = ident.name.to_string(); + if !self.globals.contains_key(&name) { + let id = self.next_global; + self.globals.insert(name, id); + self.next_global += 1; + } + } + } + } + + /// Compiles a function declaration. + /// + /// Functions in Prometeu follow the ABI: + /// 1. Parameters are mapped to the first `n` local slots. + /// 2. `PushScope` is called to protect the caller's environment. + /// 3. The body is compiled sequentially. + /// 4. `PopScope` and `Push Null` are executed before `Ret` to ensure the stack rule. + fn compile_function(&mut self, f: &Function) -> Result<()> { + self.symbol_table.clear(); + self.scope_depth = 0; + self.next_local = 0; + + // Start scope for parameters and local variables + self.enter_scope(); + self.emit_instr(InstrKind::PushScope, f.span); + + // Map parameters to locals (they are pushed by the caller before the Call instruction) + for param in &f.params.items { + if let BindingPattern::BindingIdentifier(ident) = ¶m.pattern { + let name = ident.name.to_string(); + // Parameters are considered initialized + self.declare_symbol(name, false, true, ident.span)?; + } + } + + if let Some(body) = &f.body { + // Reserve slots for all local variables and hoisted functions + let locals_to_reserve = self.count_locals(&body.statements); + for _ in 0..locals_to_reserve { + // Initializing with I32 0 as it's the safest default for Prometeu VM + self.emit_instr(InstrKind::PushInt(0), f.span); + } + + // Function and Variable hoisting within the function scope + self.hoist_functions(&body.statements)?; + self.hoist_variables(&body.statements)?; + + for stmt in &body.statements { + self.compile_stmt(stmt)?; + } + } + + // ABI Rule: Every function MUST leave exactly one value on the stack before RET. + // If the function doesn't have a return statement, we push Null. + self.emit_instr(InstrKind::PopScope, Span::default()); + self.emit_instr(InstrKind::PushNull, Span::default()); + Ok(()) + } + + /// Counts the total number of local variable and function declarations in a function body. + fn count_locals(&self, statements: &OXCVec) -> u32 { + let mut counter = LocalCounter { count: 0 }; + for stmt in statements { + counter.visit_statement(stmt); + } + counter.count + } + + /// Hoists function declarations to the top of the current scope. + fn hoist_functions(&mut self, statements: &OXCVec) -> Result<()> { + for stmt in statements { + if let Statement::FunctionDeclaration(f) = stmt { + if let Some(ident) = &f.id { + let name = ident.name.to_string(); + // Functions are hoisted and already considered initialized + self.declare_symbol(name, false, true, ident.span)?; + } + } + } + Ok(()) + } + + /// Hoists variable declarations (let/const) to the top of the current scope. + fn hoist_variables(&mut self, statements: &OXCVec) -> Result<()> { + for stmt in statements { + if let Statement::VariableDeclaration(var) = stmt { + let is_const = var.kind == VariableDeclarationKind::Const; + for decl in &var.declarations { + if let BindingPattern::BindingIdentifier(ident) = &decl.id { + let name = ident.name.to_string(); + // Register as uninitialized for TDZ + self.declare_symbol(name, is_const, false, ident.span)?; + } + } + } + } + Ok(()) + } + + /// Translates a Statement into bytecode. + fn compile_stmt(&mut self, stmt: &Statement) -> Result<()> { + match stmt { + // var x = 10; + Statement::VariableDeclaration(var) => { + let is_const = var.kind == VariableDeclarationKind::Const; + for decl in &var.declarations { + if let BindingPattern::BindingIdentifier(ident) = &decl.id { + let name = ident.name.to_string(); + + // Variable should already be in the symbol table due to hoisting + let entry = self.resolve_symbol(&name) + .ok_or_else(|| anyhow!("Internal compiler error: symbol '{}' not hoisted at {:?}", name, ident.span))?; + + let slot_index = entry.slot_index; + + if let Some(init) = &decl.init { + self.compile_expr(init)?; + self.emit_instr(InstrKind::SetLocal(slot_index), decl.span); + self.initialize_symbol(&name); + } else { + if is_const { + return Err(anyhow!("Missing initializer in const declaration at {:?}", decl.span)); + } + // Default initialization to 0 + self.emit_instr(InstrKind::PushInt(0), decl.span); + self.emit_instr(InstrKind::SetLocal(slot_index), decl.span); + self.initialize_symbol(&name); + } + } + } + } + // console.log("hello"); + Statement::ExpressionStatement(expr_stmt) => { + self.compile_expr(&expr_stmt.expression)?; + // ABI requires us to Pop unused return values from the stack to prevent leaks + self.emit_instr(InstrKind::Pop, expr_stmt.span); + } + // if (a == b) { ... } else { ... } + Statement::IfStatement(if_stmt) => { + let else_label = self.new_label("else"); + let end_label = self.new_label("end_if"); + + self.compile_expr(&if_stmt.test)?; + self.emit_instr(InstrKind::JmpIfFalse(IRLabel(else_label.clone())), if_stmt.span); + + self.compile_stmt(&if_stmt.consequent)?; + self.emit_instr(InstrKind::Jmp(IRLabel(end_label.clone())), Span::default()); + + self.emit_label(else_label); + if let Some(alt) = &if_stmt.alternate { + self.compile_stmt(alt)?; + } + + self.emit_label(end_label); + } + // { let x = 1; } + Statement::BlockStatement(block) => { + self.enter_scope(); + self.emit_instr(InstrKind::PushScope, block.span); + + // Hoist functions and variables in the block + self.hoist_functions(&block.body)?; + self.hoist_variables(&block.body)?; + + for stmt in &block.body { + self.compile_stmt(stmt)?; + } + + self.emit_instr(InstrKind::PopScope, block.span); + self.exit_scope(); + } + // Function declarations are handled by hoisting and compiled separately + Statement::FunctionDeclaration(_) => {} + _ => return Err(anyhow!("Unsupported statement type at {:?}", stmt.span())), + } + Ok(()) + } + + /// Translates an Expression into bytecode. + /// Expressions always leave exactly one value at the top of the stack. + fn compile_expr(&mut self, expr: &Expression) -> Result<()> { + match expr { + // Literals: push the value directly onto the stack + Expression::NumericLiteral(n) => { + let val = n.value; + if val.fract() == 0.0 && val >= i32::MIN as f64 && val <= i32::MAX as f64 { + self.emit_instr(InstrKind::PushInt(val as i64), n.span); + } else { + self.emit_instr(InstrKind::PushFloat(val), n.span); + } + } + Expression::BooleanLiteral(b) => { + self.emit_instr(InstrKind::PushBool(b.value), b.span); + } + Expression::StringLiteral(s) => { + self.emit_instr(InstrKind::PushString(s.value.to_string()), s.span); + } + Expression::NullLiteral(n) => { + self.emit_instr(InstrKind::PushNull, n.span); + } + // Variable access: resolve to GetLocal or GetGlobal + Expression::Identifier(ident) => { + let name = ident.name.to_string(); + if let Some(entry) = self.resolve_symbol(&name) { + if !entry.is_initialized { + return Err(anyhow!("TDZ Violation: Variable '{}' accessed before initialization at {:?}", name, ident.span)); + } + self.emit_instr(InstrKind::GetLocal(entry.slot_index), ident.span); + } else if let Some(&id) = self.globals.get(&name) { + self.emit_instr(InstrKind::GetGlobal(id), ident.span); + } else { + return Err(anyhow!("Undefined variable: {} at {:?}", name, ident.span)); + } + } + // Assignment: evaluate RHS and store result in LHS slot + Expression::AssignmentExpression(assign) => { + if let AssignmentTarget::AssignmentTargetIdentifier(ident) = &assign.left { + let name = ident.name.to_string(); + + if let Some(entry) = self.resolve_symbol(&name) { + if entry.is_const { + return Err(anyhow!("Assignment to constant variable '{}' at {:?}", name, assign.span)); + } + if !entry.is_initialized { + return Err(anyhow!("TDZ Violation: Variable '{}' accessed before initialization at {:?}", name, assign.span)); + } + let slot_index = entry.slot_index; + self.compile_expr(&assign.right)?; + self.emit_instr(InstrKind::SetLocal(slot_index), assign.span); + self.emit_instr(InstrKind::GetLocal(slot_index), assign.span); // Assignment returns the value + } else if let Some(&id) = self.globals.get(&name) { + self.compile_expr(&assign.right)?; + self.emit_instr(InstrKind::SetGlobal(id), assign.span); + self.emit_instr(InstrKind::GetGlobal(id), assign.span); + } else { + return Err(anyhow!("Undefined variable: {} at {:?}", name, ident.span)); + } + } else { + return Err(anyhow!("Unsupported assignment target at {:?}", assign.span)); + } + } + // Binary operations: evaluate both sides and apply the opcode + Expression::BinaryExpression(bin) => { + self.compile_expr(&bin.left)?; + self.compile_expr(&bin.right)?; + let kind = match bin.operator { + BinaryOperator::Addition => InstrKind::Add, + BinaryOperator::Subtraction => InstrKind::Sub, + BinaryOperator::Multiplication => InstrKind::Mul, + BinaryOperator::Division => InstrKind::Div, + BinaryOperator::Equality => InstrKind::Eq, + BinaryOperator::Inequality => InstrKind::Neq, + BinaryOperator::LessThan => InstrKind::Lt, + BinaryOperator::GreaterThan => InstrKind::Gt, + BinaryOperator::LessEqualThan => InstrKind::Lte, + BinaryOperator::GreaterEqualThan => InstrKind::Gte, + _ => return Err(anyhow!("Unsupported binary operator {:?} at {:?}", bin.operator, bin.span)), + }; + self.emit_instr(kind, bin.span); + } + // Logical operations: evaluate both sides and apply the opcode + Expression::LogicalExpression(log) => { + self.compile_expr(&log.left)?; + self.compile_expr(&log.right)?; + let kind = match log.operator { + LogicalOperator::And => InstrKind::And, + LogicalOperator::Or => InstrKind::Or, + _ => return Err(anyhow!("Unsupported logical operator {:?} at {:?}", log.operator, log.span)), + }; + self.emit_instr(kind, log.span); + } + // Unary operations: evaluate argument and apply the opcode + Expression::UnaryExpression(unary) => { + self.compile_expr(&unary.argument)?; + let kind = match unary.operator { + UnaryOperator::UnaryNegation => InstrKind::Neg, + UnaryOperator::UnaryPlus => return Ok(()), + UnaryOperator::LogicalNot => InstrKind::Not, + _ => return Err(anyhow!("Unsupported unary operator {:?} at {:?}", unary.operator, unary.span)), + }; + self.emit_instr(kind, unary.span); + } + // Function calls: resolve to Syscall or Call + Expression::CallExpression(call) => { + let name = ast_util::get_callee_name(&call.callee)?; + if let Some(syscall_id) = syscall_map::map_syscall(&name) { + if syscall_id == 0xFFFF_FFFF { + // Special case for Color.rgb(r, g, b) + // It's compiled to a sequence of bitwise operations for performance + if call.arguments.len() != 3 { + return Err(anyhow!("Color.rgb expects 3 arguments at {:?}", call.span)); + } + + // Argument 0: r (shift right 3, shift left 11) + if let Some(expr) = call.arguments[0].as_expression() { + self.compile_expr(expr)?; + self.emit_instr(InstrKind::PushInt(3), call.span); + self.emit_instr(InstrKind::Shr, call.span); + self.emit_instr(InstrKind::PushInt(11), call.span); + self.emit_instr(InstrKind::Shl, call.span); + } + + // Argument 1: g (shift right 2, shift left 5) + if let Some(expr) = call.arguments[1].as_expression() { + self.compile_expr(expr)?; + self.emit_instr(InstrKind::PushInt(2), call.span); + self.emit_instr(InstrKind::Shr, call.span); + self.emit_instr(InstrKind::PushInt(5), call.span); + self.emit_instr(InstrKind::Shl, call.span); + } + + self.emit_instr(InstrKind::BitOr, call.span); + + // Argument 2: b (shift right 3) + if let Some(expr) = call.arguments[2].as_expression() { + self.compile_expr(expr)?; + self.emit_instr(InstrKind::PushInt(3), call.span); + self.emit_instr(InstrKind::Shr, call.span); + } + + self.emit_instr(InstrKind::BitOr, call.span); + + } else { + // Standard System Call + for arg in &call.arguments { + if let Some(expr) = arg.as_expression() { + self.compile_expr(expr)?; + } + } + self.emit_instr(InstrKind::Syscall(name), call.span); + } + } else { + // Local function call (to a function defined in the project) + for arg in &call.arguments { + if let Some(expr) = arg.as_expression() { + self.compile_expr(expr)?; + } + } + self.emit_instr(InstrKind::Call { name, arg_count: call.arguments.len() as u32 }, call.span); + } + } + // Member access (e.g., Color.RED, Pad.A.down) + Expression::StaticMemberExpression(member) => { + let full_name = ast_util::get_member_expr_name(expr)?; + + if full_name.to_lowercase().starts_with("color.") { + // Resolved at compile-time to literal values + match full_name.to_lowercase().as_str() { + "color.black" => self.emit_instr(InstrKind::PushInt(Color::BLACK.raw() as i64), member.span), + "color.white" => self.emit_instr(InstrKind::PushInt(Color::WHITE.raw() as i64), member.span), + "color.red" => self.emit_instr(InstrKind::PushInt(Color::RED.raw() as i64), member.span), + "color.green" => self.emit_instr(InstrKind::PushInt(Color::GREEN.raw() as i64), member.span), + "color.blue" => self.emit_instr(InstrKind::PushInt(Color::BLUE.raw() as i64), member.span), + "color.yellow" => self.emit_instr(InstrKind::PushInt(Color::YELLOW.raw() as i64), member.span), + "color.cyan" => self.emit_instr(InstrKind::PushInt(Color::CYAN.raw() as i64), member.span), + "color.gray" | "color.grey" => self.emit_instr(InstrKind::PushInt(Color::GRAY.raw() as i64), member.span), + "color.orange" => self.emit_instr(InstrKind::PushInt(Color::ORANGE.raw() as i64), member.span), + "color.indigo" => self.emit_instr(InstrKind::PushInt(Color::INDIGO.raw() as i64), member.span), + "color.magenta" => self.emit_instr(InstrKind::PushInt(Color::MAGENTA.raw() as i64), member.span), + "color.colorkey" | "color.color_key" => self.emit_instr(InstrKind::PushInt(Color::COLOR_KEY.raw() as i64), member.span), + _ => return Err(anyhow!("Unsupported color constant: {} at {:?}", full_name, member.span)), + } + } else if full_name.to_lowercase().starts_with("pad.") { + // Re-mapped to specific input syscalls + let parts: Vec<&str> = full_name.split('.').collect(); + if parts.len() == 3 { + let btn_name = parts[1]; + let state_name = parts[2]; + let btn_id = input_map::map_btn_name(btn_name)?; + let syscall_name = match state_name { + "down" => "InputGetPad", + "pressed" => "InputGetPadPressed", + "released" => "InputGetPadReleased", + "holdFrames" => "InputGetPadHold", + _ => return Err(anyhow!("Unsupported button state: {} at {:?}", state_name, member.span)), + }; + self.emit_instr(InstrKind::PushInt(btn_id as i64), member.span); + self.emit_instr(InstrKind::Syscall(syscall_name.to_string()), member.span); + } else { + return Err(anyhow!("Partial Pad access not supported: {} at {:?}", full_name, member.span)); + } + } else if full_name.to_lowercase().starts_with("touch.") { + // Re-mapped to specific touch syscalls + let parts: Vec<&str> = full_name.split('.').collect(); + match parts.len() { + 2 => { + let prop = parts[1]; + let syscall_name = match prop { + "x" => "TouchGetX", + "y" => "TouchGetY", + _ => return Err(anyhow!("Unsupported touch property: {} at {:?}", prop, member.span)), + }; + self.emit_instr(InstrKind::Syscall(syscall_name.to_string()), member.span); + } + 3 if parts[1] == "button" => { + let state_name = parts[2]; + let syscall_name = match state_name { + "down" => "TouchIsDown", + "pressed" => "TouchIsPressed", + "released" => "TouchIsReleased", + "holdFrames" => "TouchGetHold", + _ => return Err(anyhow!("Unsupported touch button state: {} at {:?}", state_name, member.span)), + }; + self.emit_instr(InstrKind::Syscall(syscall_name.to_string()), member.span); + } + _ => return Err(anyhow!("Unsupported touch access: {} at {:?}", full_name, member.span)), + } + } else { + return Err(anyhow!("Member expression outside call not supported: {} at {:?}", full_name, member.span)); + } + } + _ => return Err(anyhow!("Unsupported expression type at {:?}", expr.span())), + } + Ok(()) + } + + /// Generates a new unique label name for control flow. + fn new_label(&mut self, prefix: &str) -> String { + let label = format!("{}_{}", prefix, self.label_count); + self.label_count += 1; + label + } + + /// Emits a label definition in the instruction stream. + fn emit_label(&mut self, name: String) { + self.instructions.push(IRInstruction::new(InstrKind::Label(IRLabel(name)), None)); + } + + /// Emits an IR instruction. + fn emit_instr(&mut self, kind: InstrKind, span: Span) { + let ir_span = if !span.is_unspanned() { + Some(IRSpan::new(0, span.start, span.end)) + } else { + None + }; + self.instructions.push(IRInstruction::new(kind, ir_span)); + } +} diff --git a/crates/prometeu-compiler/src/frontends/ts/validate.rs b/crates/prometeu-compiler/src/frontends/ts/validate.rs new file mode 100644 index 00000000..7bf78266 --- /dev/null +++ b/crates/prometeu-compiler/src/frontends/ts/validate.rs @@ -0,0 +1,180 @@ +use crate::backend::syscall_registry as syscall_map; +use crate::frontends::ts::ast_util; +use anyhow::{anyhow, Result}; +use oxc_ast::ast::*; +use oxc_ast_visit::{walk, Visit}; +use oxc_span::GetSpan; +use oxc_syntax::scope::ScopeFlags; + +/// AST Visitor that ensures the source code follows the Prometeu subset of JS/TS. +/// +/// Since the Prometeu Virtual Machine is highly optimized and focused on game logic, +/// it does not support the full ECMA-262 specification (e.g., no `async`, `class`, +/// `try/catch`, or `generators`). +pub struct Validator { + /// List of validation errors found during the pass. + errors: Vec, + /// Set of function names defined in the project, used to distinguish + /// local calls from invalid references. + local_functions: std::collections::HashSet, +} + +impl Validator { + /// Performs a full validation pass on a program. + /// + /// Returns `Ok(())` if the program is valid, or a combined error message + /// listing all violations. + pub fn validate(program: &Program) -> Result<()> { + let mut validator = Self { + errors: Vec::new(), + local_functions: std::collections::HashSet::new(), + }; + + // 1. Discovery Pass: Collect all function names and imports recursively + validator.discover_functions(program); + + // 2. Traversal Pass: Check every node for compatibility + validator.visit_program(program); + + if validator.errors.is_empty() { + Ok(()) + } else { + Err(anyhow!("Validation errors:\n{}", validator.errors.join("\n"))) + } + } + + /// Recursively discovers all function declarations in the program. + fn discover_functions(&mut self, program: &Program) { + struct FunctionDiscoverer<'a> { + functions: &'a mut std::collections::HashSet, + } + impl<'a, 'b> Visit<'b> for FunctionDiscoverer<'a> { + fn visit_function(&mut self, f: &Function<'b>, _flags: ScopeFlags) { + if let Some(ident) = &f.id { + self.functions.insert(ident.name.to_string()); + } + walk::walk_function(self, f, _flags); + } + fn visit_import_declaration(&mut self, decl: &ImportDeclaration<'b>) { + if let Some(specifiers) = &decl.specifiers { + for specifier in specifiers { + match specifier { + ImportDeclarationSpecifier::ImportSpecifier(s) => { + self.functions.insert(s.local.name.to_string()); + } + ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => { + self.functions.insert(s.local.name.to_string()); + } + ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => { + self.functions.insert(s.local.name.to_string()); + } + } + } + } + } + } + let mut discoverer = FunctionDiscoverer { functions: &mut self.local_functions }; + discoverer.visit_program(program); + } +} + +impl<'a> Visit<'a> for Validator { + /// Validates that only supported expressions are used. + fn visit_expression(&mut self, expr: &Expression<'a>) { + match expr { + Expression::NumericLiteral(_) | + Expression::BooleanLiteral(_) | + Expression::StringLiteral(_) | + Expression::NullLiteral(_) | + Expression::Identifier(_) | + Expression::AssignmentExpression(_) | + Expression::BinaryExpression(_) | + Expression::LogicalExpression(_) | + Expression::UnaryExpression(_) | + Expression::CallExpression(_) | + Expression::StaticMemberExpression(_) => { + // Basic JS logic is supported. + walk::walk_expression(self, expr); + } + _ => { + self.errors.push(format!("Unsupported expression type at {:?}. Note: Closures, arrow functions, and object literals are not yet supported.", expr.span())); + } + } + } + + fn visit_call_expression(&mut self, expr: &CallExpression<'a>) { + if let Ok(name) = ast_util::get_callee_name(&expr.callee) { + if syscall_map::map_syscall(&name).is_none() && !self.local_functions.contains(&name) { + self.errors.push(format!("Unsupported function call: {} at {:?}", name, expr.span)); + } + } else { + self.errors.push(format!("Unsupported callee expression at {:?}", expr.callee.span())); + } + walk::walk_call_expression(self, expr); + } + + fn visit_unary_expression(&mut self, expr: &UnaryExpression<'a>) { + match expr.operator { + UnaryOperator::UnaryNegation | + UnaryOperator::UnaryPlus | + UnaryOperator::LogicalNot => { + walk::walk_unary_expression(self, expr); + } + _ => { + self.errors.push(format!("Unsupported unary operator {:?} at {:?}", expr.operator, expr.span)); + } + } + } + + fn visit_binary_expression(&mut self, expr: &BinaryExpression<'a>) { + match expr.operator { + BinaryOperator::Addition | + BinaryOperator::Subtraction | + BinaryOperator::Multiplication | + BinaryOperator::Division | + BinaryOperator::Equality | + BinaryOperator::Inequality | + BinaryOperator::LessThan | + BinaryOperator::GreaterThan | + BinaryOperator::LessEqualThan | + BinaryOperator::GreaterEqualThan => { + walk::walk_binary_expression(self, expr); + } + _ => { + self.errors.push(format!("Unsupported binary operator {:?} at {:?}", expr.operator, expr.span)); + } + } + } + + fn visit_logical_expression(&mut self, expr: &LogicalExpression<'a>) { + match expr.operator { + LogicalOperator::And | + LogicalOperator::Or => { + walk::walk_logical_expression(self, expr); + } + _ => { + self.errors.push(format!("Unsupported logical operator {:?} at {:?}", expr.operator, expr.span)); + } + } + } + + /// Validates that only supported statements are used. + fn visit_statement(&mut self, stmt: &Statement<'a>) { + match stmt { + Statement::VariableDeclaration(_) | + Statement::ExpressionStatement(_) | + Statement::IfStatement(_) | + Statement::BlockStatement(_) | + Statement::ExportNamedDeclaration(_) | + Statement::ImportDeclaration(_) | + Statement::FunctionDeclaration(_) | + Statement::ReturnStatement(_) => { + // These are the only statements the PVM handles currently. + walk::walk_statement(self, stmt); + } + _ => { + self.errors.push(format!("Unsupported statement type at {:?}. Note: Prometeu does not support while/for loops or classes yet.", stmt.span())); + } + } + } +} diff --git a/crates/prometeu-compiler/src/ir/instr.rs b/crates/prometeu-compiler/src/ir/instr.rs new file mode 100644 index 00000000..acd24067 --- /dev/null +++ b/crates/prometeu-compiler/src/ir/instr.rs @@ -0,0 +1,78 @@ +use crate::common::spans::Span; + +#[derive(Debug, Clone)] +pub struct Instruction { + pub kind: InstrKind, + pub span: Option, +} + +impl Instruction { + pub fn new(kind: InstrKind, span: Option) -> Self { + Self { kind, span } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Label(pub String); + +#[derive(Debug, Clone)] +pub enum InstrKind { + Nop, + Halt, + + // Literals + PushInt(i64), + PushFloat(f64), + PushBool(bool), + PushString(String), + PushNull, + + // Stack Ops + Pop, + Dup, + Swap, + + // Arithmetic + Add, + Sub, + Mul, + Div, + Neg, + + // Logical/Comparison + Eq, + Neq, + Lt, + Gt, + Lte, + Gte, + And, + Or, + Not, + + // Bitwise + BitAnd, + BitOr, + BitXor, + Shl, + Shr, + + // Variables + GetLocal(u32), + SetLocal(u32), + GetGlobal(u32), + SetGlobal(u32), + + // Control Flow + Jmp(Label), + JmpIfFalse(Label), + Label(Label), + Call { name: String, arg_count: u32 }, + Ret, + + // OS / System + Syscall(String), + FrameSync, + PushScope, + PopScope, +} diff --git a/crates/prometeu-compiler/src/ir/mod.rs b/crates/prometeu-compiler/src/ir/mod.rs new file mode 100644 index 00000000..cb73bc68 --- /dev/null +++ b/crates/prometeu-compiler/src/ir/mod.rs @@ -0,0 +1,8 @@ +pub mod types; +pub mod module; +pub mod instr; +pub mod validate; + +pub use instr::Instruction; +pub use module::Module; +pub use types::Type; diff --git a/crates/prometeu-compiler/src/ir/module.rs b/crates/prometeu-compiler/src/ir/module.rs new file mode 100644 index 00000000..ac038980 --- /dev/null +++ b/crates/prometeu-compiler/src/ir/module.rs @@ -0,0 +1,40 @@ +use crate::ir::instr::Instruction; +use crate::ir::types::Type; + +#[derive(Debug, Clone)] +pub struct Module { + pub name: String, + pub functions: Vec, + pub globals: Vec, +} + +#[derive(Debug, Clone)] +pub struct Function { + pub name: String, + pub params: Vec, + pub return_type: Type, + pub body: Vec, +} + +#[derive(Debug, Clone)] +pub struct Param { + pub name: String, + pub r#type: Type, +} + +#[derive(Debug, Clone)] +pub struct Global { + pub name: String, + pub r#type: Type, + pub slot: u32, +} + +impl Module { + pub fn new(name: String) -> Self { + Self { + name, + functions: Vec::new(), + globals: Vec::new(), + } + } +} diff --git a/crates/prometeu-compiler/src/ir/types.rs b/crates/prometeu-compiler/src/ir/types.rs new file mode 100644 index 00000000..4eecdf6e --- /dev/null +++ b/crates/prometeu-compiler/src/ir/types.rs @@ -0,0 +1,14 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Type { + Any, + Null, + Bool, + Int, + Float, + String, + Color, + Array(Box), + Object, + Function, + Void, +} diff --git a/crates/prometeu-compiler/src/ir/validate.rs b/crates/prometeu-compiler/src/ir/validate.rs new file mode 100644 index 00000000..4a52dfa3 --- /dev/null +++ b/crates/prometeu-compiler/src/ir/validate.rs @@ -0,0 +1,10 @@ +use crate::common::diagnostics::DiagnosticBundle; +use crate::ir::module::Module; + +pub fn validate_module(_module: &Module) -> Result<(), DiagnosticBundle> { + // TODO: Implement common IR validations: + // - Type checking rules + // - Syscall signatures + // - VM invariants + Ok(()) +} diff --git a/crates/prometeu-compiler/src/lib.rs b/crates/prometeu-compiler/src/lib.rs index 8167207b..75a82391 100644 --- a/crates/prometeu-compiler/src/lib.rs +++ b/crates/prometeu-compiler/src/lib.rs @@ -15,7 +15,10 @@ //! prometeu-compiler build ./my-game --entry ./src/main.ts --out ./game.pbc //! ``` -pub mod codegen; +pub mod common; +pub mod ir; +pub mod backend; +pub mod frontends; pub mod compiler; use anyhow::Result;