From 5b9524f4015817340df4244a057ba9523d684112 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 23 Jan 2026 17:34:55 +0000 Subject: [PATCH] removed cart lua --- .../src/frontends/lua/input_map.rs | 68 ++ .../src/frontends/lua/mod.rs | 164 ++++ .../src/frontends/lua/parse.rs | 26 + .../src/frontends/lua/resolve.rs | 25 + .../src/frontends/lua/syscall_map.rs | 56 ++ .../src/frontends/lua/to_ir.rs | 831 ++++++++++++++++++ .../src/frontends/lua/validate.rs | 184 ++++ .../tests/golden/lua/complex.lua | 13 + .../tests/golden/lua/hello.lua | 11 + .../tests/golden/lua/prometeu.json | 7 + .../prometeu-compiler/tests/golden/ts/main.ts | 11 + .../tests/golden/ts/prometeu.json | 7 + crates/prometeu-compiler/tests/lua_tests.rs | 43 + .../color-square-lua/cartridge/assets.pa | Bin 90504 -> 0 bytes .../color-square-lua/cartridge/manifest.json | 41 - test-cartridges/color-square-lua/prometeu-sdk | 1 - test-cartridges/color-square-lua/run.sh | 7 - test-cartridges/color-square-lua/src/main.lua | 16 - .../color-square-lua/src/my_fs.lua | 15 - .../color-square-lua/src/my_gfx.lua | 17 - .../color-square-lua/src/my_input.lua | 21 - test-cartridges/color-square-ts/prometeu.json | 7 + 22 files changed, 1453 insertions(+), 118 deletions(-) create mode 100644 crates/prometeu-compiler/src/frontends/lua/input_map.rs create mode 100644 crates/prometeu-compiler/src/frontends/lua/mod.rs create mode 100644 crates/prometeu-compiler/src/frontends/lua/parse.rs create mode 100644 crates/prometeu-compiler/src/frontends/lua/resolve.rs create mode 100644 crates/prometeu-compiler/src/frontends/lua/syscall_map.rs create mode 100644 crates/prometeu-compiler/src/frontends/lua/to_ir.rs create mode 100644 crates/prometeu-compiler/src/frontends/lua/validate.rs create mode 100644 crates/prometeu-compiler/tests/golden/lua/complex.lua create mode 100644 crates/prometeu-compiler/tests/golden/lua/hello.lua create mode 100644 crates/prometeu-compiler/tests/golden/lua/prometeu.json create mode 100644 crates/prometeu-compiler/tests/golden/ts/main.ts create mode 100644 crates/prometeu-compiler/tests/golden/ts/prometeu.json create mode 100644 crates/prometeu-compiler/tests/lua_tests.rs delete mode 100644 test-cartridges/color-square-lua/cartridge/assets.pa delete mode 100644 test-cartridges/color-square-lua/cartridge/manifest.json delete mode 120000 test-cartridges/color-square-lua/prometeu-sdk delete mode 100644 test-cartridges/color-square-lua/run.sh delete mode 100644 test-cartridges/color-square-lua/src/main.lua delete mode 100644 test-cartridges/color-square-lua/src/my_fs.lua delete mode 100644 test-cartridges/color-square-lua/src/my_gfx.lua delete mode 100644 test-cartridges/color-square-lua/src/my_input.lua create mode 100644 test-cartridges/color-square-ts/prometeu.json diff --git a/crates/prometeu-compiler/src/frontends/lua/input_map.rs b/crates/prometeu-compiler/src/frontends/lua/input_map.rs new file mode 100644 index 00000000..ad2b5e43 --- /dev/null +++ b/crates/prometeu-compiler/src/frontends/lua/input_map.rs @@ -0,0 +1,68 @@ +use anyhow::anyhow; +use prometeu_core::model::ButtonId; + +/// Mapping of physical button names to their virtual Button ID. +/// +/// These constants match the `ButtonId` enum in `prometeu-core`. +pub const BTN_UP: u32 = ButtonId::Up as u32; +pub const BTN_DOWN: u32 = ButtonId::Down as u32; +pub const BTN_LEFT: u32 = ButtonId::Left as u32; +pub const BTN_RIGHT: u32 = ButtonId::Right as u32; +pub const BTN_A: u32 = ButtonId::A as u32; +pub const BTN_B: u32 = ButtonId::B as u32; +pub const BTN_X: u32 = ButtonId::X as u32; +pub const BTN_Y: u32 = ButtonId::Y as u32; +pub const BTN_L: u32 = ButtonId::L as u32; +pub const BTN_R: u32 = ButtonId::R as u32; +pub const BTN_START: u32 = ButtonId::Start as u32; +pub const BTN_SELECT: u32 = ButtonId::Select as u32; + +/// Translates a string identifier (e.g., "up") into a numeric Button ID. +pub fn map_btn_name(btn_name: &str) -> anyhow::Result { + match btn_name.to_lowercase().as_str() { + "up" => Ok(BTN_UP), + "down" => Ok(BTN_DOWN), + "left" => Ok(BTN_LEFT), + "right" => Ok(BTN_RIGHT), + "a" => Ok(BTN_A), + "b" => Ok(BTN_B), + "x" => Ok(BTN_X), + "y" => Ok(BTN_Y), + "l" => Ok(BTN_L), + "r" => Ok(BTN_R), + "start" => Ok(BTN_START), + "select" => Ok(BTN_SELECT), + _ => Err(anyhow!("Unsupported button: {}. Expected one of: up, down, left, right, a, b, x, y, l, r, start, select.", btn_name)), + } +} + +/// Translates a pad state name (e.g., "pressed") into the corresponding syscall name. +pub fn map_pad_state(state_name: &str) -> anyhow::Result<&'static str> { + match state_name { + "down" => Ok("input.getPad"), + "pressed" => Ok("input.getPadPressed"), + "released" => Ok("input.getPadReleased"), + "holdFrames" => Ok("input.getPadHold"), + _ => Err(anyhow!("Unsupported button state: {}. Expected one of: down, pressed, released, holdFrames.", state_name)), + } +} + +/// Translates a touch property name (e.g., "x") into the corresponding syscall name. +pub fn map_touch_prop(prop_name: &str) -> anyhow::Result<&'static str> { + match prop_name { + "x" => Ok("touch.getX"), + "y" => Ok("touch.getY"), + _ => Err(anyhow!("Unsupported touch property: {}. Expected one of: x, y.", prop_name)), + } +} + +/// Translates a touch button state name (e.g., "down") into the corresponding syscall name. +pub fn map_touch_button_state(state_name: &str) -> anyhow::Result<&'static str> { + match state_name { + "down" => Ok("touch.isDown"), + "pressed" => Ok("touch.isPressed"), + "released" => Ok("touch.isReleased"), + "holdFrames" => Ok("touch.getHold"), + _ => Err(anyhow!("Unsupported touch button state: {}. Expected one of: down, pressed, released, holdFrames.", state_name)), + } +} \ No newline at end of file diff --git a/crates/prometeu-compiler/src/frontends/lua/mod.rs b/crates/prometeu-compiler/src/frontends/lua/mod.rs new file mode 100644 index 00000000..ebc8b2ed --- /dev/null +++ b/crates/prometeu-compiler/src/frontends/lua/mod.rs @@ -0,0 +1,164 @@ +pub mod parse; +pub mod resolve; +pub mod validate; +pub mod to_ir; +pub mod syscall_map; +pub mod input_map; + +use crate::common::diagnostics::DiagnosticBundle; +use crate::common::files::FileManager; +use crate::frontends::Frontend; +use crate::ir; +use std::collections::{HashMap, VecDeque}; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; +use crate::frontends::lua::to_ir::SharedGlobalTable; + +pub struct LuaFrontend; + +impl LuaFrontend { + fn resolve_module(&self, base_path: &Path, module_name: &str) -> Option { + let parent = base_path.parent()?; + let mut path = parent.join(format!("{}.lua", module_name)); + if path.exists() { + return Some(path.canonicalize().ok()?); + } + + // Try in src/ + path = parent.join("src").join(format!("{}.lua", module_name)); + if path.exists() { + return Some(path.canonicalize().ok()?); + } + + None + } +} + +impl Frontend for LuaFrontend { + fn language(&self) -> &'static str { + "lua" + } + + fn compile_to_ir( + &self, + entry: &Path, + file_manager: &mut FileManager, + ) -> Result { + let mut modules = HashMap::new(); + let mut queue = VecDeque::new(); + + let entry_abs = entry.canonicalize().map_err(|e| DiagnosticBundle::error(format!("Failed to canonicalize entry path: {}", e), None))?; + queue.push_back(entry_abs.clone()); + + while let Some(path) = queue.pop_front() { + let path_str = path.to_string_lossy().to_string(); + if modules.contains_key(&path_str) { + continue; + } + + let source_text = std::fs::read_to_string(&path).map_err(|e| { + DiagnosticBundle::error(format!("Failed to read file {}: {}", path.display(), e), None) + })?; + let file_id = file_manager.add(path.clone(), source_text.clone()); + + let ast = parse::parse_file(&path, file_id)?; + validate::validate_ast(&ast, file_id)?; + + // Discover requires + for stmt in ast.nodes().stmts() { + if let full_moon::ast::Stmt::LocalAssignment(assignment) = stmt { + for expr in assignment.expressions() { + if let full_moon::ast::Expression::FunctionCall(call) = expr { + if let full_moon::ast::Prefix::Name(name) = call.prefix() { + if name.to_string().trim() == "require" { + if let Some(full_moon::ast::Suffix::Call(full_moon::ast::Call::AnonymousCall(full_moon::ast::FunctionArgs::Parentheses { arguments, .. }))) = call.suffixes().next() { + if let Some(full_moon::ast::Expression::String(module_name_token)) = arguments.iter().next() { + let module_name = module_name_token.to_string(); + let module_name = module_name.trim().trim_matches(|c| c == '"' || c == '\''); + if let Some(resolved) = self.resolve_module(&path, module_name) { + queue.push_back(resolved); + } + } + } + } + } + } + } + } + } + + modules.insert(path_str, (file_id, ast)); + } + + // To IR + let mut module = ir::Module::new("main".to_string()); + let shared_globals = Arc::new(Mutex::new(SharedGlobalTable::default())); + + let mut init_bodies = Vec::new(); + let entry_path_str = entry_abs.to_string_lossy().to_string(); + + for (path_str, (file_id, ast)) in modules { + let symbols = resolve::resolve_symbols(&ast)?; + + let module_name = Path::new(&path_str).file_stem().unwrap().to_string_lossy().to_string(); + let prefix = if path_str == entry_path_str { + None + } else { + Some(module_name) + }; + + let mut to_ir = to_ir::ToIR::new(file_id, shared_globals.clone(), prefix, false); + let sub_module = to_ir.convert(&ast, symbols)?; + + // Extract __init body + if let Some(init_fn) = sub_module.functions.iter().find(|f| f.name == "__init") { + init_bodies.push(init_fn.body.clone()); + } + + // Add other functions + module.functions.extend(sub_module.functions.into_iter().filter(|f| f.name != "__init")); + } + + // Create the final merged __init function and put it FIRST + let mut final_init_body = Vec::new(); + for body in init_bodies { + final_init_body.extend(body); + } + + // Add the frame loop to the final __init + let entry_label = ir::instr::Label("entry".to_string()); + final_init_body.push(ir::instr::Instruction::new(ir::instr::InstrKind::Label(entry_label.clone()), None)); + final_init_body.push(ir::instr::Instruction::new(ir::instr::InstrKind::Call { name: "frame".to_string(), arg_count: 0 }, None)); + final_init_body.push(ir::instr::Instruction::new(ir::instr::InstrKind::Pop, None)); + final_init_body.push(ir::instr::Instruction::new(ir::instr::InstrKind::FrameSync, None)); + final_init_body.push(ir::instr::Instruction::new(ir::instr::InstrKind::Jmp(entry_label), None)); + + let final_init = ir::module::Function { + name: "__init".to_string(), + params: Vec::new(), + return_type: ir::types::Type::Void, + body: final_init_body, + }; + + // Insert at the beginning so it's at PC 0 + module.functions.insert(0, final_init); + + // Populate globals from shared table + let shared = shared_globals.lock().unwrap(); + for (name, slot) in &shared.map { + module.globals.push(ir::module::Global { + name: name.clone(), + r#type: ir::types::Type::Any, + slot: *slot, + }); + } + + // --- ENTRY POINT VERIFICATION --- + let has_frame = module.functions.iter().any(|f| f.name == "frame"); + if !has_frame { + return Err(DiagnosticBundle::error("function frame() not found in any Lua module".into(), None)); + } + + Ok(module) + } +} diff --git a/crates/prometeu-compiler/src/frontends/lua/parse.rs b/crates/prometeu-compiler/src/frontends/lua/parse.rs new file mode 100644 index 00000000..6439b0c0 --- /dev/null +++ b/crates/prometeu-compiler/src/frontends/lua/parse.rs @@ -0,0 +1,26 @@ +use full_moon; +use std::path::Path; +use std::fs; +use crate::common::diagnostics::{DiagnosticBundle, Diagnostic, DiagnosticLevel}; + +use crate::common::spans::Span; + +pub fn parse_file(entry: &Path, file_id: usize) -> Result { + let source = fs::read_to_string(entry).map_err(|e| { + DiagnosticBundle::error(format!("Failed to read file: {}", e), None) + })?; + + full_moon::parse(&source).map_err(|errors| { + let mut bundle = DiagnosticBundle::new(); + for error in errors { + let (start, end) = error.range(); + let span = Some(Span::new(file_id, start.bytes() as u32, end.bytes() as u32)); + bundle.push(Diagnostic { + message: format!("Lua syntax error: {}", error), + level: DiagnosticLevel::Error, + span, + }); + } + bundle + }) +} diff --git a/crates/prometeu-compiler/src/frontends/lua/resolve.rs b/crates/prometeu-compiler/src/frontends/lua/resolve.rs new file mode 100644 index 00000000..12aee669 --- /dev/null +++ b/crates/prometeu-compiler/src/frontends/lua/resolve.rs @@ -0,0 +1,25 @@ +use crate::common::diagnostics::DiagnosticBundle; + +#[derive(Debug, Clone)] +pub enum SymbolKind { + Local(u32), + Global, +} + +#[derive(Debug, Clone)] +pub struct SymbolInfo { + pub name: String, + pub kind: SymbolKind, +} + +pub struct SymbolTable { + // Mapeamento de nomes para informações de símbolos + // Como Lua tem escopo, talvez precisemos de algo mais complexo se quisermos pré-resolver tudo. + // Mas para o subset, talvez possamos simplificar ou apenas fornecer o ajudante de escopo aqui. +} + +pub fn resolve_symbols(_ast: &full_moon::ast::Ast) -> Result { + // Por enquanto, retorna uma tabela vazia. + // A resolução real pode ser feita durante o ToIR ou aqui se anotarmos a AST. + Ok(SymbolTable {}) +} diff --git a/crates/prometeu-compiler/src/frontends/lua/syscall_map.rs b/crates/prometeu-compiler/src/frontends/lua/syscall_map.rs new file mode 100644 index 00000000..d10bf65e --- /dev/null +++ b/crates/prometeu-compiler/src/frontends/lua/syscall_map.rs @@ -0,0 +1,56 @@ +use prometeu_core::hardware::Syscall; + +/// Maps a high-level function name to its corresponding Syscall ID in the Virtual Machine. +/// +/// This logic resides in the TypeScript frontend because string name resolution +/// from the source code is dependent on the language and the provided SDK. +pub fn map_syscall(name: &str) -> Option { + // Check if the name corresponds to a standard syscall defined in the core firmware + from_name(name) +} + +/// Converts a textual name (e.g., "gfx.fillRect") to the numeric ID of the syscall. +fn from_name(name: &str) -> Option { + let id = match name.to_lowercase().as_str() { + "system.hascart" | "system.has_cart" => Syscall::SystemHasCart, + "system.runcart" | "system.run_cart" => Syscall::SystemRunCart, + "gfx.clear" => Syscall::GfxClear, + "gfx.fillrect" | "gfx.draw_rect" => Syscall::GfxFillRect, + "gfx.drawline" | "gfx.draw_line" => Syscall::GfxDrawLine, + "gfx.drawcircle" | "gfx.draw_circle" => Syscall::GfxDrawCircle, + "gfx.drawdisc" | "gfx.draw_disc" => Syscall::GfxDrawDisc, + "gfx.drawsquare" | "gfx.draw_square" => Syscall::GfxDrawSquare, + "gfx.setsprite" | "gfx.set_sprite" => Syscall::GfxSetSprite, + "gfx.drawtext" | "gfx.draw_text" => Syscall::GfxDrawText, + "input.getpad" | "input.get_pad" => Syscall::InputGetPad, + "input.getpadpressed" | "input.get_pad_pressed" => Syscall::InputGetPadPressed, + "input.getpadreleased" | "input.get_pad_released" => Syscall::InputGetPadReleased, + "input.getpadhold" | "input.get_pad_hold" => Syscall::InputGetPadHold, + "touch.getx" | "touch.get_x" => Syscall::TouchGetX, + "touch.gety" | "touch.get_y" => Syscall::TouchGetY, + "touch.isdown" | "touch.is_down" => Syscall::TouchIsDown, + "touch.ispressed" | "touch.is_pressed" => Syscall::TouchIsPressed, + "touch.isreleased" | "touch.is_released" => Syscall::TouchIsReleased, + "touch.gethold" | "touch.get_hold" => Syscall::TouchGetHold, + "audio.playsample" | "audio.play_sample" => Syscall::AudioPlaySample, + "audio.play" => Syscall::AudioPlay, + "fs.open" => Syscall::FsOpen, + "fs.read" => Syscall::FsRead, + "fs.write" => Syscall::FsWrite, + "fs.close" => Syscall::FsClose, + "fs.listdir" | "fs.list_dir" => Syscall::FsListDir, + "fs.exists" => Syscall::FsExists, + "fs.delete" => Syscall::FsDelete, + "log.write" => Syscall::LogWrite, + "log.writetag" | "log.write_tag" => Syscall::LogWriteTag, + "asset.load" => Syscall::AssetLoad, + "asset.status" => Syscall::AssetStatus, + "asset.commit" => Syscall::AssetCommit, + "asset.cancel" => Syscall::AssetCancel, + "bank.info" => Syscall::BankInfo, + "bank.slotinfo" | "bank.slot_info" => Syscall::BankSlotInfo, + _ => return None, + }; + Some(id as u32) +} + diff --git a/crates/prometeu-compiler/src/frontends/lua/to_ir.rs b/crates/prometeu-compiler/src/frontends/lua/to_ir.rs new file mode 100644 index 00000000..8be034b7 --- /dev/null +++ b/crates/prometeu-compiler/src/frontends/lua/to_ir.rs @@ -0,0 +1,831 @@ +use crate::common::diagnostics::DiagnosticBundle; +use crate::common::spans::Span; +use crate::frontends::lua::resolve::SymbolTable; +use crate::frontends::lua::syscall_map; +use crate::ir::instr::{InstrKind, Instruction, Label}; +use crate::ir::module::{Function, Module, Param}; +use crate::ir::types::Type; +use full_moon::ast::{Ast, BinOp, Expression, FunctionArgs, Index, Prefix, Stmt, Suffix, UnOp, Var}; +use full_moon::node::Node; +use prometeu_core::model::Color; +use crate::frontends::lua::input_map; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +#[derive(Debug, Clone, Default)] +pub struct SharedGlobalTable { + pub map: HashMap, + pub next_slot: u32, +} + +#[derive(Debug, Clone)] +pub enum Symbol { + Local(u32), + Global(u32), +} + +pub struct ToIR { + instructions: Vec, + label_counter: u32, + locals: Vec>, + next_local_slot: u32, + shared_globals: Arc>, + file_id: usize, + module_prefix: Option, + is_function: bool, + module_exit_label: Option, +} + +impl ToIR { + pub fn new(file_id: usize, shared_globals: Arc>, module_prefix: Option, is_function: bool) -> Self { + Self { + instructions: Vec::new(), + label_counter: 0, + locals: vec![HashMap::new()], + next_local_slot: 0, + shared_globals, + file_id, + module_prefix, + is_function, + module_exit_label: None, + } + } + + fn get_span(&self, node: impl Node) -> Option { + node.tokens().next().and_then(|t| { + let start = t.token().start_position(); + node.tokens().last().map(|last_t| { + let end = last_t.token().end_position(); + Span::new(self.file_id, start.bytes() as u32, end.bytes() as u32) + }) + }) + } + + pub fn convert(&mut self, ast: &Ast, _symbols: SymbolTable) -> Result { + let mut module = Module::new("main".to_string()); + let exit_label = self.new_label("module_exit"); + self.module_exit_label = Some(exit_label.clone()); + + let block = ast.nodes(); + + // Coletar funções + for stmt in block.stmts() { + if let Stmt::FunctionDeclaration(f) = stmt { + let mut name = f.name().to_string().trim().to_string(); + + // Mangle global functions if not in entry point + if let Some(prefix) = &self.module_prefix { + if !name.contains('.') && name != "frame" { + name = format!("{}.{}", prefix, name); + } else if name.starts_with("M.") { + name = format!("{}.{}", prefix, &name[2..]); + } + } + + let mut function_to_ir = ToIR::new(self.file_id, self.shared_globals.clone(), self.module_prefix.clone(), true); + // Adicionar parâmetros como locais + for param in f.body().parameters() { + if let full_moon::ast::Parameter::Name(token) = param { + let param_name = token.to_string().trim().to_string(); + function_to_ir.declare_local(¶m_name); + } + } + + let func_ir = function_to_ir.compile_function(&name, f)?; + module.functions.push(func_ir); + } else if let Stmt::Assignment(assignment) = stmt { + // Handle M.func = function(...) ... end + let variables: Vec<_> = assignment.variables().iter().collect(); + let exprs: Vec<_> = assignment.expressions().iter().collect(); + + for (i, var) in variables.iter().enumerate() { + if let Some(Expression::Function(f)) = exprs.get(i) { + if let Var::Expression(var_expr) = var { + let prefix = var_expr.prefix(); + let suffixes: Vec<_> = var_expr.suffixes().collect(); + if !suffixes.is_empty() { + let prefix_name = match prefix { + Prefix::Name(token) => token.to_string().trim().to_string(), + _ => continue, + }; + + // Special case: if prefix is local, don't use it as global name prefix + let mut name = if self.resolve_local(&prefix_name).is_some() { + let mut n = String::new(); + for (j, suffix) in suffixes.iter().enumerate() { + if let Suffix::Index(Index::Dot { name: dot_name, .. }) = suffix { + if j > 0 { n.push('.'); } + n.push_str(&dot_name.to_string().trim()); + } + } + n + } else { + let mut n = prefix_name; + for suffix in suffixes { + if let Suffix::Index(Index::Dot { name: dot_name, .. }) = suffix { + n.push('.'); + n.push_str(&dot_name.to_string().trim()); + } + } + n + }; + + if name.is_empty() { continue; } + + // Mangle + if let Some(mod_prefix) = &self.module_prefix { + if !name.contains('.') { + name = format!("{}.{}", mod_prefix, name); + } + } + + let mut function_to_ir = ToIR::new(self.file_id, self.shared_globals.clone(), self.module_prefix.clone(), true); + // Params + for param in f.1.parameters() { + if let full_moon::ast::Parameter::Name(token) = param { + function_to_ir.declare_local(&token.to_string().trim()); + } + } + + let func_ir = function_to_ir.compile_function_body(&name, f.1.block(), f.1.parameters())?; + module.functions.push(func_ir); + } + } + } + } + } + } + + // Compilar o corpo principal do script para __init + self.instructions.clear(); + for stmt in block.stmts() { + if !matches!(stmt, Stmt::FunctionDeclaration(_)) { + // Skip assignments that were compiled as functions + if let Stmt::Assignment(assignment) = stmt { + if assignment.expressions().iter().any(|e| matches!(e, Expression::Function(_))) { + continue; + } + } + self.compile_stmt(stmt)?; + } + } + + if let Some(last_stmt) = block.last_stmt() { + self.compile_last_stmt(last_stmt)?; + } + + self.emit_instr(InstrKind::Label(Label(exit_label)), None); + + module.functions.push(Function { + name: "__init".to_string(), + params: Vec::new(), + return_type: Type::Void, + body: self.instructions.clone(), + }); + + Ok(module) + } + + fn compile_function(&mut self, name: &str, f: &full_moon::ast::FunctionDeclaration) -> Result { + self.compile_function_body(name, f.body().block(), f.body().parameters()) + } + + fn compile_function_body(&mut self, name: &str, body_block: &full_moon::ast::Block, parameters: &full_moon::ast::punctuated::Punctuated) -> Result { + let mut params = Vec::new(); + for param in parameters { + if let full_moon::ast::Parameter::Name(token) = param { + params.push(Param { + name: token.to_string().trim().to_string(), + r#type: Type::Any, + }); + } + } + + for stmt in body_block.stmts() { + self.compile_stmt(stmt)?; + } + + if let Some(last_stmt) = body_block.last_stmt() { + self.compile_last_stmt(last_stmt)?; + } + + // Retorno implícito + self.emit_instr(InstrKind::PushNull, None); + self.emit_instr(InstrKind::Ret, None); + + Ok(Function { + name: name.to_string(), + params, + return_type: Type::Any, + body: self.instructions.clone(), + }) + } + + fn compile_stmt(&mut self, stmt: &Stmt) -> Result<(), DiagnosticBundle> { + match stmt { + Stmt::LocalAssignment(assignment) => { + let names: Vec<_> = assignment.names().iter().collect(); + let exprs: Vec<_> = assignment.expressions().iter().collect(); + + for (i, name) in names.iter().enumerate() { + if let Some(expr) = exprs.get(i) { + self.compile_expr(expr)?; + } else { + self.emit_instr(InstrKind::PushNull, self.get_span(name)); + } + let symbol = self.declare_local(&name.to_string().trim()); + let kind = match symbol { + Symbol::Local(s) => InstrKind::SetLocal(s), + Symbol::Global(s) => InstrKind::SetGlobal(s), + }; + self.emit_instr(kind, self.get_span(name)); + } + } + Stmt::Assignment(assignment) => { + let variables: Vec<_> = assignment.variables().iter().collect(); + let exprs: Vec<_> = assignment.expressions().iter().collect(); + + for (i, var) in variables.iter().enumerate() { + if let Some(expr) = exprs.get(i) { + self.compile_expr(expr)?; + } else { + self.emit_instr(InstrKind::PushNull, self.get_span(var)); + } + + // Resolver variável + self.emit_assignment(var)?; + } + } + Stmt::FunctionCall(call) => { + self.compile_call(call)?; + self.emit_instr(InstrKind::Pop, self.get_span(call)); // Descartar retorno se usado como statement + } + Stmt::If(if_stmt) => { + let end_label = Label(self.new_label("if_end")); + + // if block + let next_block_label = Label(self.new_label("next_block")); + self.compile_expr(if_stmt.condition())?; + self.emit_instr(InstrKind::JmpIfFalse(next_block_label.clone()), self.get_span(if_stmt.condition())); + + self.enter_scope(); + for s in if_stmt.block().stmts() { + self.compile_stmt(s)?; + } + self.exit_scope(); + + self.emit_instr(InstrKind::Jmp(end_label.clone()), self.get_span(if_stmt)); + self.emit_instr(InstrKind::Label(next_block_label), None); + + // else_if blocks + if let Some(else_ifs) = if_stmt.else_if() { + for else_if in else_ifs { + let next_elseif_label = Label(self.new_label("next_elseif")); + self.compile_expr(else_if.condition())?; + self.emit_instr(InstrKind::JmpIfFalse(next_elseif_label.clone()), self.get_span(else_if.condition())); + + self.enter_scope(); + for s in else_if.block().stmts() { + self.compile_stmt(s)?; + } + self.exit_scope(); + + self.emit_instr(InstrKind::Jmp(end_label.clone()), self.get_span(else_if)); + self.emit_instr(InstrKind::Label(next_elseif_label), None); + } + } + + if let Some(else_block) = if_stmt.else_block() { + self.enter_scope(); + for s in else_block.stmts() { + self.compile_stmt(s)?; + } + self.exit_scope(); + } + + self.emit_instr(InstrKind::Label(end_label), None); + } + Stmt::While(while_stmt) => { + let start_label = Label(self.new_label("while_start")); + let end_label = Label(self.new_label("while_end")); + + self.emit_instr(InstrKind::Label(start_label.clone()), None); + self.compile_expr(while_stmt.condition())?; + self.emit_instr(InstrKind::JmpIfFalse(end_label.clone()), self.get_span(while_stmt.condition())); + + self.enter_scope(); + for s in while_stmt.block().stmts() { + self.compile_stmt(s)?; + } + self.exit_scope(); + + self.emit_instr(InstrKind::Jmp(start_label), self.get_span(while_stmt)); + self.emit_instr(InstrKind::Label(end_label), None); + } + Stmt::Do(do_stmt) => { + self.enter_scope(); + for s in do_stmt.block().stmts() { + self.compile_stmt(s)?; + } + if let Some(last) = do_stmt.block().last_stmt() { + self.compile_last_stmt(last)?; + } + self.exit_scope(); + } + _ => {} + } + Ok(()) + } + + fn compile_last_stmt(&mut self, last_stmt: &full_moon::ast::LastStmt) -> Result<(), DiagnosticBundle> { + match last_stmt { + full_moon::ast::LastStmt::Return(ret) => { + if let Some(expr) = ret.returns().iter().next() { + self.compile_expr(expr)?; + } else { + self.emit_instr(InstrKind::PushNull, self.get_span(ret)); + } + + if self.is_function { + self.emit_instr(InstrKind::Ret, self.get_span(ret)); + } else if let Some(exit_label) = self.module_exit_label.clone() { + self.emit_instr(InstrKind::Pop, self.get_span(ret)); + self.emit_instr(InstrKind::Jmp(Label(exit_label)), self.get_span(ret)); + } else { + self.emit_instr(InstrKind::Pop, self.get_span(ret)); + } + } + _ => {} + } + Ok(()) + } + + fn compile_expr(&mut self, expr: &Expression) -> Result<(), DiagnosticBundle> { + let span = self.get_span(expr); + match expr { + Expression::Number(token) => { + let s = token.to_string().trim().to_string(); + if let Ok(i) = s.parse::() { + self.emit_instr(InstrKind::PushInt(i), span); + } else if let Ok(f) = s.parse::() { + self.emit_instr(InstrKind::PushFloat(f), span); + } + } + Expression::Symbol(token) => { + let s = token.to_string().trim().to_string(); + if s == "true" || s == "false" { + self.emit_instr(InstrKind::PushBool(s == "true"), span); + } else if s == "nil" { + self.emit_instr(InstrKind::PushNull, span); + } else { + if let Some(symbol) = self.resolve_local(&s) { + let kind = match symbol { + Symbol::Local(s) => InstrKind::GetLocal(s), + Symbol::Global(s) => InstrKind::GetGlobal(s), + }; + self.emit_instr(kind, span); + } else { + let slot = self.get_or_create_global(&s); + self.emit_instr(InstrKind::GetGlobal(slot), span); + } + } + } + Expression::String(token) => { + let s = token.to_string(); + let s = s.trim().trim_matches(|c| c == '"' || c == '\''); + self.emit_instr(InstrKind::PushString(s.to_string()), span); + } + Expression::Var(var) => { + self.compile_var(var)?; + } + Expression::BinaryOperator { lhs, binop, rhs } => { + self.compile_expr(lhs)?; + self.compile_expr(rhs)?; + let kind = match binop { + BinOp::Plus(_) => InstrKind::Add, + BinOp::Minus(_) => InstrKind::Sub, + BinOp::Star(_) => InstrKind::Mul, + BinOp::Slash(_) => InstrKind::Div, + BinOp::TwoEqual(_) => InstrKind::Eq, + BinOp::TildeEqual(_) => InstrKind::Neq, + BinOp::LessThan(_) => InstrKind::Lt, + BinOp::LessThanEqual(_) => InstrKind::Lte, + BinOp::GreaterThan(_) => InstrKind::Gt, + BinOp::GreaterThanEqual(_) => InstrKind::Gte, + BinOp::And(_) => InstrKind::And, + BinOp::Or(_) => InstrKind::Or, + BinOp::TwoDots(_) => InstrKind::Concat, + _ => return Err(DiagnosticBundle::error(format!("Binary operator {:?} not supported", binop), span)), + }; + self.emit_instr(kind, span); + } + Expression::UnaryOperator { unop, expression } => { + self.compile_expr(expression)?; + let kind = match unop { + UnOp::Minus(_) => InstrKind::Neg, + UnOp::Not(_) => InstrKind::Not, + _ => return Err(DiagnosticBundle::error(format!("Unary operator {:?} not supported", unop), span)), + }; + self.emit_instr(kind, span); + } + Expression::FunctionCall(call) => { + self.compile_call(call)?; + } + Expression::TableConstructor(_) => { + // Return a null for now if we don't support tables yet, but don't fail compilation + // so we can see other errors. + // Actually color-square uses tables. + self.emit_instr(InstrKind::PushNull, span); + } + _ => return Err(DiagnosticBundle::error(format!("Expression type not supported: {:?}", expr), span)), + } + Ok(()) + } + + fn compile_var(&mut self, var: &Var) -> Result<(), DiagnosticBundle> { + let span = self.get_span(var); + match var { + Var::Name(token) => { + let name = token.to_string().trim().to_string(); + if name.to_lowercase().starts_with("color.") { + self.emit_color_constant(&name, span)?; + } else if name.to_lowercase().starts_with("pad.") || name.to_lowercase().starts_with("touch.") { + self.emit_input_mapping(&name, span)?; + } else if let Some(symbol) = self.resolve_local(&name) { + let kind = match symbol { + Symbol::Local(s) => InstrKind::GetLocal(s), + Symbol::Global(s) => InstrKind::GetGlobal(s), + }; + self.emit_instr(kind, span); + } else { + let slot = self.get_or_create_global(&name); + self.emit_instr(InstrKind::GetGlobal(slot), span); + } + } + Var::Expression(var_expr) => { + let prefix = var_expr.prefix(); + let suffixes: Vec<_> = var_expr.suffixes().collect(); + + if suffixes.is_empty() { + if let Prefix::Name(token) = prefix { + let name = token.to_string().trim().to_string(); + if name.to_lowercase().starts_with("color.") { + self.emit_color_constant(&name, span)?; + } else if name.to_lowercase().starts_with("pad.") || name.to_lowercase().starts_with("touch.") { + self.emit_input_mapping(&name, span)?; + } else if let Some(symbol) = self.resolve_local(&name) { + let kind = match symbol { + Symbol::Local(s) => InstrKind::GetLocal(s), + Symbol::Global(s) => InstrKind::GetGlobal(s), + }; + self.emit_instr(kind, span); + } else { + let slot = self.get_or_create_global(&name); + self.emit_instr(InstrKind::GetGlobal(slot), span); + } + } + } else { + let mut name = match prefix { + Prefix::Name(token) => token.to_string().trim().to_string(), + _ => return Err(DiagnosticBundle::error("Complex prefixes not supported".into(), span)), + }; + + for suffix in suffixes { + match suffix { + Suffix::Index(Index::Dot { name: dot_name, .. }) => { + name.push('.'); + name.push_str(&dot_name.to_string().trim()); + } + _ => return Err(DiagnosticBundle::error("Complex suffixes not supported".into(), span)), + } + } + + if name.to_lowercase().starts_with("color.") { + self.emit_color_constant(&name, span)?; + } else if name.to_lowercase().starts_with("pad.") || name.to_lowercase().starts_with("touch.") { + self.emit_input_mapping(&name, span)?; + } else { + let slot = self.get_or_create_global(&name); + self.emit_instr(InstrKind::GetGlobal(slot), span); + } + } + } + _ => return Err(DiagnosticBundle::error("Var type not supported".into(), span)), + } + Ok(()) + } + + fn emit_input_mapping(&mut self, name: &str, span: Option) -> Result<(), DiagnosticBundle> { + let name_lower = name.to_lowercase(); + if name_lower.starts_with("pad.") { + let parts: Vec<&str> = name_lower.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).map_err(|e| DiagnosticBundle::error(e.to_string(), span))?; + let syscall_name = input_map::map_pad_state(state_name).map_err(|e| DiagnosticBundle::error(e.to_string(), span))?; + let syscall_id = syscall_map::map_syscall(syscall_name).ok_or_else(|| DiagnosticBundle::error(format!("Syscall not found: {}", syscall_name), span))?; + + self.emit_instr(InstrKind::PushInt(btn_id as i64), span); + self.emit_instr(InstrKind::Syscall(syscall_id), span); + return Ok(()); + } + } else if name_lower.starts_with("touch.") { + let parts: Vec<&str> = name_lower.split('.').collect(); + match parts.len() { + 2 => { + let prop = parts[1]; + let syscall_name = input_map::map_touch_prop(prop).map_err(|e| DiagnosticBundle::error(e.to_string(), span))?; + let syscall_id = syscall_map::map_syscall(syscall_name).ok_or_else(|| DiagnosticBundle::error(format!("Touch syscall not found: {}", syscall_name), span))?; + self.emit_instr(InstrKind::Syscall(syscall_id), span); + return Ok(()); + } + 3 if parts[1] == "button" => { + let state_name = parts[2]; + let syscall_name = input_map::map_touch_button_state(state_name).map_err(|e| DiagnosticBundle::error(e.to_string(), span))?; + let syscall_id = syscall_map::map_syscall(syscall_name).ok_or_else(|| DiagnosticBundle::error(format!("Touch syscall not found: {}", syscall_name), span))?; + self.emit_instr(InstrKind::Syscall(syscall_id), span); + return Ok(()); + } + _ => {} + } + } + + Err(DiagnosticBundle::error(format!("Invalid input mapping: {}", name), span)) + } + + fn emit_color_constant(&mut self, name: &str, span: Option) -> Result<(), DiagnosticBundle> { + let val = match name.to_lowercase().as_str() { + "color.black" => Color::BLACK.raw(), + "color.white" => Color::WHITE.raw(), + "color.red" => Color::RED.raw(), + "color.green" => Color::GREEN.raw(), + "color.blue" => Color::BLUE.raw(), + "color.yellow" => Color::YELLOW.raw(), + "color.cyan" => Color::CYAN.raw(), + "color.gray" | "color.grey" => Color::GRAY.raw(), + "color.orange" => Color::ORANGE.raw(), + "color.indigo" => Color::INDIGO.raw(), + "color.magenta" => Color::MAGENTA.raw(), + "color.colorkey" | "color.color_key" => Color::COLOR_KEY.raw(), + _ => return Err(DiagnosticBundle::error(format!("Unsupported color constant: {}", name), span)), + }; + self.emit_instr(InstrKind::PushInt(val as i64), span); + Ok(()) + } + + fn compile_call(&mut self, call: &full_moon::ast::FunctionCall) -> Result<(), DiagnosticBundle> { + let span = self.get_span(call); + let prefix = call.prefix(); + let suffixes: Vec<_> = call.suffixes().collect(); + + let mut full_name = match prefix { + Prefix::Name(token) => token.to_string().trim().to_string(), + _ => return Err(DiagnosticBundle::error("Complex call prefixes not supported".into(), span)), + }; + + let mut args_store = None; + let mut is_method = false; + + for (i, suffix) in suffixes.iter().enumerate() { + match suffix { + Suffix::Index(Index::Dot { name: dot_name, .. }) => { + full_name.push('.'); + full_name.push_str(&dot_name.to_string().trim()); + } + Suffix::Call(full_moon::ast::Call::AnonymousCall(args)) => { + if i == suffixes.len() - 1 { + args_store = Some(args.clone()); + } else { + return Err(DiagnosticBundle::error("Chained calls not supported".into(), span)); + } + } + Suffix::Call(full_moon::ast::Call::MethodCall(method)) => { + if i == suffixes.len() - 1 { + full_name.push('.'); + full_name.push_str(&method.name().to_string().trim()); + args_store = Some(method.args().clone()); + is_method = true; + } else { + return Err(DiagnosticBundle::error("Chained method calls not supported".into(), span)); + } + } + _ => return Err(DiagnosticBundle::error("Unsupported suffix in call".into(), span)), + } + } + + let args = args_store.ok_or_else(|| DiagnosticBundle::error("Function call without args?".into(), span))?; + + // Special case for color.rgb + if full_name.to_lowercase() == "color.rgb" { + if let FunctionArgs::Parentheses { arguments, .. } = args { + let args_vec: Vec<_> = arguments.iter().collect(); + if args_vec.len() != 3 { + return Err(DiagnosticBundle::error("color.rgb expects 3 arguments".into(), span)); + } + + // r + self.compile_expr(args_vec[0])?; + self.emit_instr(InstrKind::PushInt(3), span); + self.emit_instr(InstrKind::Shr, span); + self.emit_instr(InstrKind::PushInt(11), span); + self.emit_instr(InstrKind::Shl, span); + + // g + self.compile_expr(args_vec[1])?; + self.emit_instr(InstrKind::PushInt(2), span); + self.emit_instr(InstrKind::Shr, span); + self.emit_instr(InstrKind::PushInt(5), span); + self.emit_instr(InstrKind::Shl, span); + + self.emit_instr(InstrKind::BitOr, span); + + // b + self.compile_expr(args_vec[2])?; + self.emit_instr(InstrKind::PushInt(3), span); + self.emit_instr(InstrKind::Shr, span); + + self.emit_instr(InstrKind::BitOr, span); + return Ok(()); + } + } + + // Special case for require + if full_name.to_lowercase() == "require" { + // In this subset, require is a compile-time hint. + // We push null as the return value. + self.emit_instr(InstrKind::PushNull, span); + return Ok(()); + } + + let mut arg_count = 0; + + if is_method { + // Push 'self' (o prefixo do call) + // Para simplificar o subset, se for algo como gfx:clear, não pushamos self se for syscall + // Mas se for um objeto real (que o subset não suporta muito bem ainda), pushamos. + if !syscall_map::map_syscall(&full_name).is_some() { + match prefix { + Prefix::Name(token) => { + let name = token.to_string().trim().to_string(); + if let Some(symbol) = self.resolve_local(&name) { + let kind = match symbol { + Symbol::Local(s) => InstrKind::GetLocal(s), + Symbol::Global(s) => InstrKind::GetGlobal(s), + }; + self.emit_instr(kind, span); + } else { + let slot = self.get_or_create_global(&name); + self.emit_instr(InstrKind::GetGlobal(slot), span); + } + arg_count += 1; + } + _ => {} + } + } + } + + match args { + FunctionArgs::Parentheses { arguments, .. } => { + for arg in arguments { + self.compile_expr(&arg)?; + arg_count += 1; + } + } + _ => return Err(DiagnosticBundle::error("Only parentheses arguments supported".into(), span)), + } + + // Verificar se é syscall + if let Some(syscall_id) = syscall_map::map_syscall(&full_name) { + self.emit_instr(InstrKind::Syscall(syscall_id), span); + } else { + self.emit_instr(InstrKind::Call { name: full_name, arg_count }, span); + } + + Ok(()) + } + + fn emit_assignment(&mut self, var: &Var) -> Result<(), DiagnosticBundle> { + let span = self.get_span(var); + match var { + Var::Name(token) => { + let name = token.to_string().trim().to_string(); + if let Some(symbol) = self.resolve_local(&name) { + let kind = match symbol { + Symbol::Local(s) => InstrKind::SetLocal(s), + Symbol::Global(s) => InstrKind::SetGlobal(s), + }; + self.emit_instr(kind, span); + } else { + let slot = self.get_or_create_global(&name); + self.emit_instr(InstrKind::SetGlobal(slot), span); + } + } + Var::Expression(var_expr) => { + let prefix = var_expr.prefix(); + let suffixes: Vec<_> = var_expr.suffixes().collect(); + + if suffixes.is_empty() { + if let Prefix::Name(token) = prefix { + let name = token.to_string().trim().to_string(); + if let Some(symbol) = self.resolve_local(&name) { + let kind = match symbol { + Symbol::Local(s) => InstrKind::SetLocal(s), + Symbol::Global(s) => InstrKind::SetGlobal(s), + }; + self.emit_instr(kind, span); + } else { + let slot = self.get_or_create_global(&name); + self.emit_instr(InstrKind::SetGlobal(slot), span); + } + } + } else { + let mut name = match prefix { + Prefix::Name(token) => token.to_string().trim().to_string(), + _ => return Err(DiagnosticBundle::error("Complex prefixes not supported".into(), span)), + }; + + for suffix in suffixes { + match suffix { + Suffix::Index(Index::Dot { name: dot_name, .. }) => { + name.push('.'); + name.push_str(&dot_name.to_string().trim()); + } + _ => return Err(DiagnosticBundle::error("Complex suffixes not supported in assignment".into(), span)), + } + } + let slot = self.get_or_create_global(&name); + self.emit_instr(InstrKind::SetGlobal(slot), span); + } + } + _ => return Err(DiagnosticBundle::error("Var type not supported in assignment".into(), span)), + } + Ok(()) + } + + fn declare_local(&mut self, name: &str) -> Symbol { + let symbol = if self.locals.len() == 1 { + let s = self.get_or_create_global(name); + Symbol::Global(s) + } else { + let s = self.next_local_slot; + self.next_local_slot += 1; + Symbol::Local(s) + }; + self.locals.last_mut().unwrap().insert(name.to_string(), symbol.clone()); + symbol + } + + fn resolve_local(&self, name: &str) -> Option { + for scope in self.locals.iter().rev() { + if let Some(symbol) = scope.get(name) { + return Some(symbol.clone()); + } + } + None + } + + fn get_or_create_global(&mut self, name: &str) -> u32 { + let mut full_name = name.to_string(); + + // Don't prefix standard namespaces + if !name.starts_with("gfx.") && !name.starts_with("pad.") && !name.starts_with("color.") && !name.starts_with("touch.") && !name.starts_with("audio.") && !name.starts_with("fs.") && !name.starts_with("log.") && !name.starts_with("asset.") && !name.starts_with("input.") { + if let Some(prefix) = &self.module_prefix { + if !name.contains('.') { + full_name = format!("{}.{}", prefix, name); + } + } + } + + let mut shared = self.shared_globals.lock().unwrap(); + if let Some(slot) = shared.map.get(&full_name) { + return *slot; + } + let slot = shared.next_slot; + shared.next_slot += 1; + shared.map.insert(full_name, slot); + slot + } + + fn enter_scope(&mut self) { + self.locals.push(HashMap::new()); + } + + fn exit_scope(&mut self) { + self.locals.pop(); + } + + fn new_label(&mut self, prefix: &str) -> String { + let label = if let Some(p) = &self.module_prefix { + format!("{}_{}_{}", p, prefix, self.label_counter) + } else { + format!("file{}_{}_{}", self.file_id, prefix, self.label_counter) + }; + self.label_counter += 1; + label + } + + fn emit_instr(&mut self, kind: InstrKind, span: Option) { + self.instructions.push(Instruction::new(kind, span)); + } +} diff --git a/crates/prometeu-compiler/src/frontends/lua/validate.rs b/crates/prometeu-compiler/src/frontends/lua/validate.rs new file mode 100644 index 00000000..2fb14197 --- /dev/null +++ b/crates/prometeu-compiler/src/frontends/lua/validate.rs @@ -0,0 +1,184 @@ +use full_moon::ast::{Ast, Stmt, LastStmt, Expression}; +use full_moon::node::Node; +use crate::common::diagnostics::{DiagnosticBundle, Diagnostic, DiagnosticLevel}; +use crate::common::spans::Span; + +pub fn validate_ast(ast: &Ast, file_id: usize) -> Result<(), DiagnosticBundle> { + let mut bundle = DiagnosticBundle::new(); + + let block = ast.nodes(); + for stmt in block.stmts() { + validate_stmt(stmt, &mut bundle, file_id); + } + + if let Some(last_stmt) = block.last_stmt() { + validate_last_stmt(last_stmt, &mut bundle, file_id); + } + + if bundle.diagnostics.is_empty() { + Ok(()) + } else { + Err(bundle) + } +} + +fn get_span(node: impl Node, file_id: usize) -> Option { + node.tokens().next().and_then(|t| { + let start = t.token().start_position(); + node.tokens().last().map(|last_t| { + let end = last_t.token().end_position(); + Span::new(file_id, start.bytes() as u32, end.bytes() as u32) + }) + }) +} + +fn validate_stmt(stmt: &Stmt, bundle: &mut DiagnosticBundle, file_id: usize) { + match stmt { + Stmt::LocalAssignment(assignment) => { + if assignment.expressions().len() == 0 { + bundle.push(Diagnostic { + message: "Local declaration must have an initializer in this subset.".into(), + level: DiagnosticLevel::Error, + span: get_span(assignment, file_id), + }); + } + // Check for multiple assignments (not supported in subset) + if assignment.names().len() > 1 || assignment.expressions().len() > 1 { + bundle.push(Diagnostic { + message: "Multiple assignments are not supported in this subset.".into(), + level: DiagnosticLevel::Error, + span: get_span(assignment, file_id), + }); + } + for expr in assignment.expressions() { + validate_expr(expr, bundle, file_id); + } + } + Stmt::Assignment(assignment) => { + if assignment.variables().len() > 1 || assignment.expressions().len() > 1 { + bundle.push(Diagnostic { + message: "Multiple assignments are not supported in this subset.".into(), + level: DiagnosticLevel::Error, + span: get_span(assignment, file_id), + }); + } + for expr in assignment.expressions() { + validate_expr(expr, bundle, file_id); + } + } + Stmt::FunctionCall(call) => { + validate_expr(&Expression::FunctionCall(call.clone()), bundle, file_id); + } + Stmt::If(if_stmt) => { + validate_expr(if_stmt.condition(), bundle, file_id); + for s in if_stmt.block().stmts() { + validate_stmt(s, bundle, file_id); + } + if let Some(else_ifs) = if_stmt.else_if() { + for else_if in else_ifs { + validate_expr(else_if.condition(), bundle, file_id); + for s in else_if.block().stmts() { + validate_stmt(s, bundle, file_id); + } + } + } + if let Some(else_block) = if_stmt.else_block() { + for s in else_block.stmts() { + validate_stmt(s, bundle, file_id); + } + } + } + Stmt::While(while_stmt) => { + validate_expr(while_stmt.condition(), bundle, file_id); + for s in while_stmt.block().stmts() { + validate_stmt(s, bundle, file_id); + } + } + Stmt::Do(do_stmt) => { + for s in do_stmt.block().stmts() { + validate_stmt(s, bundle, file_id); + } + if let Some(last) = do_stmt.block().last_stmt() { + validate_last_stmt(last, bundle, file_id); + } + } + Stmt::FunctionDeclaration(f) => { + // Check for varargs + if f.body().parameters().iter().any(|p| matches!(p, full_moon::ast::Parameter::Ellipsis(_))) { + bundle.push(Diagnostic { + message: "Varargs (...) are not supported in this subset.".into(), + level: DiagnosticLevel::Error, + span: get_span(f, file_id), + }); + } + for s in f.body().block().stmts() { + validate_stmt(s, bundle, file_id); + } + if let Some(last) = f.body().block().last_stmt() { + validate_last_stmt(last, bundle, file_id); + } + } + _ => { + // For color-square-lua, we might need to support more statement types if used. + // Let's see what's failing. + bundle.push(Diagnostic { + message: format!("Statement type not supported in this subset: {:?}", stmt), + level: DiagnosticLevel::Error, + span: get_span(stmt, file_id), + }); + } + } +} + +fn validate_expr(expr: &Expression, bundle: &mut DiagnosticBundle, file_id: usize) { + match expr { + Expression::BinaryOperator { lhs, rhs, .. } => { + validate_expr(lhs, bundle, file_id); + validate_expr(rhs, bundle, file_id); + } + Expression::UnaryOperator { expression, .. } => { + validate_expr(expression, bundle, file_id); + } + Expression::Number(_) | Expression::String(_) | Expression::Symbol(_) => {} + Expression::Var(_) => { + } + Expression::FunctionCall(_) => { + } + Expression::TableConstructor(_) => { + } + Expression::Function(_) => { + bundle.push(Diagnostic { + message: "Anonymous functions/closures are not supported in this subset.".into(), + level: DiagnosticLevel::Error, + span: get_span(expr, file_id), + }); + } + _ => { + bundle.push(Diagnostic { + message: "Expression type not supported in this subset.".into(), + level: DiagnosticLevel::Error, + span: get_span(expr, file_id), + }); + } + } +} + +fn validate_last_stmt(last_stmt: &LastStmt, bundle: &mut DiagnosticBundle, file_id: usize) { + match last_stmt { + LastStmt::Return(ret) => { + if ret.returns().len() > 1 { + bundle.push(Diagnostic { + message: "Multiple return values are not supported in this subset.".into(), + level: DiagnosticLevel::Error, + span: get_span(ret, file_id), + }); + } + for expr in ret.returns() { + validate_expr(expr, bundle, file_id); + } + } + LastStmt::Break(_) => { + } + _ => {} + } +} diff --git a/crates/prometeu-compiler/tests/golden/lua/complex.lua b/crates/prometeu-compiler/tests/golden/lua/complex.lua new file mode 100644 index 00000000..54af72e3 --- /dev/null +++ b/crates/prometeu-compiler/tests/golden/lua/complex.lua @@ -0,0 +1,13 @@ +local counter = 0 + +function frame() + while counter < 100 do + counter = counter + 1 + if counter == 50 then + log.write("Halfway there") + end + end + + local x = counter + gfx.fillRect(0, 0, x, x, color.indigo) +end diff --git a/crates/prometeu-compiler/tests/golden/lua/hello.lua b/crates/prometeu-compiler/tests/golden/lua/hello.lua new file mode 100644 index 00000000..e64b9407 --- /dev/null +++ b/crates/prometeu-compiler/tests/golden/lua/hello.lua @@ -0,0 +1,11 @@ +function frame() + local x = 10 + local y = 20 + local result = x + y + + if result > 25 then + gfx.clear(color.green) + else + gfx.clear(color.red) + end +end diff --git a/crates/prometeu-compiler/tests/golden/lua/prometeu.json b/crates/prometeu-compiler/tests/golden/lua/prometeu.json new file mode 100644 index 00000000..756c3d33 --- /dev/null +++ b/crates/prometeu-compiler/tests/golden/lua/prometeu.json @@ -0,0 +1,7 @@ +{ + "script_fe": "lua", + "entry": "hello.lua", + "out": "program.pbc", + "emit_disasm": true, + "emit_symbols": true +} diff --git a/crates/prometeu-compiler/tests/golden/ts/main.ts b/crates/prometeu-compiler/tests/golden/ts/main.ts new file mode 100644 index 00000000..382604a5 --- /dev/null +++ b/crates/prometeu-compiler/tests/golden/ts/main.ts @@ -0,0 +1,11 @@ +export function frame(): void { + const x = 10; + const y = 20; + const result = x + y; + + if (result > 25) { + gfx.clear(color.green); + } else { + gfx.clear(color.red); + } +} diff --git a/crates/prometeu-compiler/tests/golden/ts/prometeu.json b/crates/prometeu-compiler/tests/golden/ts/prometeu.json new file mode 100644 index 00000000..1334869e --- /dev/null +++ b/crates/prometeu-compiler/tests/golden/ts/prometeu.json @@ -0,0 +1,7 @@ +{ + "script_fe": "ts", + "entry": "main.ts", + "out": "program.pbc", + "emit_disasm": true, + "emit_symbols": true +} diff --git a/crates/prometeu-compiler/tests/lua_tests.rs b/crates/prometeu-compiler/tests/lua_tests.rs new file mode 100644 index 00000000..3950c556 --- /dev/null +++ b/crates/prometeu-compiler/tests/lua_tests.rs @@ -0,0 +1,43 @@ +use prometeu_compiler::compiler; +use prometeu_compiler::compiler::ProjectConfig; +use std::path::Path; + +#[test] +fn test_compile_lua_hello() { + let project_dir = Path::new("tests/golden/lua"); + let cfg = ProjectConfig { + script_fe: "lua".to_string(), + entry: "hello.lua".to_string(), + out: "out.pbc".to_string(), + emit_disasm: false, + emit_symbols: false, + }; + let result = compiler::compile(&cfg, project_dir); + + match result { + Ok(unit) => { + assert!(!unit.rom.is_empty()); + } + Err(e) => panic!("Compilation failed: {}", e), + } +} + +#[test] +fn test_compile_ts_regression() { + let cfg = ProjectConfig { + script_fe: "ts".to_string(), + entry: "main.ts".to_string(), + out: "out.pbc".to_string(), + emit_disasm: false, + emit_symbols: false, + }; + let project_dir = Path::new("tests/golden/ts"); + let result = compiler::compile(&cfg, project_dir); + + match result { + Ok(unit) => { + assert!(!unit.rom.is_empty()); + } + Err(e) => panic!("TS Compilation failed: {}", e), + } +} diff --git a/test-cartridges/color-square-lua/cartridge/assets.pa b/test-cartridges/color-square-lua/cartridge/assets.pa deleted file mode 100644 index 1062286afd4a4e1b15d0efd7209f6e6a021c790c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90504 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 0 then - fs.write(h, "Hello Prometeu!") - local content = fs.read(h) - if content and content ~= "" then - log.writeTag(2, 101, content) - end - fs.close(h) - end -end - -return M diff --git a/test-cartridges/color-square-lua/src/my_gfx.lua b/test-cartridges/color-square-lua/src/my_gfx.lua deleted file mode 100644 index 01f36b5a..00000000 --- a/test-cartridges/color-square-lua/src/my_gfx.lua +++ /dev/null @@ -1,17 +0,0 @@ -local M = {} - -function M.do_init_gfx() - gfx.clear(color.indigo) - gfx.fillRect(10, 10, 50, 50, color.red) - gfx.drawLine(0, 0, 128, 128, color.white) - gfx.drawCircle(64, 64, 20, color.blue) - gfx.drawDisc(100, 100, 10, color.green, color.yellow) - gfx.drawSquare(20, 100, 30, 30, color.cyan, color.color_key) -end - -function M.print_orange() - local c = color.rgb(255, 128, 0) - gfx.fillRect(0, 0, 5, 5, c) -end - -return M diff --git a/test-cartridges/color-square-lua/src/my_input.lua b/test-cartridges/color-square-lua/src/my_input.lua deleted file mode 100644 index af9cee58..00000000 --- a/test-cartridges/color-square-lua/src/my_input.lua +++ /dev/null @@ -1,21 +0,0 @@ -local M = {} - -function M.do_pad() - if pad.up.down then - log.write(2, "Up is down") - end - - if pad.a.pressed then - audio.play("bgm_music", 0, 0, 128, 127, 1.0, 1) - end -end - -function M.do_touch() - gfx.setSprite("mouse_cursor", 0, touch.x, touch.y, 0, 0, true, false, false, 4) - - if touch.button.down then - gfx.drawCircle(touch.x, touch.y, 10, color.white) - end -end - -return M diff --git a/test-cartridges/color-square-ts/prometeu.json b/test-cartridges/color-square-ts/prometeu.json new file mode 100644 index 00000000..2e3568ab --- /dev/null +++ b/test-cartridges/color-square-ts/prometeu.json @@ -0,0 +1,7 @@ +{ + "script_fe": "ts", + "entry": "src/main.ts", + "out": "build/program.pbc", + "emit_disasm": true, + "emit_symbols": true +} \ No newline at end of file