removed ts and lua
This commit is contained in:
parent
c795eeb3dc
commit
05838d10b0
@ -6,7 +6,6 @@
|
||||
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;
|
||||
@ -61,7 +60,7 @@ pub fn compile(entry: &Path) -> Result<CompilationUnit> {
|
||||
|
||||
// 1. Select Frontend (Currently only TS is supported)
|
||||
// The frontend is responsible for parsing source code and producing the IR.
|
||||
let frontend = TypescriptFrontend;
|
||||
let frontend = /** ??? **/;
|
||||
|
||||
// 2. Compile to IR (Intermediate Representation)
|
||||
// This step abstracts away source-specific syntax (like TypeScript) into a
|
||||
|
||||
@ -1,68 +0,0 @@
|
||||
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<u32> {
|
||||
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)),
|
||||
}
|
||||
}
|
||||
@ -1,164 +0,0 @@
|
||||
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<PathBuf> {
|
||||
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<ir::Module, DiagnosticBundle> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
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<full_moon::ast::Ast, DiagnosticBundle> {
|
||||
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
|
||||
})
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
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<SymbolTable, DiagnosticBundle> {
|
||||
// Por enquanto, retorna uma tabela vazia.
|
||||
// A resolução real pode ser feita durante o ToIR ou aqui se anotarmos a AST.
|
||||
Ok(SymbolTable {})
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
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<u32> {
|
||||
// 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<u32> {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -1,831 +0,0 @@
|
||||
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<String, u32>,
|
||||
pub next_slot: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Symbol {
|
||||
Local(u32),
|
||||
Global(u32),
|
||||
}
|
||||
|
||||
pub struct ToIR {
|
||||
instructions: Vec<Instruction>,
|
||||
label_counter: u32,
|
||||
locals: Vec<HashMap<String, Symbol>>,
|
||||
next_local_slot: u32,
|
||||
shared_globals: Arc<Mutex<SharedGlobalTable>>,
|
||||
file_id: usize,
|
||||
module_prefix: Option<String>,
|
||||
is_function: bool,
|
||||
module_exit_label: Option<String>,
|
||||
}
|
||||
|
||||
impl ToIR {
|
||||
pub fn new(file_id: usize, shared_globals: Arc<Mutex<SharedGlobalTable>>, module_prefix: Option<String>, 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<Span> {
|
||||
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<Module, DiagnosticBundle> {
|
||||
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<Function, DiagnosticBundle> {
|
||||
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<full_moon::ast::Parameter>) -> Result<Function, DiagnosticBundle> {
|
||||
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::<i64>() {
|
||||
self.emit_instr(InstrKind::PushInt(i), span);
|
||||
} else if let Ok(f) = s.parse::<f64>() {
|
||||
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<Span>) -> 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<Span>) -> 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<Symbol> {
|
||||
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<Span>) {
|
||||
self.instructions.push(Instruction::new(kind, span));
|
||||
}
|
||||
}
|
||||
@ -1,184 +0,0 @@
|
||||
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<Span> {
|
||||
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(_) => {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@ -2,8 +2,6 @@ use crate::common::diagnostics::DiagnosticBundle;
|
||||
use crate::ir;
|
||||
use std::path::Path;
|
||||
|
||||
pub mod ts;
|
||||
|
||||
use crate::common::files::FileManager;
|
||||
|
||||
pub trait Frontend {
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use oxc_ast::ast::*;
|
||||
|
||||
/// Extracts the name of the function being called from a CallExpression.
|
||||
pub fn get_callee_name(expr: &Expression) -> Result<String> {
|
||||
get_member_expr_name(expr)
|
||||
}
|
||||
|
||||
/// Recursively extracts a full name from an identifier or a member expression.
|
||||
///
|
||||
/// Example:
|
||||
/// - `foo` -> "foo"
|
||||
/// - `Color.RED` -> "Color.RED"
|
||||
/// - `PGfx.drawRect` -> "Gfx.drawRect" (Stripping 'P' prefix if appropriate)
|
||||
pub fn get_member_expr_name(expr: &Expression) -> Result<String> {
|
||||
match expr {
|
||||
Expression::Identifier(ident) => {
|
||||
let name = ident.name.to_string();
|
||||
// Prometeu SDK uses 'P' prefix for some internal names to avoid conflicts
|
||||
// with standard JS identifiers. We strip it here to match the ISA names.
|
||||
if name.len() > 1 && name.starts_with('P') && name.chars().nth(1).unwrap().is_uppercase() {
|
||||
Ok(name[1..].to_string())
|
||||
} else {
|
||||
Ok(name)
|
||||
}
|
||||
}
|
||||
Expression::StaticMemberExpression(member) => {
|
||||
let obj = get_member_expr_name(&member.object)?;
|
||||
let prop = member.property.name.to_string();
|
||||
Ok(format!("{}.{}", obj, prop))
|
||||
}
|
||||
_ => Err(anyhow!("Unsupported expression for name resolution")),
|
||||
}
|
||||
}
|
||||
@ -1,68 +0,0 @@
|
||||
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<u32> {
|
||||
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)),
|
||||
}
|
||||
}
|
||||
@ -1,112 +0,0 @@
|
||||
//! # TypeScript Frontend
|
||||
//!
|
||||
//! This module implements the TypeScript/JavaScript frontend for the Prometeu Compiler.
|
||||
//! It is responsible for:
|
||||
//! 1. Parsing `.ts` files into an Abstract Syntax Tree (AST).
|
||||
//! 2. Resolving imports and building a dependency graph.
|
||||
//! 3. Validating that the source code uses only supported VM features.
|
||||
//! 4. Lowering the high-level AST into the generic Intermediate Representation (IR).
|
||||
|
||||
pub mod parse;
|
||||
pub mod resolve;
|
||||
pub mod validate;
|
||||
pub mod to_ir;
|
||||
pub mod ast_util;
|
||||
pub mod input_map;
|
||||
pub mod syscall_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;
|
||||
|
||||
/// The main entry point for the TypeScript compiler frontend.
|
||||
pub struct TypescriptFrontend;
|
||||
|
||||
impl Frontend for TypescriptFrontend {
|
||||
fn language(&self) -> &'static str {
|
||||
"typescript"
|
||||
}
|
||||
|
||||
/// Compiles a TypeScript entry file (and all its dependencies) into an IR Module.
|
||||
///
|
||||
/// # The Compilation Pipeline:
|
||||
///
|
||||
/// 1. **Discovery & Parsing**:
|
||||
/// Starting from the `entry` file, it reads, parses, and discovers imports recursively.
|
||||
/// It uses the `oxc` parser for high-performance AST generation.
|
||||
///
|
||||
/// 2. **Semantic Validation**:
|
||||
/// Each parsed module is checked against the `Validator`. Since Prometeu is a
|
||||
/// specialized VM, it doesn't support the full range of TS/JS features (e.g., no `try/catch` yet).
|
||||
///
|
||||
/// 3. **IR Lowering**:
|
||||
/// Once all modules are parsed and validated, the `ToIR` engine traverses the ASTs
|
||||
/// and emits equivalent IR instructions.
|
||||
fn compile_to_ir(
|
||||
&self,
|
||||
entry: &Path,
|
||||
file_manager: &mut crate::common::files::FileManager,
|
||||
) -> Result<ir::Module, DiagnosticBundle> {
|
||||
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 ---
|
||||
// We traverse the dependency graph breadth-first to ensure all files are loaded.
|
||||
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);
|
||||
|
||||
// Parse the source into an AST
|
||||
let program = parse::parse_file(&allocator, &path).map_err(|e| DiagnosticBundle::error(format!("Failed to parse module: {}", e), None))?;
|
||||
|
||||
// --- PHASE 2: Individual Module Validation ---
|
||||
// Ensure the code adheres to Prometeu's restricted subset of TypeScript.
|
||||
validate::Validator::validate(&program).map_err(|e| DiagnosticBundle::error(format!("Validation error: {}", e), None))?;
|
||||
|
||||
// Discover new imports and add them to the queue
|
||||
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 ---
|
||||
// Collect all parsed modules and feed them into the IR generator.
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
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<Program<'a>> {
|
||||
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)
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
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<PathBuf> {
|
||||
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()?)
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
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<u32> {
|
||||
// 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<u32> {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -1,803 +0,0 @@
|
||||
//! # To IR Lowering
|
||||
//!
|
||||
//! This module implements the core translation logic from the TypeScript AST (Abstract Syntax Tree)
|
||||
//! into the Prometeu Intermediate Representation (IR).
|
||||
//!
|
||||
//! ## How it works:
|
||||
//!
|
||||
//! The `ToIR` engine traverses the AST and for each node (expression, statement, etc.),
|
||||
//! it emits equivalent stack-based IR instructions.
|
||||
//!
|
||||
//! ### Symbol Resolution
|
||||
//! It maintains a scoped symbol table to track local variables and their assigned slots.
|
||||
//! It also manages global variables, which are mapped to permanent memory slots in the VM.
|
||||
//!
|
||||
//! ### Control Flow
|
||||
//! High-level control flow (like `if`, `while`, `for`) is lowered into jumps and labels.
|
||||
//! Unique labels are generated for each branch to avoid collisions.
|
||||
//!
|
||||
//! ### Example Translation:
|
||||
//! **TS Source:**
|
||||
//! ```typescript
|
||||
//! let x = 10 + 20;
|
||||
//! ```
|
||||
//! **Generated IR:**
|
||||
//! ```text
|
||||
//! PushInt(10)
|
||||
//! PushInt(20)
|
||||
//! Add
|
||||
//! SetLocal(0)
|
||||
//! ```
|
||||
|
||||
use crate::frontends::ts::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.
|
||||
///
|
||||
/// This is used to determine how many slots need to be reserved on the stack frame
|
||||
/// before executing a function.
|
||||
struct LocalCounter {
|
||||
count: u32,
|
||||
}
|
||||
|
||||
impl<'a> Visit<'a> for LocalCounter {
|
||||
/// Discovers function declarations which also occupy a slot in the current frame.
|
||||
fn visit_statement(&mut self, stmt: &Statement<'a>) {
|
||||
match stmt {
|
||||
Statement::FunctionDeclaration(f) => {
|
||||
self.visit_function(f, ScopeFlags::empty());
|
||||
}
|
||||
_ => walk::walk_statement(self, stmt),
|
||||
}
|
||||
}
|
||||
|
||||
/// Discovers variable declarations (let/const/var) and increments the local count.
|
||||
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.
|
||||
struct SymbolEntry {
|
||||
/// The unique index assigned to this variable in the current frame or global memory.
|
||||
slot_index: u32,
|
||||
/// Whether the variable was declared with `const`.
|
||||
is_const: bool,
|
||||
/// Tracks if the variable has been assigned a value (used for Temporal Dead Zone checks).
|
||||
is_initialized: bool,
|
||||
}
|
||||
|
||||
/// 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<ir::Instruction>,
|
||||
/// Scoped symbol table. Each element is a scope level containing a map of symbols.
|
||||
/// This allows for variable shadowing and block scope resolution.
|
||||
symbol_table: Vec<HashMap<String, SymbolEntry>>,
|
||||
/// 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<String, u32>,
|
||||
/// Counter for the next available local variable ID in the current function.
|
||||
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 {
|
||||
slot_index,
|
||||
is_const,
|
||||
is_initialized,
|
||||
};
|
||||
|
||||
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<ir::Module> {
|
||||
// --- 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<Statement>) -> 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<Statement>) -> 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<Statement>) -> 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)?;
|
||||
let name_lower = name.to_lowercase();
|
||||
|
||||
if name_lower == "color.rgb" {
|
||||
// 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 if name_lower.starts_with("input.btn") || name_lower.starts_with("pinput.btn") {
|
||||
// Special case for legacy Input.btnX() calls
|
||||
// They map to input.getPad(BTN_X)
|
||||
let btn_name = if name_lower.starts_with("pinput.btn") {
|
||||
&name[10..]
|
||||
} else {
|
||||
&name[9..]
|
||||
};
|
||||
|
||||
// Strip parentheses if any (though get_callee_name should handle just the callee)
|
||||
let btn_name = btn_name.strip_suffix("()").unwrap_or(btn_name);
|
||||
|
||||
let btn_id = input_map::map_btn_name(btn_name)?;
|
||||
self.emit_instr(InstrKind::PushInt(btn_id as i64), call.span);
|
||||
let pad_id = syscall_map::map_syscall("input.getPad").ok_or_else(|| anyhow!("input.getPad syscall not found"))?;
|
||||
self.emit_instr(InstrKind::Syscall(pad_id), call.span);
|
||||
|
||||
} else if let Some(syscall_id) = syscall_map::map_syscall(&name) {
|
||||
// Standard System Call
|
||||
for arg in &call.arguments {
|
||||
if let Some(expr) = arg.as_expression() {
|
||||
self.compile_expr(expr)?;
|
||||
}
|
||||
}
|
||||
self.emit_instr(InstrKind::Syscall(syscall_id), 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 = input_map::map_pad_state(state_name)?;
|
||||
let syscall_id = syscall_map::map_syscall(syscall_name).unwrap();
|
||||
self.emit_instr(InstrKind::PushInt(btn_id as i64), member.span);
|
||||
self.emit_instr(InstrKind::Syscall(syscall_id), 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 = input_map::map_touch_prop(prop)?;
|
||||
let syscall_id = syscall_map::map_syscall(syscall_name).unwrap();
|
||||
self.emit_instr(InstrKind::Syscall(syscall_id), member.span);
|
||||
}
|
||||
3 if parts[1] == "button" => {
|
||||
let state_name = parts[2];
|
||||
let syscall_name = input_map::map_touch_button_state(state_name)?;
|
||||
let syscall_id = syscall_map::map_syscall(syscall_name).unwrap();
|
||||
self.emit_instr(InstrKind::Syscall(syscall_id), 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));
|
||||
}
|
||||
}
|
||||
@ -1,183 +0,0 @@
|
||||
use crate::frontends::ts::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<String>,
|
||||
/// Set of function names defined in the project, used to distinguish
|
||||
/// local calls from invalid references.
|
||||
local_functions: std::collections::HashSet<String>,
|
||||
}
|
||||
|
||||
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<String>,
|
||||
}
|
||||
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) {
|
||||
let name_lower = name.to_lowercase();
|
||||
let is_intrinsic = name_lower == "color.rgb" || name_lower.starts_with("input.btn") || name_lower.starts_with("pinput.btn");
|
||||
|
||||
if !is_intrinsic && 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
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
|
||||
@ -1,11 +0,0 @@
|
||||
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
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"script_fe": "lua",
|
||||
"entry": "hello.lua",
|
||||
"out": "program.pbc",
|
||||
"emit_disasm": true,
|
||||
"emit_symbols": true
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"script_fe": "ts",
|
||||
"entry": "main.ts",
|
||||
"out": "program.pbc",
|
||||
"emit_disasm": true,
|
||||
"emit_symbols": true
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user