split compiler into backend and fronted
This commit is contained in:
parent
5caa3e112e
commit
39eb8b2827
63
crates/prometeu-compiler/src/backend/artifacts.rs
Normal file
63
crates/prometeu-compiler/src/backend/artifacts.rs
Normal file
@ -0,0 +1,63 @@
|
||||
use crate::common::symbols::Symbol;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use prometeu_bytecode::disasm::disasm;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
pub struct Artifacts {
|
||||
pub rom: Vec<u8>,
|
||||
pub symbols: Vec<Symbol>,
|
||||
}
|
||||
|
||||
impl Artifacts {
|
||||
pub fn new(rom: Vec<u8>, symbols: Vec<Symbol>) -> Self {
|
||||
Self { rom, symbols }
|
||||
}
|
||||
|
||||
pub fn export(&self, out: &Path, emit_disasm: bool, emit_symbols: bool) -> Result<()> {
|
||||
// 1. Save the main binary
|
||||
fs::write(out, &self.rom).with_context(|| format!("Failed to write PBC to {:?}", out))?;
|
||||
|
||||
// 2. Export symbols for the HostDebugger
|
||||
if emit_symbols {
|
||||
let symbols_path = out.with_file_name("symbols.json");
|
||||
let symbols_json = serde_json::to_string_pretty(&self.symbols)?;
|
||||
fs::write(&symbols_path, symbols_json)?;
|
||||
}
|
||||
|
||||
// 3. Export human-readable disassembly for developer inspection
|
||||
if emit_disasm {
|
||||
let disasm_path = out.with_extension("disasm.txt");
|
||||
|
||||
// Extract the actual bytecode (stripping the PBC header if present)
|
||||
let rom_to_disasm = if let Ok(pbc) = prometeu_bytecode::pbc::parse_pbc(&self.rom) {
|
||||
pbc.rom
|
||||
} else {
|
||||
self.rom.clone()
|
||||
};
|
||||
|
||||
let instructions = disasm(&rom_to_disasm).map_err(|e| anyhow!("Disassembly failed: {}", e))?;
|
||||
|
||||
let mut disasm_text = String::new();
|
||||
for instr in instructions {
|
||||
// Find a matching symbol to show which source line generated this instruction
|
||||
let symbol = self.symbols.iter().find(|s| s.pc == instr.pc);
|
||||
let comment = if let Some(s) = symbol {
|
||||
format!(" ; {}:{}", s.file, s.line)
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let operands_str = instr.operands.iter()
|
||||
.map(|o| format!("{:?}", o))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
disasm_text.push_str(&format!("{:08X} {:?} {}{}\n", instr.pc, instr.opcode, operands_str, comment));
|
||||
}
|
||||
fs::write(disasm_path, disasm_text)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
0
crates/prometeu-compiler/src/backend/disasm.rs
Normal file
0
crates/prometeu-compiler/src/backend/disasm.rs
Normal file
181
crates/prometeu-compiler/src/backend/emit_bytecode.rs
Normal file
181
crates/prometeu-compiler/src/backend/emit_bytecode.rs
Normal file
@ -0,0 +1,181 @@
|
||||
use crate::backend::syscall_registry;
|
||||
use crate::common::files::FileManager;
|
||||
use crate::common::symbols::Symbol;
|
||||
use crate::ir;
|
||||
use crate::ir::instr::InstrKind;
|
||||
use anyhow::{anyhow, Result};
|
||||
use prometeu_bytecode::asm::{assemble, update_pc_by_operand, Asm, Operand};
|
||||
use prometeu_bytecode::opcode::OpCode;
|
||||
use prometeu_bytecode::pbc::{write_pbc, ConstantPoolEntry, PbcFile};
|
||||
|
||||
pub struct EmitResult {
|
||||
pub rom: Vec<u8>,
|
||||
pub symbols: Vec<Symbol>,
|
||||
}
|
||||
|
||||
pub fn emit_module(module: &ir::Module, file_manager: &FileManager) -> Result<EmitResult> {
|
||||
let mut emitter = BytecodeEmitter::new(file_manager);
|
||||
emitter.emit(module)
|
||||
}
|
||||
|
||||
struct BytecodeEmitter<'a> {
|
||||
constant_pool: Vec<ConstantPoolEntry>,
|
||||
file_manager: &'a FileManager,
|
||||
}
|
||||
|
||||
impl<'a> BytecodeEmitter<'a> {
|
||||
fn new(file_manager: &'a FileManager) -> Self {
|
||||
Self {
|
||||
constant_pool: vec![ConstantPoolEntry::Null],
|
||||
file_manager,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_constant(&mut self, entry: ConstantPoolEntry) -> u32 {
|
||||
if let Some(pos) = self.constant_pool.iter().position(|e| e == &entry) {
|
||||
pos as u32
|
||||
} else {
|
||||
let id = self.constant_pool.len() as u32;
|
||||
self.constant_pool.push(entry);
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
fn emit(&mut self, module: &ir::Module) -> Result<EmitResult> {
|
||||
let mut asm_instrs = Vec::new();
|
||||
let mut ir_instr_map = Vec::new(); // Maps Asm index to IR instruction (for symbols)
|
||||
|
||||
for function in &module.functions {
|
||||
asm_instrs.push(Asm::Label(function.name.clone()));
|
||||
ir_instr_map.push(None);
|
||||
|
||||
for instr in &function.body {
|
||||
let start_idx = asm_instrs.len();
|
||||
match &instr.kind {
|
||||
InstrKind::Nop => asm_instrs.push(Asm::Op(OpCode::Nop, vec![])),
|
||||
InstrKind::Halt => asm_instrs.push(Asm::Op(OpCode::Halt, vec![])),
|
||||
InstrKind::PushInt(v) => {
|
||||
asm_instrs.push(Asm::Op(OpCode::PushI64, vec![Operand::I64(*v)]));
|
||||
}
|
||||
InstrKind::PushFloat(v) => {
|
||||
asm_instrs.push(Asm::Op(OpCode::PushF64, vec![Operand::F64(*v)]));
|
||||
}
|
||||
InstrKind::PushBool(v) => {
|
||||
asm_instrs.push(Asm::Op(OpCode::PushBool, vec![Operand::Bool(*v)]));
|
||||
}
|
||||
InstrKind::PushString(s) => {
|
||||
let id = self.add_constant(ConstantPoolEntry::String(s.clone()));
|
||||
asm_instrs.push(Asm::Op(OpCode::PushConst, vec![Operand::U32(id)]));
|
||||
}
|
||||
InstrKind::PushNull => {
|
||||
asm_instrs.push(Asm::Op(OpCode::PushConst, vec![Operand::U32(0)]));
|
||||
}
|
||||
InstrKind::Pop => asm_instrs.push(Asm::Op(OpCode::Pop, vec![])),
|
||||
InstrKind::Dup => asm_instrs.push(Asm::Op(OpCode::Dup, vec![])),
|
||||
InstrKind::Swap => asm_instrs.push(Asm::Op(OpCode::Swap, vec![])),
|
||||
InstrKind::Add => asm_instrs.push(Asm::Op(OpCode::Add, vec![])),
|
||||
InstrKind::Sub => asm_instrs.push(Asm::Op(OpCode::Sub, vec![])),
|
||||
InstrKind::Mul => asm_instrs.push(Asm::Op(OpCode::Mul, vec![])),
|
||||
InstrKind::Div => asm_instrs.push(Asm::Op(OpCode::Div, vec![])),
|
||||
InstrKind::Neg => asm_instrs.push(Asm::Op(OpCode::Neg, vec![])),
|
||||
InstrKind::Eq => asm_instrs.push(Asm::Op(OpCode::Eq, vec![])),
|
||||
InstrKind::Neq => asm_instrs.push(Asm::Op(OpCode::Neq, vec![])),
|
||||
InstrKind::Lt => asm_instrs.push(Asm::Op(OpCode::Lt, vec![])),
|
||||
InstrKind::Gt => asm_instrs.push(Asm::Op(OpCode::Gt, vec![])),
|
||||
InstrKind::Lte => asm_instrs.push(Asm::Op(OpCode::Lte, vec![])),
|
||||
InstrKind::Gte => asm_instrs.push(Asm::Op(OpCode::Gte, vec![])),
|
||||
InstrKind::And => asm_instrs.push(Asm::Op(OpCode::And, vec![])),
|
||||
InstrKind::Or => asm_instrs.push(Asm::Op(OpCode::Or, vec![])),
|
||||
InstrKind::Not => asm_instrs.push(Asm::Op(OpCode::Not, vec![])),
|
||||
InstrKind::BitAnd => asm_instrs.push(Asm::Op(OpCode::BitAnd, vec![])),
|
||||
InstrKind::BitOr => asm_instrs.push(Asm::Op(OpCode::BitOr, vec![])),
|
||||
InstrKind::BitXor => asm_instrs.push(Asm::Op(OpCode::BitXor, vec![])),
|
||||
InstrKind::Shl => asm_instrs.push(Asm::Op(OpCode::Shl, vec![])),
|
||||
InstrKind::Shr => asm_instrs.push(Asm::Op(OpCode::Shr, vec![])),
|
||||
InstrKind::GetLocal(slot) => {
|
||||
asm_instrs.push(Asm::Op(OpCode::GetLocal, vec![Operand::U32(*slot)]));
|
||||
}
|
||||
InstrKind::SetLocal(slot) => {
|
||||
asm_instrs.push(Asm::Op(OpCode::SetLocal, vec![Operand::U32(*slot)]));
|
||||
}
|
||||
InstrKind::GetGlobal(slot) => {
|
||||
asm_instrs.push(Asm::Op(OpCode::GetGlobal, vec![Operand::U32(*slot)]));
|
||||
}
|
||||
InstrKind::SetGlobal(slot) => {
|
||||
asm_instrs.push(Asm::Op(OpCode::SetGlobal, vec![Operand::U32(*slot)]));
|
||||
}
|
||||
InstrKind::Jmp(label) => {
|
||||
asm_instrs.push(Asm::Op(OpCode::Jmp, vec![Operand::Label(label.0.clone())]));
|
||||
}
|
||||
InstrKind::JmpIfFalse(label) => {
|
||||
asm_instrs.push(Asm::Op(OpCode::JmpIfFalse, vec![Operand::Label(label.0.clone())]));
|
||||
}
|
||||
InstrKind::Label(label) => {
|
||||
asm_instrs.push(Asm::Label(label.0.clone()));
|
||||
}
|
||||
InstrKind::Call { name, arg_count } => {
|
||||
asm_instrs.push(Asm::Op(OpCode::Call, vec![Operand::Label(name.clone()), Operand::U32(*arg_count)]));
|
||||
}
|
||||
InstrKind::Ret => asm_instrs.push(Asm::Op(OpCode::Ret, vec![])),
|
||||
InstrKind::Syscall(name) => {
|
||||
if let Some(id) = syscall_registry::map_syscall(name) {
|
||||
asm_instrs.push(Asm::Op(OpCode::Syscall, vec![Operand::U32(id)]));
|
||||
} else {
|
||||
return Err(anyhow!("Unknown syscall: {}", name));
|
||||
}
|
||||
}
|
||||
InstrKind::FrameSync => asm_instrs.push(Asm::Op(OpCode::FrameSync, vec![])),
|
||||
InstrKind::PushScope => asm_instrs.push(Asm::Op(OpCode::PushScope, vec![])),
|
||||
InstrKind::PopScope => asm_instrs.push(Asm::Op(OpCode::PopScope, vec![])),
|
||||
}
|
||||
|
||||
let end_idx = asm_instrs.len();
|
||||
for _ in start_idx..end_idx {
|
||||
ir_instr_map.push(Some(instr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let bytecode = assemble(&asm_instrs).map_err(|e| anyhow!(e))?;
|
||||
|
||||
// Resolve symbols
|
||||
let mut symbols = Vec::new();
|
||||
let mut current_pc = 0u32;
|
||||
for (i, asm) in asm_instrs.iter().enumerate() {
|
||||
if let Some(ir_instr) = ir_instr_map[i] {
|
||||
if let Some(span) = ir_instr.span {
|
||||
let (line, col) = self.file_manager.lookup_pos(span.file_id, span.start);
|
||||
let file_path = self.file_manager.get_path(span.file_id)
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
symbols.push(Symbol {
|
||||
pc: current_pc,
|
||||
file: file_path,
|
||||
line,
|
||||
col,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
match asm {
|
||||
Asm::Label(_) => {}
|
||||
Asm::Op(_opcode, operands) => {
|
||||
current_pc += 2;
|
||||
current_pc = update_pc_by_operand(current_pc, operands);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pbc = PbcFile {
|
||||
cp: self.constant_pool.clone(),
|
||||
rom: bytecode,
|
||||
};
|
||||
|
||||
let out = write_pbc(&pbc).map_err(|e| anyhow!(e))?;
|
||||
Ok(EmitResult {
|
||||
rom: out,
|
||||
symbols,
|
||||
})
|
||||
}
|
||||
}
|
||||
0
crates/prometeu-compiler/src/backend/lowering.rs
Normal file
0
crates/prometeu-compiler/src/backend/lowering.rs
Normal file
7
crates/prometeu-compiler/src/backend/mod.rs
Normal file
7
crates/prometeu-compiler/src/backend/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub mod syscall_registry;
|
||||
pub mod lowering;
|
||||
pub mod emit_bytecode;
|
||||
pub mod disasm;
|
||||
pub mod artifacts;
|
||||
|
||||
pub use emit_bytecode::emit_module;
|
||||
26
crates/prometeu-compiler/src/backend/syscall_registry.rs
Normal file
26
crates/prometeu-compiler/src/backend/syscall_registry.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use prometeu_core::hardware::Syscall;
|
||||
|
||||
/// Maps a High-Level function name to its corresponding Virtual Machine Syscall ID.
|
||||
pub fn map_syscall(name: &str) -> Option<u32> {
|
||||
// Check if the name matches a standard syscall defined in the core firmware
|
||||
if let Some(syscall) = Syscall::from_name(name) {
|
||||
return Some(syscall as u32);
|
||||
}
|
||||
|
||||
// Handle special compiler intrinsics that don't map 1:1 to a single syscall ID.
|
||||
match name {
|
||||
// Color.rgb(r,g,b) is expanded to bitwise logic by the codegen
|
||||
"Color.rgb" | "color.rgb" => Some(0xFFFF_FFFF),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_syscall_name(id: u32) -> Option<String> {
|
||||
if id == 0xFFFF_FFFF {
|
||||
return Some("Color.rgb".to_string());
|
||||
}
|
||||
|
||||
// This is a bit hacky because Syscall is an enum, we might need a better way in prometeu-core
|
||||
// For now, let's just assume we can find it.
|
||||
None // TODO: implement reverse mapping if needed
|
||||
}
|
||||
@ -5,8 +5,8 @@ use anyhow::{anyhow, Result};
|
||||
use oxc_allocator::Vec as OXCVec;
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_ast_visit::{walk, Visit};
|
||||
use oxc_syntax::scope::ScopeFlags;
|
||||
use oxc_span::{GetSpan, Span};
|
||||
use oxc_syntax::scope::ScopeFlags;
|
||||
use prometeu_bytecode::asm;
|
||||
use prometeu_bytecode::asm::{assemble, Asm, Operand};
|
||||
use prometeu_bytecode::opcode::OpCode;
|
||||
|
||||
@ -3,8 +3,8 @@ use crate::codegen::syscall_map;
|
||||
use anyhow::{anyhow, Result};
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_ast_visit::{walk, Visit};
|
||||
use oxc_syntax::scope::ScopeFlags;
|
||||
use oxc_span::GetSpan;
|
||||
use oxc_syntax::scope::ScopeFlags;
|
||||
|
||||
/// AST Visitor that ensures the source code follows the Prometeu subset of JS/TS.
|
||||
///
|
||||
|
||||
55
crates/prometeu-compiler/src/common/diagnostics.rs
Normal file
55
crates/prometeu-compiler/src/common/diagnostics.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use crate::common::spans::Span;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DiagnosticLevel {
|
||||
Error,
|
||||
Warning,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Diagnostic {
|
||||
pub level: DiagnosticLevel,
|
||||
pub message: String,
|
||||
pub span: Option<Span>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DiagnosticBundle {
|
||||
pub diagnostics: Vec<Diagnostic>,
|
||||
}
|
||||
|
||||
impl DiagnosticBundle {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
diagnostics: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, diagnostic: Diagnostic) {
|
||||
self.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
pub fn error(message: String, span: Option<Span>) -> Self {
|
||||
let mut bundle = Self::new();
|
||||
bundle.push(Diagnostic {
|
||||
level: DiagnosticLevel::Error,
|
||||
message,
|
||||
span,
|
||||
});
|
||||
bundle
|
||||
}
|
||||
|
||||
pub fn has_errors(&self) -> bool {
|
||||
self.diagnostics
|
||||
.iter()
|
||||
.any(|d| matches!(d.level, DiagnosticLevel::Error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Diagnostic> for DiagnosticBundle {
|
||||
fn from(diagnostic: Diagnostic) -> Self {
|
||||
let mut bundle = Self::new();
|
||||
bundle.push(diagnostic);
|
||||
bundle
|
||||
}
|
||||
}
|
||||
59
crates/prometeu-compiler/src/common/files.rs
Normal file
59
crates/prometeu-compiler/src/common/files.rs
Normal file
@ -0,0 +1,59 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct SourceFile {
|
||||
pub id: usize,
|
||||
pub path: PathBuf,
|
||||
pub source: Arc<str>,
|
||||
}
|
||||
|
||||
pub struct FileManager {
|
||||
files: Vec<SourceFile>,
|
||||
}
|
||||
|
||||
impl FileManager {
|
||||
pub fn new() -> Self {
|
||||
Self { files: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn add(&mut self, path: PathBuf, source: String) -> usize {
|
||||
let id = self.files.len();
|
||||
self.files.push(SourceFile {
|
||||
id,
|
||||
path,
|
||||
source: Arc::from(source),
|
||||
});
|
||||
id
|
||||
}
|
||||
|
||||
pub fn get_file(&self, id: usize) -> Option<&SourceFile> {
|
||||
self.files.get(id)
|
||||
}
|
||||
|
||||
pub fn get_path(&self, id: usize) -> Option<PathBuf> {
|
||||
self.files.get(id).map(|f| f.path.clone())
|
||||
}
|
||||
|
||||
pub fn lookup_pos(&self, file_id: usize, pos: u32) -> (usize, usize) {
|
||||
let file = if let Some(f) = self.files.get(file_id) {
|
||||
f
|
||||
} else {
|
||||
return (0, 0);
|
||||
};
|
||||
|
||||
let mut line = 1;
|
||||
let mut col = 1;
|
||||
for (i, c) in file.source.char_indices() {
|
||||
if i as u32 == pos {
|
||||
break;
|
||||
}
|
||||
if c == '\n' {
|
||||
line += 1;
|
||||
col = 1;
|
||||
} else {
|
||||
col += 1;
|
||||
}
|
||||
}
|
||||
(line, col)
|
||||
}
|
||||
}
|
||||
5
crates/prometeu-compiler/src/common/mod.rs
Normal file
5
crates/prometeu-compiler/src/common/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub mod diagnostics;
|
||||
pub mod spans;
|
||||
pub mod files;
|
||||
pub mod symbols;
|
||||
pub mod sourcemap;
|
||||
1
crates/prometeu-compiler/src/common/sourcemap.rs
Normal file
1
crates/prometeu-compiler/src/common/sourcemap.rs
Normal file
@ -0,0 +1 @@
|
||||
// PC <-> Span mapping logic
|
||||
16
crates/prometeu-compiler/src/common/spans.rs
Normal file
16
crates/prometeu-compiler/src/common/spans.rs
Normal file
@ -0,0 +1,16 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Span {
|
||||
pub file_id: usize,
|
||||
pub start: u32,
|
||||
pub end: u32,
|
||||
}
|
||||
|
||||
impl Span {
|
||||
pub fn new(file_id: usize, start: u32, end: u32) -> Self {
|
||||
Self {
|
||||
file_id,
|
||||
start,
|
||||
end,
|
||||
}
|
||||
}
|
||||
}
|
||||
9
crates/prometeu-compiler/src/common/symbols.rs
Normal file
9
crates/prometeu-compiler/src/common/symbols.rs
Normal file
@ -0,0 +1,9 @@
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct Symbol {
|
||||
pub pc: u32,
|
||||
pub file: String,
|
||||
pub line: usize,
|
||||
pub col: usize,
|
||||
}
|
||||
@ -1,29 +1,11 @@
|
||||
use crate::codegen::validator::Validator;
|
||||
use crate::codegen::Codegen;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_parser::Parser;
|
||||
use oxc_span::SourceType;
|
||||
use prometeu_bytecode::disasm::disasm;
|
||||
use serde::Serialize;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Represents a debug symbol that maps a Program Counter (PC) to a specific
|
||||
/// source code location.
|
||||
#[derive(Serialize)]
|
||||
pub struct Symbol {
|
||||
/// The absolute address in the bytecode.
|
||||
pub pc: u32,
|
||||
/// The original source file path.
|
||||
pub file: String,
|
||||
/// 1-based line number.
|
||||
pub line: usize,
|
||||
/// 1-based column number.
|
||||
pub col: usize,
|
||||
}
|
||||
use crate::backend;
|
||||
use crate::common::files::FileManager;
|
||||
use crate::common::symbols::Symbol;
|
||||
use crate::frontends::ts::TypescriptFrontend;
|
||||
use crate::frontends::Frontend;
|
||||
use crate::ir;
|
||||
use anyhow::Result;
|
||||
use std::path::Path;
|
||||
|
||||
/// The result of a successful compilation process.
|
||||
/// It contains the final binary and the metadata needed for debugging.
|
||||
@ -36,156 +18,32 @@ pub struct CompilationUnit {
|
||||
|
||||
impl CompilationUnit {
|
||||
/// Writes the compilation results to the disk.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `out` - The target path for the .pbc file.
|
||||
/// * `emit_disasm` - If true, generates a .disasm.txt file next to the output.
|
||||
/// * `emit_symbols` - If true, generates a symbols.json file for debuggers.
|
||||
pub fn export(&self, out: &Path, emit_disasm: bool, emit_symbols: bool) -> Result<()> {
|
||||
// 1. Save the main binary
|
||||
fs::write(out, &self.rom).with_context(|| format!("Failed to write PBC to {:?}", out))?;
|
||||
|
||||
// 2. Export symbols for the HostDebugger
|
||||
if emit_symbols {
|
||||
let symbols_path = out.with_file_name("symbols.json");
|
||||
let symbols_json = serde_json::to_string_pretty(&self.symbols)?;
|
||||
fs::write(&symbols_path, symbols_json)?;
|
||||
}
|
||||
|
||||
// 3. Export human-readable disassembly for developer inspection
|
||||
if emit_disasm {
|
||||
let disasm_path = out.with_extension("disasm.txt");
|
||||
|
||||
// Extract the actual bytecode (stripping the PBC header if present)
|
||||
let rom_to_disasm = if let Ok(pbc) = prometeu_bytecode::pbc::parse_pbc(&self.rom) {
|
||||
pbc.rom
|
||||
} else {
|
||||
self.rom.clone()
|
||||
};
|
||||
|
||||
let instructions = disasm(&rom_to_disasm).map_err(|e| anyhow::anyhow!("Disassembly failed: {}", e))?;
|
||||
|
||||
let mut disasm_text = String::new();
|
||||
for instr in instructions {
|
||||
// Find a matching symbol to show which source line generated this instruction
|
||||
let symbol = self.symbols.iter().find(|s| s.pc == instr.pc);
|
||||
let comment = if let Some(s) = symbol {
|
||||
format!(" ; {}:{}", s.file, s.line)
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let operands_str = instr.operands.iter()
|
||||
.map(|o| format!("{:?}", o))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
disasm_text.push_str(&format!("{:08X} {:?} {}{}\n", instr.pc, instr.opcode, operands_str, comment));
|
||||
}
|
||||
fs::write(disasm_path, disasm_text)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
let artifacts = backend::artifacts::Artifacts::new(self.rom.clone(), self.symbols.clone());
|
||||
artifacts.export(out, emit_disasm, emit_symbols)
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to resolve import paths (e.g., converting './utils' to './utils.ts').
|
||||
fn resolve_import(base_path: &Path, import_str: &str) -> Result<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()?)
|
||||
}
|
||||
|
||||
/// Orchestrates the compilation of a Prometeu project starting from an entry file.
|
||||
///
|
||||
/// This function:
|
||||
/// 1. Crawls the import graph to find all dependencies.
|
||||
/// 2. Parses each file using `oxc`.
|
||||
/// 3. Validates that the code adheres to Prometeu's restricted JS/TS subset.
|
||||
/// 4. Hands over all programs to the `Codegen` for binary generation.
|
||||
pub fn compile(entry: &Path) -> Result<CompilationUnit> {
|
||||
let allocator = Allocator::default();
|
||||
let mut modules = HashMap::new();
|
||||
let mut queue = VecDeque::new();
|
||||
let mut file_manager = FileManager::new();
|
||||
|
||||
// Start with the entry point
|
||||
let entry_abs = entry.canonicalize()
|
||||
.with_context(|| format!("Failed to canonicalize entry path: {:?}", entry))?;
|
||||
queue.push_back(entry_abs.clone());
|
||||
|
||||
// --- PHASE 1: Dependency Resolution and Parsing ---
|
||||
while let Some(path) = queue.pop_front() {
|
||||
let path_str = path.to_string_lossy().to_string();
|
||||
if modules.contains_key(&path_str) {
|
||||
continue; // Already processed
|
||||
}
|
||||
|
||||
let source_text = fs::read_to_string(&path)
|
||||
.with_context(|| format!("Failed to read file: {:?}", path))?;
|
||||
// 1. Select Frontend (Currently only TS is supported)
|
||||
let frontend = TypescriptFrontend;
|
||||
|
||||
// 2. Compile to IR
|
||||
let ir_module = frontend.compile_to_ir(entry, &mut file_manager)
|
||||
.map_err(|bundle| anyhow::anyhow!("Compilation failed with {} errors", bundle.diagnostics.len()))?;
|
||||
|
||||
// We use an Allocator to manage the lifetime of AST nodes and source strings efficiently
|
||||
let source_text_ptr: &str = allocator.alloc_str(&source_text);
|
||||
// 3. IR Validation
|
||||
ir::validate::validate_module(&ir_module)
|
||||
.map_err(|bundle| anyhow::anyhow!("IR Validation failed: {:?}", bundle))?;
|
||||
|
||||
let source_type = SourceType::from_path(&path).unwrap_or_default();
|
||||
let parser_ret = Parser::new(&allocator, source_text_ptr, source_type).parse();
|
||||
|
||||
// Handle syntax errors immediately
|
||||
if !parser_ret.errors.is_empty() {
|
||||
for error in parser_ret.errors {
|
||||
eprintln!("{:?}", error);
|
||||
}
|
||||
return Err(anyhow!("Failed to parse module: {:?}", path));
|
||||
}
|
||||
|
||||
// --- PHASE 2: Individual Module Validation ---
|
||||
// Checks for things like 'class' or 'async' which are not supported by PVM.
|
||||
Validator::validate(&parser_ret.program)?;
|
||||
|
||||
// Discover new imports to crawl
|
||||
for item in &parser_ret.program.body {
|
||||
if let Statement::ImportDeclaration(decl) = item {
|
||||
let import_path = decl.source.value.as_str();
|
||||
let resolved = resolve_import(&path, import_path)?;
|
||||
queue.push_back(resolved);
|
||||
}
|
||||
}
|
||||
|
||||
modules.insert(path_str, (source_text_ptr, parser_ret.program));
|
||||
}
|
||||
|
||||
// --- PHASE 3: Code Generation ---
|
||||
let entry_str = entry_abs.to_string_lossy().to_string();
|
||||
let mut program_list = Vec::new();
|
||||
// 4. Emit Bytecode
|
||||
let result = backend::emit_module(&ir_module, &file_manager)?;
|
||||
|
||||
// Ensure the entry module (containing 'frame()') is processed first
|
||||
let entry_data = modules.get(&entry_str).ok_or_else(|| anyhow!("Entry module not found after loading"))?;
|
||||
program_list.push((entry_str.clone(), entry_data.0.to_string(), &entry_data.1));
|
||||
|
||||
// Collect all other modules
|
||||
for (path, (source, program)) in &modules {
|
||||
if path != &entry_str {
|
||||
program_list.push((path.clone(), source.to_string(), program));
|
||||
}
|
||||
}
|
||||
|
||||
let mut codegen = Codegen::new(entry_str.clone(), entry_data.0.to_string());
|
||||
let rom = codegen.compile_programs(program_list)?;
|
||||
|
||||
Ok(CompilationUnit {
|
||||
rom,
|
||||
symbols: codegen.symbols,
|
||||
rom: result.rom,
|
||||
symbols: result.symbols,
|
||||
})
|
||||
}
|
||||
|
||||
17
crates/prometeu-compiler/src/frontends/mod.rs
Normal file
17
crates/prometeu-compiler/src/frontends/mod.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use crate::common::diagnostics::DiagnosticBundle;
|
||||
use crate::ir;
|
||||
use std::path::Path;
|
||||
|
||||
pub mod ts;
|
||||
|
||||
use crate::common::files::FileManager;
|
||||
|
||||
pub trait Frontend {
|
||||
fn language(&self) -> &'static str;
|
||||
|
||||
fn compile_to_ir(
|
||||
&self,
|
||||
entry: &Path,
|
||||
file_manager: &mut FileManager,
|
||||
) -> Result<ir::Module, DiagnosticBundle>;
|
||||
}
|
||||
82
crates/prometeu-compiler/src/frontends/ts/mod.rs
Normal file
82
crates/prometeu-compiler/src/frontends/ts/mod.rs
Normal file
@ -0,0 +1,82 @@
|
||||
pub mod parse;
|
||||
pub mod resolve;
|
||||
pub mod validate;
|
||||
pub mod to_ir;
|
||||
pub mod ast_util;
|
||||
pub mod input_map;
|
||||
|
||||
use crate::common::diagnostics::DiagnosticBundle;
|
||||
use crate::frontends::Frontend;
|
||||
use crate::ir;
|
||||
use oxc_allocator::Allocator;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
pub struct TypescriptFrontend;
|
||||
|
||||
impl Frontend for TypescriptFrontend {
|
||||
fn language(&self) -> &'static str {
|
||||
"typescript"
|
||||
}
|
||||
|
||||
fn compile_to_ir(
|
||||
&self,
|
||||
entry: &Path,
|
||||
file_manager: &mut crate::common::files::FileManager,
|
||||
) -> Result<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 ---
|
||||
while let Some(path) = queue.pop_front() {
|
||||
let path_str: String = path.to_string_lossy().into_owned();
|
||||
if modules.contains_key(&path_str) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let source_text = fs::read_to_string(&path).map_err(|e| DiagnosticBundle::error(format!("Failed to read file: {}", e), None))?;
|
||||
let file_id = file_manager.add(path.clone(), source_text.clone());
|
||||
let source_text_ptr = allocator.alloc_str(&source_text);
|
||||
|
||||
let program = parse::parse_file(&allocator, &path).map_err(|e| DiagnosticBundle::error(format!("Failed to parse module: {}", e), None))?;
|
||||
|
||||
// --- PHASE 2: Individual Module Validation ---
|
||||
validate::Validator::validate(&program).map_err(|e| DiagnosticBundle::error(format!("Validation error: {}", e), None))?;
|
||||
|
||||
// Discover new imports
|
||||
for item in &program.body {
|
||||
if let oxc_ast::ast::Statement::ImportDeclaration(decl) = item {
|
||||
let import_path = decl.source.value.as_str();
|
||||
let resolved = resolve::resolve_import(&path, import_path).map_err(|e| DiagnosticBundle::error(format!("Resolve error: {}", e), None))?;
|
||||
queue.push_back(resolved);
|
||||
}
|
||||
}
|
||||
|
||||
modules.insert(path_str, (file_id, source_text_ptr, program));
|
||||
}
|
||||
|
||||
// --- PHASE 3: To IR ---
|
||||
let entry_str = entry_abs.to_string_lossy().to_string();
|
||||
let mut program_list = Vec::new();
|
||||
|
||||
let entry_data = modules.get(&entry_str).ok_or_else(|| DiagnosticBundle::error("Entry module not found".into(), None))?;
|
||||
program_list.push((entry_data.0, entry_str.clone(), entry_data.1.to_string(), &entry_data.2));
|
||||
|
||||
for (path, (file_id, source, program)) in &modules {
|
||||
if path != &entry_str {
|
||||
program_list.push((*file_id, path.clone(), source.to_string(), program));
|
||||
}
|
||||
}
|
||||
|
||||
let mut to_ir_engine = to_ir::ToIR::new(entry_str.clone(), entry_data.1.to_string());
|
||||
let module = to_ir_engine.compile_to_ir(program_list).map_err(|e| DiagnosticBundle::error(format!("IR Generation error: {}", e), None))?;
|
||||
|
||||
Ok(module)
|
||||
}
|
||||
}
|
||||
25
crates/prometeu-compiler/src/frontends/ts/parse.rs
Normal file
25
crates/prometeu-compiler/src/frontends/ts/parse.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_ast::ast::Program;
|
||||
use oxc_parser::Parser;
|
||||
use oxc_span::SourceType;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn parse_file<'a>(allocator: &'a Allocator, path: &Path) -> Result<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)
|
||||
}
|
||||
22
crates/prometeu-compiler/src/frontends/ts/resolve.rs
Normal file
22
crates/prometeu-compiler/src/frontends/ts/resolve.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Helper to resolve import paths (e.g., converting './utils' to './utils.ts').
|
||||
pub fn resolve_import(base_path: &Path, import_str: &str) -> Result<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()?)
|
||||
}
|
||||
770
crates/prometeu-compiler/src/frontends/ts/to_ir.rs
Normal file
770
crates/prometeu-compiler/src/frontends/ts/to_ir.rs
Normal file
@ -0,0 +1,770 @@
|
||||
use crate::backend::syscall_registry as syscall_map;
|
||||
use crate::common::spans::Span as IRSpan;
|
||||
use crate::frontends::ts::ast_util;
|
||||
use crate::frontends::ts::input_map;
|
||||
use crate::ir;
|
||||
use crate::ir::instr::{InstrKind, Instruction as IRInstruction, Label as IRLabel};
|
||||
use anyhow::{anyhow, Result};
|
||||
use oxc_allocator::Vec as OXCVec;
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_ast_visit::{walk, Visit};
|
||||
use oxc_span::{GetSpan, Span};
|
||||
use oxc_syntax::scope::ScopeFlags;
|
||||
use prometeu_core::model::Color;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Helper to count local variables and hoisted functions in a function body.
|
||||
struct LocalCounter {
|
||||
count: u32,
|
||||
}
|
||||
|
||||
impl<'a> Visit<'a> for LocalCounter {
|
||||
fn visit_statement(&mut self, stmt: &Statement<'a>) {
|
||||
match stmt {
|
||||
Statement::FunctionDeclaration(f) => {
|
||||
self.visit_function(f, ScopeFlags::empty());
|
||||
}
|
||||
_ => walk::walk_statement(self, stmt),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_variable_declaration(&mut self, decl: &VariableDeclaration<'a>) {
|
||||
self.count += decl.declarations.len() as u32;
|
||||
walk::walk_variable_declaration(self, decl);
|
||||
}
|
||||
|
||||
fn visit_function(&mut self, f: &Function<'a>, _flags: ScopeFlags) {
|
||||
if f.id.is_some() {
|
||||
self.count += 1;
|
||||
}
|
||||
// Stop recursion: nested functions have their own frames and locals.
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata for a symbol (variable or function) in the symbol table.
|
||||
#[allow(dead_code)]
|
||||
struct SymbolEntry {
|
||||
name: String,
|
||||
slot_index: u32,
|
||||
scope_depth: usize,
|
||||
is_const: bool,
|
||||
is_initialized: bool,
|
||||
// PC of the instruction where the symbol was declared/initialized.
|
||||
// Useful for debug symbols and potentially for more advanced TDZ checks.
|
||||
declared_at_pc: u32,
|
||||
initialized_at_pc: u32,
|
||||
}
|
||||
|
||||
/// The TS AST to Prometeu IR translator.
|
||||
pub struct ToIR {
|
||||
/// Name of the file being compiled (used for debug symbols).
|
||||
file_name: String,
|
||||
/// Full source code of the file (used for position lookup).
|
||||
source_text: String,
|
||||
/// ID of the file being compiled.
|
||||
current_file_id: usize,
|
||||
/// The stream of generated IR instructions.
|
||||
pub instructions: Vec<ir::Instruction>,
|
||||
/// Scoped symbol table. Each element is a scope level containing a map of symbols.
|
||||
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.
|
||||
next_local: u32,
|
||||
/// Counter for the next available global variable ID.
|
||||
next_global: u32,
|
||||
/// Counter for generating unique labels (e.g., for 'if' or 'while' blocks).
|
||||
label_count: u32,
|
||||
}
|
||||
|
||||
impl ToIR {
|
||||
/// Creates a new ToIR instance for a specific file.
|
||||
pub fn new(file_name: String, source_text: String) -> Self {
|
||||
Self {
|
||||
file_name,
|
||||
source_text,
|
||||
current_file_id: 0,
|
||||
instructions: Vec::new(),
|
||||
symbol_table: Vec::new(),
|
||||
scope_depth: 0,
|
||||
globals: HashMap::new(),
|
||||
next_local: 0,
|
||||
next_global: 0,
|
||||
label_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Enters a new scope level.
|
||||
fn enter_scope(&mut self) {
|
||||
self.symbol_table.push(HashMap::new());
|
||||
self.scope_depth += 1;
|
||||
}
|
||||
|
||||
/// Exits the current scope level.
|
||||
fn exit_scope(&mut self) {
|
||||
self.symbol_table.pop();
|
||||
self.scope_depth -= 1;
|
||||
}
|
||||
|
||||
/// Declares a new symbol in the current scope.
|
||||
fn declare_symbol(&mut self, name: String, is_const: bool, is_initialized: bool, span: Span) -> Result<&mut SymbolEntry> {
|
||||
let current_scope = self.symbol_table.last_mut().ok_or_else(|| anyhow!("No active scope"))?;
|
||||
|
||||
if current_scope.contains_key(&name) {
|
||||
return Err(anyhow!("Variable '{}' already declared in this scope at {:?}", name, span));
|
||||
}
|
||||
|
||||
let slot_index = self.next_local;
|
||||
self.next_local += 1;
|
||||
|
||||
let entry = SymbolEntry {
|
||||
name: name.clone(),
|
||||
slot_index,
|
||||
scope_depth: self.scope_depth,
|
||||
is_const,
|
||||
is_initialized,
|
||||
declared_at_pc: 0, // Will be filled if needed
|
||||
initialized_at_pc: 0,
|
||||
};
|
||||
|
||||
current_scope.insert(name.clone(), entry);
|
||||
Ok(self.symbol_table.last_mut().unwrap().get_mut(&name).unwrap())
|
||||
}
|
||||
|
||||
/// Marks a symbol as initialized.
|
||||
fn initialize_symbol(&mut self, name: &str) {
|
||||
for scope in self.symbol_table.iter_mut().rev() {
|
||||
if let Some(entry) = scope.get_mut(name) {
|
||||
entry.is_initialized = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves a symbol name to its entry, searching from inner to outer scopes.
|
||||
fn resolve_symbol(&self, name: &str) -> Option<&SymbolEntry> {
|
||||
for scope in self.symbol_table.iter().rev() {
|
||||
if let Some(entry) = scope.get(name) {
|
||||
return Some(entry);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Discovers all function declarations in a program, including nested ones.
|
||||
fn discover_functions<'a>(
|
||||
&self,
|
||||
file_id: usize,
|
||||
file: String,
|
||||
source: String,
|
||||
program: &'a Program<'a>,
|
||||
all_functions: &mut Vec<(usize, String, String, &'a Function<'a>)>,
|
||||
) {
|
||||
struct Collector<'a, 'b> {
|
||||
file_id: usize,
|
||||
file: String,
|
||||
source: String,
|
||||
functions: &'b mut Vec<(usize, String, String, &'a Function<'a>)>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> Visit<'a> for Collector<'a, 'b> {
|
||||
fn visit_function(&mut self, f: &Function<'a>, flags: ScopeFlags) {
|
||||
// Safety: The program AST lives long enough as it's owned by the caller
|
||||
// of compile_programs and outlives the compilation process.
|
||||
let f_ref = unsafe { std::mem::transmute::<&Function<'a>, &'a Function<'a>>(f) };
|
||||
self.functions.push((self.file_id, self.file.clone(), self.source.clone(), f_ref));
|
||||
walk::walk_function(self, f, flags);
|
||||
}
|
||||
}
|
||||
|
||||
let mut collector = Collector {
|
||||
file_id,
|
||||
file,
|
||||
source,
|
||||
functions: all_functions,
|
||||
};
|
||||
collector.visit_program(program);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Compiles multiple programs (files) into a single Prometeu IR Module.
|
||||
pub fn compile_to_ir(&mut self, programs: Vec<(usize, String, String, &Program)>) -> Result<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)?;
|
||||
if let Some(syscall_id) = syscall_map::map_syscall(&name) {
|
||||
if syscall_id == 0xFFFF_FFFF {
|
||||
// Special case for Color.rgb(r, g, b)
|
||||
// It's compiled to a sequence of bitwise operations for performance
|
||||
if call.arguments.len() != 3 {
|
||||
return Err(anyhow!("Color.rgb expects 3 arguments at {:?}", call.span));
|
||||
}
|
||||
|
||||
// Argument 0: r (shift right 3, shift left 11)
|
||||
if let Some(expr) = call.arguments[0].as_expression() {
|
||||
self.compile_expr(expr)?;
|
||||
self.emit_instr(InstrKind::PushInt(3), call.span);
|
||||
self.emit_instr(InstrKind::Shr, call.span);
|
||||
self.emit_instr(InstrKind::PushInt(11), call.span);
|
||||
self.emit_instr(InstrKind::Shl, call.span);
|
||||
}
|
||||
|
||||
// Argument 1: g (shift right 2, shift left 5)
|
||||
if let Some(expr) = call.arguments[1].as_expression() {
|
||||
self.compile_expr(expr)?;
|
||||
self.emit_instr(InstrKind::PushInt(2), call.span);
|
||||
self.emit_instr(InstrKind::Shr, call.span);
|
||||
self.emit_instr(InstrKind::PushInt(5), call.span);
|
||||
self.emit_instr(InstrKind::Shl, call.span);
|
||||
}
|
||||
|
||||
self.emit_instr(InstrKind::BitOr, call.span);
|
||||
|
||||
// Argument 2: b (shift right 3)
|
||||
if let Some(expr) = call.arguments[2].as_expression() {
|
||||
self.compile_expr(expr)?;
|
||||
self.emit_instr(InstrKind::PushInt(3), call.span);
|
||||
self.emit_instr(InstrKind::Shr, call.span);
|
||||
}
|
||||
|
||||
self.emit_instr(InstrKind::BitOr, call.span);
|
||||
|
||||
} else {
|
||||
// Standard System Call
|
||||
for arg in &call.arguments {
|
||||
if let Some(expr) = arg.as_expression() {
|
||||
self.compile_expr(expr)?;
|
||||
}
|
||||
}
|
||||
self.emit_instr(InstrKind::Syscall(name), call.span);
|
||||
}
|
||||
} else {
|
||||
// Local function call (to a function defined in the project)
|
||||
for arg in &call.arguments {
|
||||
if let Some(expr) = arg.as_expression() {
|
||||
self.compile_expr(expr)?;
|
||||
}
|
||||
}
|
||||
self.emit_instr(InstrKind::Call { name, arg_count: call.arguments.len() as u32 }, call.span);
|
||||
}
|
||||
}
|
||||
// Member access (e.g., Color.RED, Pad.A.down)
|
||||
Expression::StaticMemberExpression(member) => {
|
||||
let full_name = ast_util::get_member_expr_name(expr)?;
|
||||
|
||||
if full_name.to_lowercase().starts_with("color.") {
|
||||
// Resolved at compile-time to literal values
|
||||
match full_name.to_lowercase().as_str() {
|
||||
"color.black" => self.emit_instr(InstrKind::PushInt(Color::BLACK.raw() as i64), member.span),
|
||||
"color.white" => self.emit_instr(InstrKind::PushInt(Color::WHITE.raw() as i64), member.span),
|
||||
"color.red" => self.emit_instr(InstrKind::PushInt(Color::RED.raw() as i64), member.span),
|
||||
"color.green" => self.emit_instr(InstrKind::PushInt(Color::GREEN.raw() as i64), member.span),
|
||||
"color.blue" => self.emit_instr(InstrKind::PushInt(Color::BLUE.raw() as i64), member.span),
|
||||
"color.yellow" => self.emit_instr(InstrKind::PushInt(Color::YELLOW.raw() as i64), member.span),
|
||||
"color.cyan" => self.emit_instr(InstrKind::PushInt(Color::CYAN.raw() as i64), member.span),
|
||||
"color.gray" | "color.grey" => self.emit_instr(InstrKind::PushInt(Color::GRAY.raw() as i64), member.span),
|
||||
"color.orange" => self.emit_instr(InstrKind::PushInt(Color::ORANGE.raw() as i64), member.span),
|
||||
"color.indigo" => self.emit_instr(InstrKind::PushInt(Color::INDIGO.raw() as i64), member.span),
|
||||
"color.magenta" => self.emit_instr(InstrKind::PushInt(Color::MAGENTA.raw() as i64), member.span),
|
||||
"color.colorkey" | "color.color_key" => self.emit_instr(InstrKind::PushInt(Color::COLOR_KEY.raw() as i64), member.span),
|
||||
_ => return Err(anyhow!("Unsupported color constant: {} at {:?}", full_name, member.span)),
|
||||
}
|
||||
} else if full_name.to_lowercase().starts_with("pad.") {
|
||||
// Re-mapped to specific input syscalls
|
||||
let parts: Vec<&str> = full_name.split('.').collect();
|
||||
if parts.len() == 3 {
|
||||
let btn_name = parts[1];
|
||||
let state_name = parts[2];
|
||||
let btn_id = input_map::map_btn_name(btn_name)?;
|
||||
let syscall_name = match state_name {
|
||||
"down" => "InputGetPad",
|
||||
"pressed" => "InputGetPadPressed",
|
||||
"released" => "InputGetPadReleased",
|
||||
"holdFrames" => "InputGetPadHold",
|
||||
_ => return Err(anyhow!("Unsupported button state: {} at {:?}", state_name, member.span)),
|
||||
};
|
||||
self.emit_instr(InstrKind::PushInt(btn_id as i64), member.span);
|
||||
self.emit_instr(InstrKind::Syscall(syscall_name.to_string()), member.span);
|
||||
} else {
|
||||
return Err(anyhow!("Partial Pad access not supported: {} at {:?}", full_name, member.span));
|
||||
}
|
||||
} else if full_name.to_lowercase().starts_with("touch.") {
|
||||
// Re-mapped to specific touch syscalls
|
||||
let parts: Vec<&str> = full_name.split('.').collect();
|
||||
match parts.len() {
|
||||
2 => {
|
||||
let prop = parts[1];
|
||||
let syscall_name = match prop {
|
||||
"x" => "TouchGetX",
|
||||
"y" => "TouchGetY",
|
||||
_ => return Err(anyhow!("Unsupported touch property: {} at {:?}", prop, member.span)),
|
||||
};
|
||||
self.emit_instr(InstrKind::Syscall(syscall_name.to_string()), member.span);
|
||||
}
|
||||
3 if parts[1] == "button" => {
|
||||
let state_name = parts[2];
|
||||
let syscall_name = match state_name {
|
||||
"down" => "TouchIsDown",
|
||||
"pressed" => "TouchIsPressed",
|
||||
"released" => "TouchIsReleased",
|
||||
"holdFrames" => "TouchGetHold",
|
||||
_ => return Err(anyhow!("Unsupported touch button state: {} at {:?}", state_name, member.span)),
|
||||
};
|
||||
self.emit_instr(InstrKind::Syscall(syscall_name.to_string()), member.span);
|
||||
}
|
||||
_ => return Err(anyhow!("Unsupported touch access: {} at {:?}", full_name, member.span)),
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow!("Member expression outside call not supported: {} at {:?}", full_name, member.span));
|
||||
}
|
||||
}
|
||||
_ => return Err(anyhow!("Unsupported expression type at {:?}", expr.span())),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generates a new unique label name for control flow.
|
||||
fn new_label(&mut self, prefix: &str) -> String {
|
||||
let label = format!("{}_{}", prefix, self.label_count);
|
||||
self.label_count += 1;
|
||||
label
|
||||
}
|
||||
|
||||
/// Emits a label definition in the instruction stream.
|
||||
fn emit_label(&mut self, name: String) {
|
||||
self.instructions.push(IRInstruction::new(InstrKind::Label(IRLabel(name)), None));
|
||||
}
|
||||
|
||||
/// Emits an IR instruction.
|
||||
fn emit_instr(&mut self, kind: InstrKind, span: Span) {
|
||||
let ir_span = if !span.is_unspanned() {
|
||||
Some(IRSpan::new(0, span.start, span.end))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.instructions.push(IRInstruction::new(kind, ir_span));
|
||||
}
|
||||
}
|
||||
180
crates/prometeu-compiler/src/frontends/ts/validate.rs
Normal file
180
crates/prometeu-compiler/src/frontends/ts/validate.rs
Normal file
@ -0,0 +1,180 @@
|
||||
use crate::backend::syscall_registry as syscall_map;
|
||||
use crate::frontends::ts::ast_util;
|
||||
use anyhow::{anyhow, Result};
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_ast_visit::{walk, Visit};
|
||||
use oxc_span::GetSpan;
|
||||
use oxc_syntax::scope::ScopeFlags;
|
||||
|
||||
/// AST Visitor that ensures the source code follows the Prometeu subset of JS/TS.
|
||||
///
|
||||
/// Since the Prometeu Virtual Machine is highly optimized and focused on game logic,
|
||||
/// it does not support the full ECMA-262 specification (e.g., no `async`, `class`,
|
||||
/// `try/catch`, or `generators`).
|
||||
pub struct Validator {
|
||||
/// List of validation errors found during the pass.
|
||||
errors: Vec<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) {
|
||||
if syscall_map::map_syscall(&name).is_none() && !self.local_functions.contains(&name) {
|
||||
self.errors.push(format!("Unsupported function call: {} at {:?}", name, expr.span));
|
||||
}
|
||||
} else {
|
||||
self.errors.push(format!("Unsupported callee expression at {:?}", expr.callee.span()));
|
||||
}
|
||||
walk::walk_call_expression(self, expr);
|
||||
}
|
||||
|
||||
fn visit_unary_expression(&mut self, expr: &UnaryExpression<'a>) {
|
||||
match expr.operator {
|
||||
UnaryOperator::UnaryNegation |
|
||||
UnaryOperator::UnaryPlus |
|
||||
UnaryOperator::LogicalNot => {
|
||||
walk::walk_unary_expression(self, expr);
|
||||
}
|
||||
_ => {
|
||||
self.errors.push(format!("Unsupported unary operator {:?} at {:?}", expr.operator, expr.span));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_binary_expression(&mut self, expr: &BinaryExpression<'a>) {
|
||||
match expr.operator {
|
||||
BinaryOperator::Addition |
|
||||
BinaryOperator::Subtraction |
|
||||
BinaryOperator::Multiplication |
|
||||
BinaryOperator::Division |
|
||||
BinaryOperator::Equality |
|
||||
BinaryOperator::Inequality |
|
||||
BinaryOperator::LessThan |
|
||||
BinaryOperator::GreaterThan |
|
||||
BinaryOperator::LessEqualThan |
|
||||
BinaryOperator::GreaterEqualThan => {
|
||||
walk::walk_binary_expression(self, expr);
|
||||
}
|
||||
_ => {
|
||||
self.errors.push(format!("Unsupported binary operator {:?} at {:?}", expr.operator, expr.span));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_logical_expression(&mut self, expr: &LogicalExpression<'a>) {
|
||||
match expr.operator {
|
||||
LogicalOperator::And |
|
||||
LogicalOperator::Or => {
|
||||
walk::walk_logical_expression(self, expr);
|
||||
}
|
||||
_ => {
|
||||
self.errors.push(format!("Unsupported logical operator {:?} at {:?}", expr.operator, expr.span));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates that only supported statements are used.
|
||||
fn visit_statement(&mut self, stmt: &Statement<'a>) {
|
||||
match stmt {
|
||||
Statement::VariableDeclaration(_) |
|
||||
Statement::ExpressionStatement(_) |
|
||||
Statement::IfStatement(_) |
|
||||
Statement::BlockStatement(_) |
|
||||
Statement::ExportNamedDeclaration(_) |
|
||||
Statement::ImportDeclaration(_) |
|
||||
Statement::FunctionDeclaration(_) |
|
||||
Statement::ReturnStatement(_) => {
|
||||
// These are the only statements the PVM handles currently.
|
||||
walk::walk_statement(self, stmt);
|
||||
}
|
||||
_ => {
|
||||
self.errors.push(format!("Unsupported statement type at {:?}. Note: Prometeu does not support while/for loops or classes yet.", stmt.span()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
78
crates/prometeu-compiler/src/ir/instr.rs
Normal file
78
crates/prometeu-compiler/src/ir/instr.rs
Normal file
@ -0,0 +1,78 @@
|
||||
use crate::common::spans::Span;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Instruction {
|
||||
pub kind: InstrKind,
|
||||
pub span: Option<Span>,
|
||||
}
|
||||
|
||||
impl Instruction {
|
||||
pub fn new(kind: InstrKind, span: Option<Span>) -> Self {
|
||||
Self { kind, span }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Label(pub String);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum InstrKind {
|
||||
Nop,
|
||||
Halt,
|
||||
|
||||
// Literals
|
||||
PushInt(i64),
|
||||
PushFloat(f64),
|
||||
PushBool(bool),
|
||||
PushString(String),
|
||||
PushNull,
|
||||
|
||||
// Stack Ops
|
||||
Pop,
|
||||
Dup,
|
||||
Swap,
|
||||
|
||||
// Arithmetic
|
||||
Add,
|
||||
Sub,
|
||||
Mul,
|
||||
Div,
|
||||
Neg,
|
||||
|
||||
// Logical/Comparison
|
||||
Eq,
|
||||
Neq,
|
||||
Lt,
|
||||
Gt,
|
||||
Lte,
|
||||
Gte,
|
||||
And,
|
||||
Or,
|
||||
Not,
|
||||
|
||||
// Bitwise
|
||||
BitAnd,
|
||||
BitOr,
|
||||
BitXor,
|
||||
Shl,
|
||||
Shr,
|
||||
|
||||
// Variables
|
||||
GetLocal(u32),
|
||||
SetLocal(u32),
|
||||
GetGlobal(u32),
|
||||
SetGlobal(u32),
|
||||
|
||||
// Control Flow
|
||||
Jmp(Label),
|
||||
JmpIfFalse(Label),
|
||||
Label(Label),
|
||||
Call { name: String, arg_count: u32 },
|
||||
Ret,
|
||||
|
||||
// OS / System
|
||||
Syscall(String),
|
||||
FrameSync,
|
||||
PushScope,
|
||||
PopScope,
|
||||
}
|
||||
8
crates/prometeu-compiler/src/ir/mod.rs
Normal file
8
crates/prometeu-compiler/src/ir/mod.rs
Normal file
@ -0,0 +1,8 @@
|
||||
pub mod types;
|
||||
pub mod module;
|
||||
pub mod instr;
|
||||
pub mod validate;
|
||||
|
||||
pub use instr::Instruction;
|
||||
pub use module::Module;
|
||||
pub use types::Type;
|
||||
40
crates/prometeu-compiler/src/ir/module.rs
Normal file
40
crates/prometeu-compiler/src/ir/module.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use crate::ir::instr::Instruction;
|
||||
use crate::ir::types::Type;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Module {
|
||||
pub name: String,
|
||||
pub functions: Vec<Function>,
|
||||
pub globals: Vec<Global>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Function {
|
||||
pub name: String,
|
||||
pub params: Vec<Param>,
|
||||
pub return_type: Type,
|
||||
pub body: Vec<Instruction>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Param {
|
||||
pub name: String,
|
||||
pub r#type: Type,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Global {
|
||||
pub name: String,
|
||||
pub r#type: Type,
|
||||
pub slot: u32,
|
||||
}
|
||||
|
||||
impl Module {
|
||||
pub fn new(name: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
functions: Vec::new(),
|
||||
globals: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
14
crates/prometeu-compiler/src/ir/types.rs
Normal file
14
crates/prometeu-compiler/src/ir/types.rs
Normal file
@ -0,0 +1,14 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Type {
|
||||
Any,
|
||||
Null,
|
||||
Bool,
|
||||
Int,
|
||||
Float,
|
||||
String,
|
||||
Color,
|
||||
Array(Box<Type>),
|
||||
Object,
|
||||
Function,
|
||||
Void,
|
||||
}
|
||||
10
crates/prometeu-compiler/src/ir/validate.rs
Normal file
10
crates/prometeu-compiler/src/ir/validate.rs
Normal file
@ -0,0 +1,10 @@
|
||||
use crate::common::diagnostics::DiagnosticBundle;
|
||||
use crate::ir::module::Module;
|
||||
|
||||
pub fn validate_module(_module: &Module) -> Result<(), DiagnosticBundle> {
|
||||
// TODO: Implement common IR validations:
|
||||
// - Type checking rules
|
||||
// - Syscall signatures
|
||||
// - VM invariants
|
||||
Ok(())
|
||||
}
|
||||
@ -15,7 +15,10 @@
|
||||
//! prometeu-compiler build ./my-game --entry ./src/main.ts --out ./game.pbc
|
||||
//! ```
|
||||
|
||||
pub mod codegen;
|
||||
pub mod common;
|
||||
pub mod ir;
|
||||
pub mod backend;
|
||||
pub mod frontends;
|
||||
pub mod compiler;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user