pr 50
This commit is contained in:
parent
83e24920b4
commit
ff4dcad5dc
@ -17,6 +17,8 @@ pub enum Operand {
|
||||
Bool(bool),
|
||||
/// A symbolic label that will be resolved to an absolute PC address.
|
||||
Label(String),
|
||||
/// A symbolic label that will be resolved to a PC address relative to another label.
|
||||
RelLabel(String, String),
|
||||
}
|
||||
|
||||
/// Represents an assembly-level element (either an instruction or a label).
|
||||
@ -33,7 +35,7 @@ pub fn update_pc_by_operand(initial_pc: u32, operands: &Vec<Operand>) -> u32 {
|
||||
let mut pcp: u32 = initial_pc;
|
||||
for operand in operands {
|
||||
match operand {
|
||||
Operand::U32(_) | Operand::I32(_) | Operand::Label(_) => pcp += 4,
|
||||
Operand::U32(_) | Operand::I32(_) | Operand::Label(_) | Operand::RelLabel(_, _) => pcp += 4,
|
||||
Operand::I64(_) | Operand::F64(_) => pcp += 8,
|
||||
Operand::Bool(_) => pcp += 1,
|
||||
}
|
||||
@ -83,6 +85,12 @@ pub fn assemble(instructions: &[Asm]) -> Result<Vec<u8>, String> {
|
||||
let addr = labels.get(name).ok_or(format!("Undefined label: {}", name))?;
|
||||
write_u32_le(&mut rom, *addr).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Operand::RelLabel(name, base) => {
|
||||
let addr = labels.get(name).ok_or(format!("Undefined label: {}", name))?;
|
||||
let base_addr = labels.get(base).ok_or(format!("Undefined base label: {}", base))?;
|
||||
let rel_addr = (*addr as i64) - (*base_addr as i64);
|
||||
write_u32_le(&mut rom, rel_addr as u32).map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,24 +41,20 @@ pub fn disasm(rom: &[u8]) -> Result<Vec<Instr>, String> {
|
||||
match opcode {
|
||||
OpCode::PushConst | OpCode::PushI32 | OpCode::PushBounded | OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue
|
||||
| OpCode::GetGlobal | OpCode::SetGlobal | OpCode::GetLocal | OpCode::SetLocal
|
||||
| OpCode::PopN | OpCode::Syscall | OpCode::GateLoad | OpCode::GateStore => {
|
||||
| OpCode::PopN | OpCode::Syscall | OpCode::GateLoad | OpCode::GateStore | OpCode::Call => {
|
||||
let v = read_u32_le(&mut cursor).map_err(|e| e.to_string())?;
|
||||
operands.push(DisasmOperand::U32(v));
|
||||
}
|
||||
OpCode::PushI64 => {
|
||||
OpCode::PushI64 | OpCode::PushF64 => {
|
||||
let v = read_i64_le(&mut cursor).map_err(|e| e.to_string())?;
|
||||
operands.push(DisasmOperand::I64(v));
|
||||
}
|
||||
OpCode::PushF64 => {
|
||||
let v = read_f64_le(&mut cursor).map_err(|e| e.to_string())?;
|
||||
operands.push(DisasmOperand::F64(v));
|
||||
}
|
||||
OpCode::PushBool => {
|
||||
let mut b_buf = [0u8; 1];
|
||||
cursor.read_exact(&mut b_buf).map_err(|e| e.to_string())?;
|
||||
operands.push(DisasmOperand::Bool(b_buf[0] != 0));
|
||||
}
|
||||
OpCode::Call | OpCode::Alloc => {
|
||||
OpCode::Alloc => {
|
||||
let v1 = read_u32_le(&mut cursor).map_err(|e| e.to_string())?;
|
||||
let v2 = read_u32_le(&mut cursor).map_err(|e| e.to_string())?;
|
||||
operands.push(DisasmOperand::U32(v1));
|
||||
|
||||
@ -8,7 +8,6 @@
|
||||
//!
|
||||
//! ## Core Components:
|
||||
//! - [`opcode`]: Defines the available instructions and their performance characteristics.
|
||||
//! - [`pbc`]: Handles the serialization and deserialization of `.pbc` files.
|
||||
//! - [`abi`]: Specifies the binary rules for operands and stack behavior.
|
||||
//! - [`asm`]: Provides a programmatic Assembler to convert high-level instructions to bytes.
|
||||
//! - [`disasm`]: Provides a Disassembler to inspect compiled bytecode.
|
||||
@ -16,7 +15,6 @@
|
||||
|
||||
pub mod opcode;
|
||||
pub mod abi;
|
||||
pub mod pbc;
|
||||
pub mod readwrite;
|
||||
pub mod asm;
|
||||
pub mod disasm;
|
||||
|
||||
@ -1,195 +0,0 @@
|
||||
use crate::readwrite::*;
|
||||
use std::io::{Cursor, Read, Write};
|
||||
|
||||
/// An entry in the Constant Pool.
|
||||
///
|
||||
/// The Constant Pool is a table of unique values used by the program.
|
||||
/// Instead of embedding large data (like strings) directly in the instruction stream,
|
||||
/// the bytecode uses `PushConst <index>` to load these values onto the stack.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ConstantPoolEntry {
|
||||
/// Reserved index (0). Represents a null/undefined value.
|
||||
Null,
|
||||
/// A 64-bit integer constant.
|
||||
Int64(i64),
|
||||
/// A 64-bit floating point constant.
|
||||
Float64(f64),
|
||||
/// A boolean constant.
|
||||
Boolean(bool),
|
||||
/// A UTF-8 string constant.
|
||||
String(String),
|
||||
/// A 32-bit integer constant.
|
||||
Int32(i32),
|
||||
}
|
||||
|
||||
/// Represents a compiled Prometeu ByteCode (.pbc) file.
|
||||
///
|
||||
/// The file format follows this structure (Little-Endian):
|
||||
/// 1. Magic Header: "PPBC" (4 bytes)
|
||||
/// 2. Version: u16 (Currently 0)
|
||||
/// 3. Flags: u16 (Reserved)
|
||||
/// 4. CP Count: u32
|
||||
/// 5. CP Entries: [Tag (u8), Data...]
|
||||
/// 6. ROM Size: u32
|
||||
/// 7. ROM Data: [u16 OpCode, Operands...][]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PbcFile {
|
||||
/// The file format version.
|
||||
pub version: u16,
|
||||
/// The list of constants used by the program.
|
||||
pub cp: Vec<ConstantPoolEntry>,
|
||||
/// The raw instruction bytes (ROM).
|
||||
pub rom: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Parses a raw byte buffer into a `PbcFile` structure.
|
||||
///
|
||||
/// This function validates the "PPBC" signature and reconstructs the
|
||||
/// Constant Pool and ROM data from the binary format.
|
||||
pub fn parse_pbc(bytes: &[u8]) -> Result<PbcFile, String> {
|
||||
if bytes.len() < 8 || &bytes[0..4] != b"PPBC" {
|
||||
return Err("Invalid PBC signature".into());
|
||||
}
|
||||
|
||||
let mut cursor = Cursor::new(&bytes[4..]);
|
||||
|
||||
let version = read_u16_le(&mut cursor).map_err(|e| e.to_string())?;
|
||||
let _flags = read_u16_le(&mut cursor).map_err(|e| e.to_string())?;
|
||||
|
||||
let cp_count = read_u32_le(&mut cursor).map_err(|e| e.to_string())? as usize;
|
||||
let mut cp = Vec::with_capacity(cp_count);
|
||||
|
||||
for _ in 0..cp_count {
|
||||
let mut tag_buf = [0u8; 1];
|
||||
cursor.read_exact(&mut tag_buf).map_err(|e| e.to_string())?;
|
||||
let tag = tag_buf[0];
|
||||
|
||||
match tag {
|
||||
1 => {
|
||||
let val = read_i64_le(&mut cursor).map_err(|e| e.to_string())?;
|
||||
cp.push(ConstantPoolEntry::Int64(val));
|
||||
}
|
||||
2 => {
|
||||
let val = read_f64_le(&mut cursor).map_err(|e| e.to_string())?;
|
||||
cp.push(ConstantPoolEntry::Float64(val));
|
||||
}
|
||||
3 => {
|
||||
let mut bool_buf = [0u8; 1];
|
||||
cursor.read_exact(&mut bool_buf).map_err(|e| e.to_string())?;
|
||||
cp.push(ConstantPoolEntry::Boolean(bool_buf[0] != 0));
|
||||
}
|
||||
4 => {
|
||||
let len = read_u32_le(&mut cursor).map_err(|e| e.to_string())? as usize;
|
||||
let mut s_buf = vec![0u8; len];
|
||||
cursor.read_exact(&mut s_buf).map_err(|e| e.to_string())?;
|
||||
let s = String::from_utf8_lossy(&s_buf).into_owned();
|
||||
cp.push(ConstantPoolEntry::String(s));
|
||||
}
|
||||
5 => {
|
||||
let val = read_u32_le(&mut cursor).map_err(|e| e.to_string())? as i32;
|
||||
cp.push(ConstantPoolEntry::Int32(val));
|
||||
}
|
||||
_ => cp.push(ConstantPoolEntry::Null),
|
||||
}
|
||||
}
|
||||
|
||||
let rom_size = read_u32_le(&mut cursor).map_err(|e| e.to_string())? as usize;
|
||||
let mut rom = vec![0u8; rom_size];
|
||||
cursor.read_exact(&mut rom).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(PbcFile { version, cp, rom })
|
||||
}
|
||||
|
||||
/// Serializes a `PbcFile` structure into a binary buffer.
|
||||
///
|
||||
/// This is used by the compiler to generate the final .pbc file.
|
||||
pub fn write_pbc(pbc: &PbcFile) -> Result<Vec<u8>, String> {
|
||||
let mut out = Vec::new();
|
||||
out.write_all(b"PPBC").map_err(|e| e.to_string())?;
|
||||
|
||||
write_u16_le(&mut out, pbc.version).map_err(|e| e.to_string())?;
|
||||
write_u16_le(&mut out, 0).map_err(|e| e.to_string())?; // Flags reserved
|
||||
|
||||
write_u32_le(&mut out, pbc.cp.len() as u32).map_err(|e| e.to_string())?;
|
||||
|
||||
for entry in &pbc.cp {
|
||||
match entry {
|
||||
ConstantPoolEntry::Null => {
|
||||
out.write_all(&[0]).map_err(|e| e.to_string())?;
|
||||
}
|
||||
ConstantPoolEntry::Int64(v) => {
|
||||
out.write_all(&[1]).map_err(|e| e.to_string())?;
|
||||
write_i64_le(&mut out, *v).map_err(|e| e.to_string())?;
|
||||
}
|
||||
ConstantPoolEntry::Float64(v) => {
|
||||
out.write_all(&[2]).map_err(|e| e.to_string())?;
|
||||
write_f64_le(&mut out, *v).map_err(|e| e.to_string())?;
|
||||
}
|
||||
ConstantPoolEntry::Boolean(v) => {
|
||||
out.write_all(&[3]).map_err(|e| e.to_string())?;
|
||||
out.write_all(&[if *v { 1 } else { 0 }]).map_err(|e| e.to_string())?;
|
||||
}
|
||||
ConstantPoolEntry::String(v) => {
|
||||
out.write_all(&[4]).map_err(|e| e.to_string())?;
|
||||
let bytes = v.as_bytes();
|
||||
write_u32_le(&mut out, bytes.len() as u32).map_err(|e| e.to_string())?;
|
||||
out.write_all(bytes).map_err(|e| e.to_string())?;
|
||||
}
|
||||
ConstantPoolEntry::Int32(v) => {
|
||||
out.write_all(&[5]).map_err(|e| e.to_string())?;
|
||||
write_u32_le(&mut out, *v as u32).map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
write_u32_le(&mut out, pbc.rom.len() as u32).map_err(|e| e.to_string())?;
|
||||
out.write_all(&pbc.rom).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::asm::{self, Asm, Operand};
|
||||
use crate::disasm;
|
||||
use crate::opcode::OpCode;
|
||||
use crate::pbc::{self, ConstantPoolEntry, PbcFile};
|
||||
|
||||
#[test]
|
||||
fn test_golden_abi_roundtrip() {
|
||||
// 1. Create a simple assembly program: PushI32 42; Halt
|
||||
let instructions = vec![
|
||||
Asm::Op(OpCode::PushI32, vec![Operand::I32(42)]),
|
||||
Asm::Op(OpCode::Halt, vec![]),
|
||||
];
|
||||
|
||||
let rom = asm::assemble(&instructions).expect("Failed to assemble");
|
||||
|
||||
// 2. Create a PBC file
|
||||
let pbc_file = PbcFile {
|
||||
version: 0,
|
||||
cp: vec![ConstantPoolEntry::Int32(100)], // Random CP entry
|
||||
rom,
|
||||
};
|
||||
|
||||
let bytes = pbc::write_pbc(&pbc_file).expect("Failed to write PBC");
|
||||
|
||||
// 3. Parse it back
|
||||
let parsed_pbc = pbc::parse_pbc(&bytes).expect("Failed to parse PBC");
|
||||
|
||||
assert_eq!(parsed_pbc.cp, pbc_file.cp);
|
||||
assert_eq!(parsed_pbc.rom, pbc_file.rom);
|
||||
|
||||
// 4. Disassemble ROM
|
||||
let dis_instrs = disasm::disasm(&parsed_pbc.rom).expect("Failed to disassemble");
|
||||
|
||||
assert_eq!(dis_instrs.len(), 2);
|
||||
assert_eq!(dis_instrs[0].opcode, OpCode::PushI32);
|
||||
if let disasm::DisasmOperand::U32(v) = dis_instrs[0].operands[0] {
|
||||
assert_eq!(v, 42);
|
||||
} else {
|
||||
panic!("Wrong operand type");
|
||||
}
|
||||
assert_eq!(dis_instrs[1].opcode, OpCode::Halt);
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,27 @@
|
||||
use crate::pbc::ConstantPoolEntry;
|
||||
use crate::opcode::OpCode;
|
||||
use crate::abi::SourceSpan;
|
||||
|
||||
/// An entry in the Constant Pool.
|
||||
///
|
||||
/// The Constant Pool is a table of unique values used by the program.
|
||||
/// Instead of embedding large data (like strings) directly in the instruction stream,
|
||||
/// the bytecode uses `PushConst <index>` to load these values onto the stack.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ConstantPoolEntry {
|
||||
/// Reserved index (0). Represents a null/undefined value.
|
||||
Null,
|
||||
/// A 64-bit integer constant.
|
||||
Int64(i64),
|
||||
/// A 64-bit floating point constant.
|
||||
Float64(f64),
|
||||
/// A boolean constant.
|
||||
Boolean(bool),
|
||||
/// A UTF-8 string constant.
|
||||
String(String),
|
||||
/// A 32-bit integer constant.
|
||||
Int32(i32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum LoadError {
|
||||
InvalidMagic,
|
||||
@ -56,6 +76,152 @@ pub struct BytecodeModule {
|
||||
pub imports: Vec<Import>,
|
||||
}
|
||||
|
||||
impl BytecodeModule {
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let cp_data = self.serialize_const_pool();
|
||||
let func_data = self.serialize_functions();
|
||||
let code_data = self.code.clone();
|
||||
let debug_data = self.debug_info.as_ref().map(|di| self.serialize_debug(di)).unwrap_or_default();
|
||||
let export_data = self.serialize_exports();
|
||||
let import_data = self.serialize_imports();
|
||||
|
||||
let mut final_sections = Vec::new();
|
||||
if !cp_data.is_empty() { final_sections.push((0, cp_data)); }
|
||||
if !func_data.is_empty() { final_sections.push((1, func_data)); }
|
||||
if !code_data.is_empty() { final_sections.push((2, code_data)); }
|
||||
if !debug_data.is_empty() { final_sections.push((3, debug_data)); }
|
||||
if !export_data.is_empty() { final_sections.push((4, export_data)); }
|
||||
if !import_data.is_empty() { final_sections.push((5, import_data)); }
|
||||
|
||||
let mut out = Vec::new();
|
||||
// Magic "PBS\0"
|
||||
out.extend_from_slice(b"PBS\0");
|
||||
// Version 0
|
||||
out.extend_from_slice(&0u16.to_le_bytes());
|
||||
// Endianness 0 (Little Endian), Reserved
|
||||
out.extend_from_slice(&[0u8, 0u8]);
|
||||
// section_count
|
||||
out.extend_from_slice(&(final_sections.len() as u32).to_le_bytes());
|
||||
// padding to 32 bytes
|
||||
out.extend_from_slice(&[0u8; 20]);
|
||||
|
||||
let mut current_offset = 32 + (final_sections.len() as u32 * 12);
|
||||
|
||||
// Write section table
|
||||
for (kind, data) in &final_sections {
|
||||
let k: u32 = *kind;
|
||||
out.extend_from_slice(&k.to_le_bytes());
|
||||
out.extend_from_slice(¤t_offset.to_le_bytes());
|
||||
out.extend_from_slice(&(data.len() as u32).to_le_bytes());
|
||||
current_offset += data.len() as u32;
|
||||
}
|
||||
|
||||
// Write section data
|
||||
for (_, data) in final_sections {
|
||||
out.extend_from_slice(&data);
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn serialize_const_pool(&self) -> Vec<u8> {
|
||||
if self.const_pool.is_empty() { return Vec::new(); }
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&(self.const_pool.len() as u32).to_le_bytes());
|
||||
for entry in &self.const_pool {
|
||||
match entry {
|
||||
ConstantPoolEntry::Null => data.push(0),
|
||||
ConstantPoolEntry::Int64(v) => {
|
||||
data.push(1);
|
||||
data.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
ConstantPoolEntry::Float64(v) => {
|
||||
data.push(2);
|
||||
data.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
ConstantPoolEntry::Boolean(v) => {
|
||||
data.push(3);
|
||||
data.push(if *v { 1 } else { 0 });
|
||||
}
|
||||
ConstantPoolEntry::String(v) => {
|
||||
data.push(4);
|
||||
let s_bytes = v.as_bytes();
|
||||
data.extend_from_slice(&(s_bytes.len() as u32).to_le_bytes());
|
||||
data.extend_from_slice(s_bytes);
|
||||
}
|
||||
ConstantPoolEntry::Int32(v) => {
|
||||
data.push(5);
|
||||
data.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
fn serialize_functions(&self) -> Vec<u8> {
|
||||
if self.functions.is_empty() { return Vec::new(); }
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&(self.functions.len() as u32).to_le_bytes());
|
||||
for f in &self.functions {
|
||||
data.extend_from_slice(&f.code_offset.to_le_bytes());
|
||||
data.extend_from_slice(&f.code_len.to_le_bytes());
|
||||
data.extend_from_slice(&f.param_slots.to_le_bytes());
|
||||
data.extend_from_slice(&f.local_slots.to_le_bytes());
|
||||
data.extend_from_slice(&f.return_slots.to_le_bytes());
|
||||
data.extend_from_slice(&f.max_stack_slots.to_le_bytes());
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
fn serialize_debug(&self, di: &DebugInfo) -> Vec<u8> {
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&(di.pc_to_span.len() as u32).to_le_bytes());
|
||||
for (pc, span) in &di.pc_to_span {
|
||||
data.extend_from_slice(&pc.to_le_bytes());
|
||||
data.extend_from_slice(&span.file_id.to_le_bytes());
|
||||
data.extend_from_slice(&span.start.to_le_bytes());
|
||||
data.extend_from_slice(&span.end.to_le_bytes());
|
||||
}
|
||||
data.extend_from_slice(&(di.function_names.len() as u32).to_le_bytes());
|
||||
for (idx, name) in &di.function_names {
|
||||
data.extend_from_slice(&idx.to_le_bytes());
|
||||
let n_bytes = name.as_bytes();
|
||||
data.extend_from_slice(&(n_bytes.len() as u32).to_le_bytes());
|
||||
data.extend_from_slice(n_bytes);
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
fn serialize_exports(&self) -> Vec<u8> {
|
||||
if self.exports.is_empty() { return Vec::new(); }
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&(self.exports.len() as u32).to_le_bytes());
|
||||
for exp in &self.exports {
|
||||
data.extend_from_slice(&exp.func_idx.to_le_bytes());
|
||||
let s_bytes = exp.symbol.as_bytes();
|
||||
data.extend_from_slice(&(s_bytes.len() as u32).to_le_bytes());
|
||||
data.extend_from_slice(s_bytes);
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
fn serialize_imports(&self) -> Vec<u8> {
|
||||
if self.imports.is_empty() { return Vec::new(); }
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&(self.imports.len() as u32).to_le_bytes());
|
||||
for imp in &self.imports {
|
||||
let s_bytes = imp.symbol.as_bytes();
|
||||
data.extend_from_slice(&(s_bytes.len() as u32).to_le_bytes());
|
||||
data.extend_from_slice(s_bytes);
|
||||
data.extend_from_slice(&(imp.relocation_pcs.len() as u32).to_le_bytes());
|
||||
for pc in &imp.relocation_pcs {
|
||||
data.extend_from_slice(&pc.to_le_bytes());
|
||||
}
|
||||
}
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BytecodeLoader;
|
||||
|
||||
impl BytecodeLoader {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use crate::common::symbols::Symbol;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result};
|
||||
use prometeu_bytecode::disasm::disasm;
|
||||
use prometeu_bytecode::v0::BytecodeLoader;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
@ -29,14 +30,14 @@ impl Artifacts {
|
||||
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
|
||||
// Extract the actual bytecode (stripping the industrial PBS\0 header)
|
||||
let rom_to_disasm = if let Ok(module) = BytecodeLoader::load(&self.rom) {
|
||||
module.code
|
||||
} else {
|
||||
self.rom.clone()
|
||||
};
|
||||
|
||||
let instructions = disasm(&rom_to_disasm).map_err(|e| anyhow!("Disassembly failed: {}", e))?;
|
||||
let instructions = disasm(&rom_to_disasm).map_err(|e| anyhow::anyhow!("Disassembly failed: {}", e))?;
|
||||
|
||||
let mut disasm_text = String::new();
|
||||
for instr in instructions {
|
||||
|
||||
@ -15,7 +15,8 @@ use crate::ir_core::ConstantValue;
|
||||
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};
|
||||
use prometeu_bytecode::v0::{BytecodeModule, FunctionMeta, DebugInfo, ConstantPoolEntry};
|
||||
use prometeu_bytecode::abi::SourceSpan;
|
||||
|
||||
/// The final output of the code generation phase.
|
||||
pub struct EmitResult {
|
||||
@ -68,34 +69,30 @@ impl<'a> BytecodeEmitter<'a> {
|
||||
self.add_constant(entry)
|
||||
}
|
||||
|
||||
/// Transforms an IR module into a binary PBC file.
|
||||
fn emit(&mut self, module: &ir_vm::Module) -> Result<EmitResult> {
|
||||
let mut asm_instrs = Vec::new();
|
||||
let mut ir_instr_map = Vec::new(); // Maps Asm index to IR instruction (for symbols)
|
||||
|
||||
// Pre-populate constant pool from IR and create a mapping for ConstIds
|
||||
let mut mapped_const_ids = Vec::with_capacity(module.const_pool.constants.len());
|
||||
for val in &module.const_pool.constants {
|
||||
mapped_const_ids.push(self.add_ir_constant(val));
|
||||
}
|
||||
|
||||
// Map FunctionIds to names for call resolution
|
||||
fn lower_instrs<'b>(
|
||||
&mut self,
|
||||
module: &'b ir_vm::Module,
|
||||
asm_instrs: &mut Vec<Asm>,
|
||||
ir_instr_map: &mut Vec<Option<&'b ir_vm::Instruction>>,
|
||||
mapped_const_ids: &[u32]
|
||||
) -> Result<Vec<(usize, usize)>> {
|
||||
let mut func_names = std::collections::HashMap::new();
|
||||
for func in &module.functions {
|
||||
func_names.insert(func.id, func.name.clone());
|
||||
}
|
||||
|
||||
// --- PHASE 1: Lowering IR to Assembly-like structures ---
|
||||
let mut ranges = Vec::new();
|
||||
|
||||
for function in &module.functions {
|
||||
let start_idx = asm_instrs.len();
|
||||
// Each function starts with a label for its entry point.
|
||||
asm_instrs.push(Asm::Label(function.name.clone()));
|
||||
ir_instr_map.push(None);
|
||||
|
||||
for instr in &function.body {
|
||||
let start_idx = asm_instrs.len();
|
||||
let op_start_idx = asm_instrs.len();
|
||||
|
||||
// Translate each IR instruction to its equivalent Bytecode OpCode.
|
||||
// Note: IR instructions are high-level, while Bytecode is low-level.
|
||||
match &instr.kind {
|
||||
InstrKind::Nop => asm_instrs.push(Asm::Op(OpCode::Nop, vec![])),
|
||||
InstrKind::Halt => asm_instrs.push(Asm::Op(OpCode::Halt, vec![])),
|
||||
@ -147,17 +144,17 @@ impl<'a> BytecodeEmitter<'a> {
|
||||
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())]));
|
||||
asm_instrs.push(Asm::Op(OpCode::Jmp, vec![Operand::RelLabel(label.0.clone(), function.name.clone())]));
|
||||
}
|
||||
InstrKind::JmpIfFalse(label) => {
|
||||
asm_instrs.push(Asm::Op(OpCode::JmpIfFalse, vec![Operand::Label(label.0.clone())]));
|
||||
asm_instrs.push(Asm::Op(OpCode::JmpIfFalse, vec![Operand::RelLabel(label.0.clone(), function.name.clone())]));
|
||||
}
|
||||
InstrKind::Label(label) => {
|
||||
asm_instrs.push(Asm::Label(label.0.clone()));
|
||||
}
|
||||
InstrKind::Call { func_id, arg_count } => {
|
||||
InstrKind::Call { func_id, .. } => {
|
||||
let name = func_names.get(func_id).ok_or_else(|| anyhow!("Undefined function ID: {:?}", func_id))?;
|
||||
asm_instrs.push(Asm::Op(OpCode::Call, vec![Operand::Label(name.clone()), Operand::U32(*arg_count)]));
|
||||
asm_instrs.push(Asm::Op(OpCode::Call, vec![Operand::Label(name.clone())]));
|
||||
}
|
||||
InstrKind::Ret => asm_instrs.push(Asm::Op(OpCode::Ret, vec![])),
|
||||
InstrKind::Syscall(id) => {
|
||||
@ -183,24 +180,75 @@ impl<'a> BytecodeEmitter<'a> {
|
||||
InstrKind::GateRelease => asm_instrs.push(Asm::Op(OpCode::GateRelease, vec![])),
|
||||
}
|
||||
|
||||
let end_idx = asm_instrs.len();
|
||||
for _ in start_idx..end_idx {
|
||||
let op_end_idx = asm_instrs.len();
|
||||
for _ in op_start_idx..op_end_idx {
|
||||
ir_instr_map.push(Some(instr));
|
||||
}
|
||||
}
|
||||
let end_idx = asm_instrs.len();
|
||||
ranges.push((start_idx, end_idx));
|
||||
}
|
||||
Ok(ranges)
|
||||
}
|
||||
|
||||
fn calculate_pcs(asm_instrs: &[Asm]) -> Vec<u32> {
|
||||
let mut pcs = Vec::with_capacity(asm_instrs.len());
|
||||
let mut current_pc = 0u32;
|
||||
for instr in asm_instrs {
|
||||
pcs.push(current_pc);
|
||||
match instr {
|
||||
Asm::Label(_) => {}
|
||||
Asm::Op(_opcode, operands) => {
|
||||
current_pc += 2;
|
||||
current_pc = update_pc_by_operand(current_pc, operands);
|
||||
}
|
||||
}
|
||||
}
|
||||
pcs
|
||||
}
|
||||
|
||||
/// Transforms an IR module into a binary PBC file (v0 industrial format).
|
||||
pub fn emit(&mut self, module: &ir_vm::Module) -> Result<EmitResult> {
|
||||
let mut mapped_const_ids = Vec::with_capacity(module.const_pool.constants.len());
|
||||
for val in &module.const_pool.constants {
|
||||
mapped_const_ids.push(self.add_ir_constant(val));
|
||||
}
|
||||
|
||||
// --- PHASE 2: Assembly (Label Resolution) ---
|
||||
// Converts the list of Ops and Labels into raw bytes, calculating jump offsets.
|
||||
let mut asm_instrs = Vec::new();
|
||||
let mut ir_instr_map = Vec::new();
|
||||
let function_ranges = self.lower_instrs(module, &mut asm_instrs, &mut ir_instr_map, &mapped_const_ids)?;
|
||||
|
||||
let pcs = Self::calculate_pcs(&asm_instrs);
|
||||
let bytecode = assemble(&asm_instrs).map_err(|e| anyhow!(e))?;
|
||||
|
||||
// --- PHASE 3: Symbol Generation ---
|
||||
// Associates each bytecode offset with a line/column in the source file.
|
||||
|
||||
let mut functions = Vec::new();
|
||||
for (i, function) in module.functions.iter().enumerate() {
|
||||
let (start_idx, end_idx) = function_ranges[i];
|
||||
let start_pc = pcs[start_idx];
|
||||
let end_pc = if end_idx < pcs.len() { pcs[end_idx] } else { bytecode.len() as u32 };
|
||||
|
||||
functions.push(FunctionMeta {
|
||||
code_offset: start_pc,
|
||||
code_len: end_pc - start_pc,
|
||||
param_slots: function.param_slots,
|
||||
local_slots: function.local_slots,
|
||||
return_slots: function.return_slots,
|
||||
max_stack_slots: 0, // Will be filled by verifier
|
||||
});
|
||||
}
|
||||
|
||||
let mut pc_to_span = Vec::new();
|
||||
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 {
|
||||
for (i, instr_opt) in ir_instr_map.iter().enumerate() {
|
||||
let current_pc = pcs[i];
|
||||
if let Some(instr) = instr_opt {
|
||||
if let Some(span) = &instr.span {
|
||||
pc_to_span.push((current_pc, SourceSpan {
|
||||
file_id: span.file_id as u32,
|
||||
start: span.start,
|
||||
end: span.end,
|
||||
}));
|
||||
|
||||
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())
|
||||
@ -214,30 +262,35 @@ impl<'a> BytecodeEmitter<'a> {
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
pc_to_span.sort_by_key(|(pc, _)| *pc);
|
||||
pc_to_span.dedup_by_key(|(pc, _)| *pc);
|
||||
|
||||
// Track the Program Counter (PC) as we iterate through instructions.
|
||||
match asm {
|
||||
Asm::Label(_) => {}
|
||||
Asm::Op(_opcode, operands) => {
|
||||
// Each OpCode takes 2 bytes (1 for opcode, 1 for padding/metadata)
|
||||
current_pc += 2;
|
||||
// Operands take additional space depending on their type.
|
||||
current_pc = update_pc_by_operand(current_pc, operands);
|
||||
}
|
||||
}
|
||||
let mut exports = Vec::new();
|
||||
let mut function_names = Vec::new();
|
||||
for (i, func) in module.functions.iter().enumerate() {
|
||||
exports.push(prometeu_bytecode::v0::Export {
|
||||
symbol: func.name.clone(),
|
||||
func_idx: i as u32,
|
||||
});
|
||||
function_names.push((i as u32, func.name.clone()));
|
||||
}
|
||||
|
||||
// --- PHASE 4: Serialization ---
|
||||
// Packages the constant pool and bytecode into the final PBC format.
|
||||
let pbc = PbcFile {
|
||||
let bytecode_module = BytecodeModule {
|
||||
version: 0,
|
||||
cp: self.constant_pool.clone(),
|
||||
rom: bytecode,
|
||||
const_pool: self.constant_pool.clone(),
|
||||
functions,
|
||||
code: bytecode,
|
||||
debug_info: Some(DebugInfo {
|
||||
pc_to_span,
|
||||
function_names,
|
||||
}),
|
||||
exports,
|
||||
imports: vec![],
|
||||
};
|
||||
|
||||
let out = write_pbc(&pbc).map_err(|e| anyhow!(e))?;
|
||||
Ok(EmitResult {
|
||||
rom: out,
|
||||
rom: bytecode_module.serialize(),
|
||||
symbols,
|
||||
})
|
||||
}
|
||||
@ -252,7 +305,7 @@ mod tests {
|
||||
use crate::ir_core::ids::FunctionId;
|
||||
use crate::ir_core::const_pool::ConstantValue;
|
||||
use crate::common::files::FileManager;
|
||||
use prometeu_bytecode::pbc::{parse_pbc, ConstantPoolEntry};
|
||||
use prometeu_bytecode::v0::{BytecodeLoader, ConstantPoolEntry};
|
||||
|
||||
#[test]
|
||||
fn test_emit_module_with_const_pool() {
|
||||
@ -271,6 +324,9 @@ mod tests {
|
||||
Instruction::new(InstrKind::PushConst(ir_vm::ConstId(id_str.0)), None),
|
||||
Instruction::new(InstrKind::Ret, None),
|
||||
],
|
||||
param_slots: 0,
|
||||
local_slots: 0,
|
||||
return_slots: 0,
|
||||
};
|
||||
|
||||
module.functions.push(function);
|
||||
@ -278,11 +334,11 @@ mod tests {
|
||||
let file_manager = FileManager::new();
|
||||
let result = emit_module(&module, &file_manager).expect("Failed to emit module");
|
||||
|
||||
let pbc = parse_pbc(&result.rom).expect("Failed to parse emitted PBC");
|
||||
let pbc = BytecodeLoader::load(&result.rom).expect("Failed to parse emitted PBC");
|
||||
|
||||
assert_eq!(pbc.cp.len(), 3);
|
||||
assert_eq!(pbc.cp[0], ConstantPoolEntry::Null);
|
||||
assert_eq!(pbc.cp[1], ConstantPoolEntry::Int64(12345));
|
||||
assert_eq!(pbc.cp[2], ConstantPoolEntry::String("hello".to_string()));
|
||||
assert_eq!(pbc.const_pool.len(), 3);
|
||||
assert_eq!(pbc.const_pool[0], ConstantPoolEntry::Null);
|
||||
assert_eq!(pbc.const_pool[1], ConstantPoolEntry::Int64(12345));
|
||||
assert_eq!(pbc.const_pool[2], ConstantPoolEntry::String("hello".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,3 +2,5 @@ pub mod emit_bytecode;
|
||||
pub mod artifacts;
|
||||
|
||||
pub use emit_bytecode::emit_module;
|
||||
pub use artifacts::Artifacts;
|
||||
pub use emit_bytecode::EmitResult;
|
||||
|
||||
@ -106,7 +106,7 @@ mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
use prometeu_bytecode::pbc::parse_pbc;
|
||||
use prometeu_bytecode::v0::BytecodeLoader;
|
||||
use prometeu_bytecode::disasm::disasm;
|
||||
use prometeu_bytecode::opcode::OpCode;
|
||||
|
||||
@ -152,8 +152,8 @@ mod tests {
|
||||
fs::write(project_dir.join("main.pbs"), code).unwrap();
|
||||
|
||||
let unit = compile(project_dir).expect("Failed to compile");
|
||||
let pbc = parse_pbc(&unit.rom).expect("Failed to parse PBC");
|
||||
let instrs = disasm(&pbc.rom).expect("Failed to disassemble");
|
||||
let pbc = BytecodeLoader::load(&unit.rom).expect("Failed to parse PBC");
|
||||
let instrs = disasm(&pbc.code).expect("Failed to disassemble");
|
||||
|
||||
let opcodes: Vec<_> = instrs.iter().map(|i| i.opcode).collect();
|
||||
|
||||
@ -202,8 +202,8 @@ mod tests {
|
||||
fs::write(project_dir.join("main.pbs"), code).unwrap();
|
||||
|
||||
let unit = compile(project_dir).expect("Failed to compile");
|
||||
let pbc = parse_pbc(&unit.rom).expect("Failed to parse PBC");
|
||||
let instrs = disasm(&pbc.rom).expect("Failed to disassemble");
|
||||
let pbc = BytecodeLoader::load(&unit.rom).expect("Failed to parse PBC");
|
||||
let instrs = disasm(&pbc.code).expect("Failed to disassemble");
|
||||
|
||||
let mut disasm_text = String::new();
|
||||
for instr in instrs {
|
||||
@ -230,37 +230,36 @@ mod tests {
|
||||
0028 GetLocal U32(0)
|
||||
002E PushConst U32(4)
|
||||
0034 Gt
|
||||
0036 JmpIfFalse U32(94)
|
||||
003C Jmp U32(66)
|
||||
0036 JmpIfFalse U32(74)
|
||||
003C Jmp U32(50)
|
||||
0042 GetLocal U32(0)
|
||||
0048 Call U32(0) U32(1)
|
||||
0052 SetLocal U32(1)
|
||||
0058 Jmp U32(100)
|
||||
005E Jmp U32(100)
|
||||
0064 Alloc U32(2) U32(1)
|
||||
006E SetLocal U32(1)
|
||||
0074 GetLocal U32(1)
|
||||
007A GateRetain
|
||||
007C SetLocal U32(2)
|
||||
0082 GetLocal U32(2)
|
||||
0088 GateRetain
|
||||
008A GateBeginMutate
|
||||
008C GetLocal U32(2)
|
||||
0092 GateRetain
|
||||
0094 GateLoad U32(0)
|
||||
009A SetLocal U32(3)
|
||||
00A0 GetLocal U32(3)
|
||||
00A6 PushConst U32(5)
|
||||
00AC Add
|
||||
00AE SetLocal U32(4)
|
||||
00B4 GateEndMutate
|
||||
00B6 GateRelease
|
||||
00B8 GetLocal U32(1)
|
||||
00BE GateRelease
|
||||
00C0 GetLocal U32(2)
|
||||
00C6 GateRelease
|
||||
00C8 PushConst U32(0)
|
||||
00CE Ret
|
||||
0048 Call U32(0)
|
||||
004E SetLocal U32(1)
|
||||
0054 Jmp U32(80)
|
||||
005A Jmp U32(80)
|
||||
0060 Alloc U32(2) U32(1)
|
||||
006A SetLocal U32(1)
|
||||
0070 GetLocal U32(1)
|
||||
0076 GateRetain
|
||||
0078 SetLocal U32(2)
|
||||
007E GetLocal U32(2)
|
||||
0084 GateRetain
|
||||
0086 GateBeginMutate
|
||||
0088 GetLocal U32(2)
|
||||
008E GateRetain
|
||||
0090 GateLoad U32(0)
|
||||
0096 SetLocal U32(3)
|
||||
009C GetLocal U32(3)
|
||||
00A2 PushConst U32(5)
|
||||
00A8 Add
|
||||
00AA SetLocal U32(4)
|
||||
00B0 GateEndMutate
|
||||
00B2 GateRelease
|
||||
00B4 GetLocal U32(1)
|
||||
00BA GateRelease
|
||||
00BC GetLocal U32(2)
|
||||
00C2 GateRelease
|
||||
00C4 Ret
|
||||
"#;
|
||||
|
||||
assert_eq!(disasm_text, expected_disasm);
|
||||
@ -269,7 +268,6 @@ mod tests {
|
||||
#[test]
|
||||
fn test_hip_conformance_v0() {
|
||||
use crate::ir_core::*;
|
||||
use crate::ir_core::ids::*;
|
||||
use crate::lowering::lower_program;
|
||||
use crate::backend;
|
||||
use std::collections::HashMap;
|
||||
@ -294,6 +292,9 @@ mod tests {
|
||||
functions: vec![Function {
|
||||
id: FunctionId(1),
|
||||
name: "main".to_string(),
|
||||
param_slots: 0,
|
||||
local_slots: 0,
|
||||
return_slots: 0,
|
||||
params: vec![],
|
||||
return_type: Type::Void,
|
||||
blocks: vec![Block {
|
||||
@ -351,23 +352,22 @@ mod tests {
|
||||
|
||||
let rom = emit_result.rom;
|
||||
|
||||
// --- 5. ASSERT GOLDEN BYTECODE (Exact Bytes) ---
|
||||
// Header: PPBC, Version: 0, Flags: 0
|
||||
assert_eq!(&rom[0..4], b"PPBC");
|
||||
assert_eq!(rom[4..6], [0, 0]); // Version 0
|
||||
assert_eq!(rom[6..8], [0, 0]); // Flags 0
|
||||
// --- 5. ASSERT INDUSTRIAL FORMAT ---
|
||||
use prometeu_bytecode::v0::BytecodeLoader;
|
||||
let pbc = BytecodeLoader::load(&rom).expect("Failed to parse industrial PBC");
|
||||
|
||||
// CP Count: 2 (Null, 42)
|
||||
assert_eq!(rom[8..12], [2, 0, 0, 0]);
|
||||
assert_eq!(&rom[0..4], b"PBS\0");
|
||||
assert_eq!(pbc.const_pool.len(), 2); // Null, 42
|
||||
|
||||
// ROM Data contains HIP opcodes:
|
||||
assert!(rom.contains(&0x60), "Bytecode must contain Alloc (0x60)");
|
||||
assert!(rom.contains(&0x67), "Bytecode must contain GateBeginMutate (0x67)");
|
||||
assert!(rom.contains(&0x62), "Bytecode must contain GateStore (0x62)");
|
||||
assert!(rom.contains(&0x63), "Bytecode must contain GateBeginPeek (0x63)");
|
||||
assert!(rom.contains(&0x61), "Bytecode must contain GateLoad (0x61)");
|
||||
assert!(rom.contains(&0x69), "Bytecode must contain GateRetain (0x69)");
|
||||
assert!(rom.contains(&0x6A), "Bytecode must contain GateRelease (0x6A)");
|
||||
let code = pbc.code;
|
||||
assert!(code.iter().any(|&b| b == 0x60), "Bytecode must contain Alloc (0x60)");
|
||||
assert!(code.iter().any(|&b| b == 0x67), "Bytecode must contain GateBeginMutate (0x67)");
|
||||
assert!(code.iter().any(|&b| b == 0x62), "Bytecode must contain GateStore (0x62)");
|
||||
assert!(code.iter().any(|&b| b == 0x63), "Bytecode must contain GateBeginPeek (0x63)");
|
||||
assert!(code.iter().any(|&b| b == 0x61), "Bytecode must contain GateLoad (0x61)");
|
||||
assert!(code.iter().any(|&b| b == 0x69), "Bytecode must contain GateRetain (0x69)");
|
||||
assert!(code.iter().any(|&b| b == 0x6A), "Bytecode must contain GateRelease (0x6A)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -30,6 +30,8 @@ pub enum Node {
|
||||
WhenArm(WhenArmNode),
|
||||
TypeName(TypeNameNode),
|
||||
TypeApp(TypeAppNode),
|
||||
ConstructorDecl(ConstructorDeclNode),
|
||||
ConstantDecl(ConstantDeclNode),
|
||||
Alloc(AllocNode),
|
||||
Mutate(MutateNode),
|
||||
Borrow(BorrowNode),
|
||||
@ -98,13 +100,33 @@ pub struct TypeDeclNode {
|
||||
pub type_kind: String, // "struct" | "contract" | "error"
|
||||
pub name: String,
|
||||
pub is_host: bool,
|
||||
pub body: Box<Node>, // TypeBody
|
||||
pub params: Vec<ParamNode>, // fields for struct/error
|
||||
pub constructors: Vec<ConstructorDeclNode>, // [ ... ]
|
||||
pub constants: Vec<ConstantDeclNode>, // [[ ... ]]
|
||||
pub body: Option<Box<Node>>, // TypeBody (methods)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ConstructorDeclNode {
|
||||
pub span: Span,
|
||||
pub params: Vec<ParamNode>,
|
||||
pub initializers: Vec<Node>,
|
||||
pub name: String,
|
||||
pub body: Box<Node>, // BlockNode
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ConstantDeclNode {
|
||||
pub span: Span,
|
||||
pub name: String,
|
||||
pub value: Box<Node>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct TypeBodyNode {
|
||||
pub span: Span,
|
||||
pub members: Vec<TypeMemberNode>,
|
||||
pub methods: Vec<ServiceFnSigNode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
|
||||
@ -74,8 +74,22 @@ impl<'a> Lexer<'a> {
|
||||
')' => TokenKind::CloseParen,
|
||||
'{' => TokenKind::OpenBrace,
|
||||
'}' => TokenKind::CloseBrace,
|
||||
'[' => TokenKind::OpenBracket,
|
||||
']' => TokenKind::CloseBracket,
|
||||
'[' => {
|
||||
if self.peek() == Some('[') {
|
||||
self.next();
|
||||
TokenKind::OpenDoubleBracket
|
||||
} else {
|
||||
TokenKind::OpenBracket
|
||||
}
|
||||
}
|
||||
']' => {
|
||||
if self.peek() == Some(']') {
|
||||
self.next();
|
||||
TokenKind::CloseDoubleBracket
|
||||
} else {
|
||||
TokenKind::CloseBracket
|
||||
}
|
||||
}
|
||||
',' => TokenKind::Comma,
|
||||
'.' => TokenKind::Dot,
|
||||
':' => TokenKind::Colon,
|
||||
|
||||
@ -26,8 +26,12 @@ pub struct Lowerer<'a> {
|
||||
function_ids: HashMap<String, FunctionId>,
|
||||
type_ids: HashMap<String, TypeId>,
|
||||
struct_slots: HashMap<String, u32>,
|
||||
struct_constructors: HashMap<String, HashMap<String, ConstructorDeclNode>>,
|
||||
type_constants: HashMap<String, HashMap<String, Node>>,
|
||||
current_type_context: Option<String>,
|
||||
contract_registry: ContractRegistry,
|
||||
diagnostics: Vec<Diagnostic>,
|
||||
max_slots_used: u32,
|
||||
}
|
||||
|
||||
impl<'a> Lowerer<'a> {
|
||||
@ -58,8 +62,12 @@ impl<'a> Lowerer<'a> {
|
||||
function_ids: HashMap::new(),
|
||||
type_ids: HashMap::new(),
|
||||
struct_slots,
|
||||
struct_constructors: HashMap::new(),
|
||||
type_constants: HashMap::new(),
|
||||
current_type_context: None,
|
||||
contract_registry: ContractRegistry::new(),
|
||||
diagnostics: Vec::new(),
|
||||
max_slots_used: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,15 +92,92 @@ impl<'a> Lowerer<'a> {
|
||||
let id = TypeId(self.next_type_id);
|
||||
self.next_type_id += 1;
|
||||
self.type_ids.insert(n.name.clone(), id);
|
||||
}
|
||||
}
|
||||
|
||||
// Second pre-scan: calculate struct slots (recursive)
|
||||
let mut struct_nodes = HashMap::new();
|
||||
for decl in &file.decls {
|
||||
if let Node::TypeDecl(n) = decl {
|
||||
if n.type_kind == "struct" {
|
||||
if let Node::TypeBody(body) = &*n.body {
|
||||
self.struct_slots.insert(n.name.clone(), body.members.len() as u32);
|
||||
struct_nodes.insert(n.name.clone(), n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut changed = true;
|
||||
while changed {
|
||||
changed = false;
|
||||
for (name, node) in &struct_nodes {
|
||||
if !self.struct_slots.contains_key(name) {
|
||||
let mut slots = 0;
|
||||
let mut all_known = true;
|
||||
for param in &node.params {
|
||||
let member_ty = self.lower_type_node(¶m.ty);
|
||||
match &member_ty {
|
||||
Type::Struct(sname) => {
|
||||
if let Some(s_slots) = self.get_builtin_struct_slots(sname) {
|
||||
slots += s_slots;
|
||||
} else if let Some(s_slots) = self.struct_slots.get(sname) {
|
||||
slots += s_slots;
|
||||
} else {
|
||||
all_known = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => slots += self.get_type_slots(&member_ty),
|
||||
}
|
||||
}
|
||||
if all_known {
|
||||
self.struct_slots.insert(name.clone(), slots);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for decl in &file.decls {
|
||||
if let Node::TypeDecl(n) = decl {
|
||||
let mut constants = HashMap::new();
|
||||
for c in &n.constants {
|
||||
constants.insert(c.name.clone(), *c.value.clone());
|
||||
}
|
||||
self.type_constants.insert(n.name.clone(), constants);
|
||||
|
||||
let mut ctors = HashMap::new();
|
||||
|
||||
// Default constructor: TypeName(...)
|
||||
if n.type_kind == "struct" {
|
||||
let mut params = Vec::new();
|
||||
let mut initializers = Vec::new();
|
||||
for p in &n.params {
|
||||
params.push(p.clone());
|
||||
initializers.push(Node::Ident(IdentNode {
|
||||
span: p.span,
|
||||
name: p.name.clone(),
|
||||
}));
|
||||
}
|
||||
let default_ctor = ConstructorDeclNode {
|
||||
span: n.span,
|
||||
params,
|
||||
initializers,
|
||||
name: n.name.clone(),
|
||||
body: Box::new(Node::Block(BlockNode {
|
||||
span: n.span,
|
||||
stmts: Vec::new(),
|
||||
tail: None,
|
||||
})),
|
||||
};
|
||||
ctors.insert(n.name.clone(), default_ctor);
|
||||
}
|
||||
|
||||
for ctor in &n.constructors {
|
||||
ctors.insert(ctor.name.clone(), ctor.clone());
|
||||
}
|
||||
self.struct_constructors.insert(n.name.clone(), ctors);
|
||||
}
|
||||
}
|
||||
|
||||
let mut module = Module {
|
||||
name: module_name.to_string(),
|
||||
functions: Vec::new(),
|
||||
@ -118,24 +203,32 @@ impl<'a> Lowerer<'a> {
|
||||
let func_id = *self.function_ids.get(&n.name).unwrap();
|
||||
self.next_block_id = 0;
|
||||
self.local_vars = vec![HashMap::new()];
|
||||
self.max_slots_used = 0;
|
||||
|
||||
let mut params = Vec::new();
|
||||
let mut local_types = HashMap::new();
|
||||
for (i, param) in n.params.iter().enumerate() {
|
||||
let mut param_slots = 0u32;
|
||||
for param in &n.params {
|
||||
let ty = self.lower_type_node(¶m.ty);
|
||||
let slots = self.get_type_slots(&ty);
|
||||
params.push(Param {
|
||||
name: param.name.clone(),
|
||||
ty: ty.clone(),
|
||||
});
|
||||
self.local_vars[0].insert(param.name.clone(), LocalInfo { slot: i as u32, ty: ty.clone() });
|
||||
local_types.insert(i as u32, ty);
|
||||
self.local_vars[0].insert(param.name.clone(), LocalInfo { slot: param_slots, ty: ty.clone() });
|
||||
for i in 0..slots {
|
||||
local_types.insert(param_slots + i, ty.clone());
|
||||
}
|
||||
param_slots += slots;
|
||||
}
|
||||
self.max_slots_used = param_slots;
|
||||
|
||||
let ret_ty = if let Some(ret) = &n.ret {
|
||||
self.lower_type_node(ret)
|
||||
} else {
|
||||
Type::Void
|
||||
};
|
||||
let return_slots = self.get_type_slots(&ret_ty);
|
||||
|
||||
let func = Function {
|
||||
id: func_id,
|
||||
@ -144,6 +237,9 @@ impl<'a> Lowerer<'a> {
|
||||
return_type: ret_ty,
|
||||
blocks: Vec::new(),
|
||||
local_types,
|
||||
param_slots: param_slots as u16,
|
||||
local_slots: 0,
|
||||
return_slots: return_slots as u16,
|
||||
};
|
||||
|
||||
self.current_function = Some(func);
|
||||
@ -160,7 +256,9 @@ impl<'a> Lowerer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.current_function.take().unwrap())
|
||||
let mut final_func = self.current_function.take().unwrap();
|
||||
final_func.local_slots = (self.max_slots_used - param_slots) as u16;
|
||||
Ok(final_func)
|
||||
}
|
||||
|
||||
fn lower_node(&mut self, node: &Node) -> Result<(), ()> {
|
||||
@ -258,8 +356,7 @@ impl<'a> Lowerer<'a> {
|
||||
self.lower_node(&n.target)?;
|
||||
|
||||
// 2. Preserve gate identity
|
||||
let gate_slot = self.get_next_local_slot();
|
||||
self.local_vars.last_mut().unwrap().insert(format!("$gate_{}", gate_slot), LocalInfo { slot: gate_slot, ty: Type::Int });
|
||||
let gate_slot = self.add_local_to_scope(format!("$gate_{}", self.get_next_local_slot()), Type::Int);
|
||||
self.emit(Instr::SetLocal(gate_slot));
|
||||
|
||||
// 3. Begin Operation
|
||||
@ -268,8 +365,7 @@ impl<'a> Lowerer<'a> {
|
||||
|
||||
// 4. Bind view to local
|
||||
self.local_vars.push(HashMap::new());
|
||||
let view_slot = self.get_next_local_slot();
|
||||
self.local_vars.last_mut().unwrap().insert(n.binding.to_string(), LocalInfo { slot: view_slot, ty: Type::Int });
|
||||
let view_slot = self.add_local_to_scope(n.binding.to_string(), Type::Int);
|
||||
self.emit(Instr::SetLocal(view_slot));
|
||||
|
||||
// 5. Body
|
||||
@ -287,8 +383,7 @@ impl<'a> Lowerer<'a> {
|
||||
self.lower_node(&n.target)?;
|
||||
|
||||
// 2. Preserve gate identity
|
||||
let gate_slot = self.get_next_local_slot();
|
||||
self.local_vars.last_mut().unwrap().insert(format!("$gate_{}", gate_slot), LocalInfo { slot: gate_slot, ty: Type::Int });
|
||||
let gate_slot = self.add_local_to_scope(format!("$gate_{}", self.get_next_local_slot()), Type::Int);
|
||||
self.emit(Instr::SetLocal(gate_slot));
|
||||
|
||||
// 3. Begin Operation
|
||||
@ -297,8 +392,7 @@ impl<'a> Lowerer<'a> {
|
||||
|
||||
// 4. Bind view to local
|
||||
self.local_vars.push(HashMap::new());
|
||||
let view_slot = self.get_next_local_slot();
|
||||
self.local_vars.last_mut().unwrap().insert(n.binding.to_string(), LocalInfo { slot: view_slot, ty: Type::Int });
|
||||
let view_slot = self.add_local_to_scope(n.binding.to_string(), Type::Int);
|
||||
self.emit(Instr::SetLocal(view_slot));
|
||||
|
||||
// 5. Body
|
||||
@ -316,8 +410,7 @@ impl<'a> Lowerer<'a> {
|
||||
self.lower_node(&n.target)?;
|
||||
|
||||
// 2. Preserve gate identity
|
||||
let gate_slot = self.get_next_local_slot();
|
||||
self.local_vars.last_mut().unwrap().insert(format!("$gate_{}", gate_slot), LocalInfo { slot: gate_slot, ty: Type::Int });
|
||||
let gate_slot = self.add_local_to_scope(format!("$gate_{}", self.get_next_local_slot()), Type::Int);
|
||||
self.emit(Instr::SetLocal(gate_slot));
|
||||
|
||||
// 3. Begin Operation
|
||||
@ -326,8 +419,7 @@ impl<'a> Lowerer<'a> {
|
||||
|
||||
// 4. Bind view to local
|
||||
self.local_vars.push(HashMap::new());
|
||||
let view_slot = self.get_next_local_slot();
|
||||
self.local_vars.last_mut().unwrap().insert(n.binding.to_string(), LocalInfo { slot: view_slot, ty: Type::Int });
|
||||
let view_slot = self.add_local_to_scope(n.binding.to_string(), Type::Int);
|
||||
self.emit(Instr::SetLocal(view_slot));
|
||||
|
||||
// 5. Body
|
||||
@ -372,10 +464,8 @@ impl<'a> Lowerer<'a> {
|
||||
} else { Type::Int }
|
||||
};
|
||||
|
||||
let slot = self.get_next_local_slot();
|
||||
let slots = self.get_type_slots(&ty);
|
||||
|
||||
self.local_vars.last_mut().unwrap().insert(n.name.clone(), LocalInfo { slot, ty });
|
||||
let slot = self.add_local_to_scope(n.name.clone(), ty);
|
||||
|
||||
for i in (0..slots).rev() {
|
||||
self.emit(Instr::SetLocal(slot + i));
|
||||
@ -434,6 +524,15 @@ impl<'a> Lowerer<'a> {
|
||||
|
||||
fn lower_member_access(&mut self, n: &MemberAccessNode) -> Result<(), ()> {
|
||||
if let Node::Ident(id) = &*n.object {
|
||||
if let Some(constants) = self.type_constants.get(&id.name).cloned() {
|
||||
if let Some(const_val) = constants.get(&n.member) {
|
||||
let old_ctx = self.current_type_context.replace(id.name.clone());
|
||||
let res = self.lower_node(const_val);
|
||||
self.current_type_context = old_ctx;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
if id.name == "Color" {
|
||||
let val = match n.member.as_str() {
|
||||
"BLACK" => 0x0000,
|
||||
@ -539,6 +638,25 @@ impl<'a> Lowerer<'a> {
|
||||
fn lower_call(&mut self, n: &CallNode) -> Result<(), ()> {
|
||||
match &*n.callee {
|
||||
Node::Ident(id_node) => {
|
||||
// 1. Check for constructor call: TypeName(...)
|
||||
let ctor = self.struct_constructors.get(&id_node.name)
|
||||
.and_then(|ctors| ctors.get(&id_node.name))
|
||||
.cloned();
|
||||
|
||||
if let Some(ctor) = ctor {
|
||||
return self.lower_constructor_call(&ctor, &n.args);
|
||||
}
|
||||
|
||||
if let Some(ctx) = &self.current_type_context {
|
||||
let ctor = self.struct_constructors.get(ctx)
|
||||
.and_then(|ctors| ctors.get(&id_node.name))
|
||||
.cloned();
|
||||
|
||||
if let Some(ctor) = ctor {
|
||||
return self.lower_constructor_call(&ctor, &n.args);
|
||||
}
|
||||
}
|
||||
|
||||
for arg in &n.args {
|
||||
self.lower_node(arg)?;
|
||||
}
|
||||
@ -559,6 +677,19 @@ impl<'a> Lowerer<'a> {
|
||||
}
|
||||
}
|
||||
Node::MemberAccess(ma) => {
|
||||
// Check if it's a constructor alias: TypeName.Alias(...)
|
||||
let ctor = if let Node::Ident(obj_id) = &*ma.object {
|
||||
self.struct_constructors.get(&obj_id.name)
|
||||
.and_then(|ctors| ctors.get(&ma.member))
|
||||
.cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(ctor) = ctor {
|
||||
return self.lower_constructor_call(&ctor, &n.args);
|
||||
}
|
||||
|
||||
// Check for Pad.any()
|
||||
if ma.member == "any" {
|
||||
if let Node::Ident(obj_id) = &*ma.object {
|
||||
@ -647,6 +778,56 @@ impl<'a> Lowerer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn lower_constructor_call(&mut self, ctor: &ConstructorDeclNode, args: &[Node]) -> Result<(), ()> {
|
||||
let mut param_map = HashMap::new();
|
||||
for (i, param) in ctor.params.iter().enumerate() {
|
||||
if i < args.len() {
|
||||
param_map.insert(param.name.clone(), args[i].clone());
|
||||
}
|
||||
}
|
||||
|
||||
for init in &ctor.initializers {
|
||||
let substituted = self.substitute_node(init, ¶m_map);
|
||||
self.lower_node(&substituted)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn substitute_node(&self, node: &Node, param_map: &HashMap<String, Node>) -> Node {
|
||||
match node {
|
||||
Node::Ident(id) => {
|
||||
if let Some(arg) = param_map.get(&id.name) {
|
||||
arg.clone()
|
||||
} else {
|
||||
node.clone()
|
||||
}
|
||||
}
|
||||
Node::Binary(bin) => {
|
||||
Node::Binary(BinaryNode {
|
||||
span: bin.span,
|
||||
left: Box::new(self.substitute_node(&bin.left, param_map)),
|
||||
right: Box::new(self.substitute_node(&bin.right, param_map)),
|
||||
op: bin.op.clone(),
|
||||
})
|
||||
}
|
||||
Node::Unary(un) => {
|
||||
Node::Unary(UnaryNode {
|
||||
span: un.span,
|
||||
op: un.op.clone(),
|
||||
expr: Box::new(self.substitute_node(&un.expr, param_map)),
|
||||
})
|
||||
}
|
||||
Node::Call(call) => {
|
||||
Node::Call(CallNode {
|
||||
span: call.span,
|
||||
callee: Box::new(self.substitute_node(&call.callee, param_map)),
|
||||
args: call.args.iter().map(|a| self.substitute_node(a, param_map)).collect(),
|
||||
})
|
||||
}
|
||||
_ => node.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn lower_pad_any(&mut self, base_slot: u32) {
|
||||
for i in 0..12 {
|
||||
let btn_base = base_slot + (i * 4);
|
||||
@ -808,6 +989,24 @@ impl<'a> Lowerer<'a> {
|
||||
self.local_vars.iter().flat_map(|s| s.values()).map(|info| self.get_type_slots(&info.ty)).sum()
|
||||
}
|
||||
|
||||
fn add_local_to_scope(&mut self, name: String, ty: Type) -> u32 {
|
||||
let slot = self.get_next_local_slot();
|
||||
let slots = self.get_type_slots(&ty);
|
||||
|
||||
if slot + slots > self.max_slots_used {
|
||||
self.max_slots_used = slot + slots;
|
||||
}
|
||||
|
||||
self.local_vars.last_mut().unwrap().insert(name, LocalInfo { slot, ty: ty.clone() });
|
||||
|
||||
if let Some(func) = &mut self.current_function {
|
||||
for i in 0..slots {
|
||||
func.local_types.insert(slot + i, ty.clone());
|
||||
}
|
||||
}
|
||||
slot
|
||||
}
|
||||
|
||||
fn find_local(&self, name: &str) -> Option<LocalInfo> {
|
||||
for scope in self.local_vars.iter().rev() {
|
||||
if let Some(info) = scope.get(name) {
|
||||
@ -817,10 +1016,26 @@ impl<'a> Lowerer<'a> {
|
||||
None
|
||||
}
|
||||
|
||||
fn get_builtin_struct_slots(&self, name: &str) -> Option<u32> {
|
||||
match name {
|
||||
"Pad" => Some(48),
|
||||
"ButtonState" => Some(4),
|
||||
"Color" => Some(1),
|
||||
"Touch" => Some(6),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_type_slots(&self, ty: &Type) -> u32 {
|
||||
match ty {
|
||||
Type::Void => 0,
|
||||
Type::Struct(name) => self.struct_slots.get(name).cloned().unwrap_or(1),
|
||||
Type::Struct(name) => {
|
||||
if let Some(slots) = self.get_builtin_struct_slots(name) {
|
||||
slots
|
||||
} else {
|
||||
self.struct_slots.get(name).cloned().unwrap_or(1)
|
||||
}
|
||||
}
|
||||
Type::Array(_, size) => *size,
|
||||
_ => 1,
|
||||
}
|
||||
@ -1127,11 +1342,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_alloc_struct_slots() {
|
||||
let code = "
|
||||
declare struct Vec3 {
|
||||
x: int,
|
||||
y: int,
|
||||
z: int
|
||||
}
|
||||
declare struct Vec3(x: int, y: int, z: int)
|
||||
fn main() {
|
||||
let v = alloc Vec3;
|
||||
}
|
||||
|
||||
@ -225,49 +225,107 @@ impl Parser {
|
||||
};
|
||||
let name = self.expect_identifier()?;
|
||||
|
||||
let mut params = Vec::new();
|
||||
if self.peek().kind == TokenKind::OpenParen {
|
||||
params = self.parse_param_list()?;
|
||||
}
|
||||
|
||||
let mut constructors = Vec::new();
|
||||
if self.peek().kind == TokenKind::OpenBracket {
|
||||
constructors = self.parse_constructor_list()?;
|
||||
}
|
||||
|
||||
let mut is_host = false;
|
||||
if self.peek().kind == TokenKind::Host {
|
||||
self.advance();
|
||||
is_host = true;
|
||||
}
|
||||
|
||||
let body = self.parse_type_body()?;
|
||||
let mut constants = Vec::new();
|
||||
if self.peek().kind == TokenKind::OpenDoubleBracket {
|
||||
self.advance();
|
||||
while self.peek().kind != TokenKind::CloseDoubleBracket && self.peek().kind != TokenKind::Eof {
|
||||
let c_start = self.peek().span.start;
|
||||
let c_name = self.expect_identifier()?;
|
||||
self.consume(TokenKind::Colon)?;
|
||||
let c_value = self.parse_expr(0)?;
|
||||
constants.push(ConstantDeclNode {
|
||||
span: Span::new(self.file_id, c_start, c_value.span().end),
|
||||
name: c_name,
|
||||
value: Box::new(c_value),
|
||||
});
|
||||
if self.peek().kind == TokenKind::Comma {
|
||||
self.advance();
|
||||
}
|
||||
}
|
||||
self.consume(TokenKind::CloseDoubleBracket)?;
|
||||
}
|
||||
|
||||
let mut body = None;
|
||||
if self.peek().kind == TokenKind::OpenBrace {
|
||||
body = Some(Box::new(self.parse_type_body()?));
|
||||
}
|
||||
|
||||
let body_span = body.span();
|
||||
let mut end_pos = start_span.end;
|
||||
if let Some(b) = &body {
|
||||
end_pos = b.span().end;
|
||||
} else if !constants.is_empty() {
|
||||
// We should use the CloseDoubleBracket span here, but I don't have it easily
|
||||
// Let's just use the last constant's end
|
||||
end_pos = constants.last().unwrap().span.end;
|
||||
} else if !params.is_empty() {
|
||||
end_pos = params.last().unwrap().span.end;
|
||||
}
|
||||
|
||||
Ok(Node::TypeDecl(TypeDeclNode {
|
||||
span: Span::new(self.file_id, start_span.start, body_span.end),
|
||||
span: Span::new(self.file_id, start_span.start, end_pos),
|
||||
vis,
|
||||
type_kind,
|
||||
name,
|
||||
is_host,
|
||||
body: Box::new(body),
|
||||
params,
|
||||
constructors,
|
||||
constants,
|
||||
body,
|
||||
}))
|
||||
}
|
||||
|
||||
fn parse_type_body(&mut self) -> Result<Node, DiagnosticBundle> {
|
||||
let start_span = self.consume(TokenKind::OpenBrace)?.span;
|
||||
let mut members = Vec::new();
|
||||
let mut methods = Vec::new();
|
||||
while self.peek().kind != TokenKind::CloseBrace && self.peek().kind != TokenKind::Eof {
|
||||
let m_start = self.peek().span.start;
|
||||
let name = self.expect_identifier()?;
|
||||
self.consume(TokenKind::Colon)?;
|
||||
let ty = self.parse_type_ref()?;
|
||||
let m_end = ty.span().end;
|
||||
members.push(TypeMemberNode {
|
||||
span: Span::new(self.file_id, m_start, m_end),
|
||||
name,
|
||||
ty: Box::new(ty)
|
||||
});
|
||||
if self.peek().kind == TokenKind::Comma {
|
||||
self.advance();
|
||||
if self.peek().kind == TokenKind::Fn {
|
||||
let sig_node = self.parse_service_member()?;
|
||||
if let Node::ServiceFnSig(sig) = sig_node {
|
||||
methods.push(sig);
|
||||
}
|
||||
if self.peek().kind == TokenKind::Semicolon {
|
||||
self.advance();
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
let m_start = self.peek().span.start;
|
||||
let name = self.expect_identifier()?;
|
||||
self.consume(TokenKind::Colon)?;
|
||||
let ty = self.parse_type_ref()?;
|
||||
let m_end = ty.span().end;
|
||||
members.push(TypeMemberNode {
|
||||
span: Span::new(self.file_id, m_start, m_end),
|
||||
name,
|
||||
ty: Box::new(ty)
|
||||
});
|
||||
if self.peek().kind == TokenKind::Comma {
|
||||
self.advance();
|
||||
} else if self.peek().kind == TokenKind::Semicolon {
|
||||
self.advance();
|
||||
}
|
||||
}
|
||||
}
|
||||
let end_span = self.consume(TokenKind::CloseBrace)?.span;
|
||||
Ok(Node::TypeBody(TypeBodyNode {
|
||||
span: Span::new(self.file_id, start_span.start, end_span.end),
|
||||
members,
|
||||
methods,
|
||||
}))
|
||||
}
|
||||
|
||||
@ -344,6 +402,22 @@ impl Parser {
|
||||
self.advance();
|
||||
"bounded".to_string()
|
||||
}
|
||||
TokenKind::None => {
|
||||
self.advance();
|
||||
"none".to_string()
|
||||
}
|
||||
TokenKind::Some => {
|
||||
self.advance();
|
||||
"some".to_string()
|
||||
}
|
||||
TokenKind::Ok => {
|
||||
self.advance();
|
||||
"ok".to_string()
|
||||
}
|
||||
TokenKind::Err => {
|
||||
self.advance();
|
||||
"err".to_string()
|
||||
}
|
||||
_ => return Err(self.error_with_code("Expected type name", Some("E_PARSE_EXPECTED_TOKEN"))),
|
||||
};
|
||||
let mut node = if self.peek().kind == TokenKind::Lt {
|
||||
@ -819,6 +893,10 @@ impl Parser {
|
||||
self.advance();
|
||||
Ok("err".to_string())
|
||||
}
|
||||
TokenKind::Bounded => {
|
||||
self.advance();
|
||||
Ok("bounded".to_string())
|
||||
}
|
||||
TokenKind::Invalid(msg) => {
|
||||
let code = if msg.contains("Unterminated string") {
|
||||
"E_LEX_UNTERMINATED_STRING"
|
||||
@ -827,7 +905,7 @@ impl Parser {
|
||||
};
|
||||
Err(self.error_with_code(&msg, Some(code)))
|
||||
}
|
||||
_ => Err(self.error_with_code("Expected identifier", Some("E_PARSE_EXPECTED_TOKEN"))),
|
||||
_ => Err(self.error_with_code(&format!("Expected identifier, found {:?}", peeked_kind), Some("E_PARSE_EXPECTED_TOKEN"))),
|
||||
}
|
||||
}
|
||||
|
||||
@ -845,6 +923,45 @@ impl Parser {
|
||||
self.errors.push(diag.clone());
|
||||
DiagnosticBundle::from(diag)
|
||||
}
|
||||
|
||||
fn parse_constructor_list(&mut self) -> Result<Vec<ConstructorDeclNode>, DiagnosticBundle> {
|
||||
self.consume(TokenKind::OpenBracket)?;
|
||||
let mut constructors = Vec::new();
|
||||
while self.peek().kind != TokenKind::CloseBracket && self.peek().kind != TokenKind::Eof {
|
||||
let start_span = self.peek().span;
|
||||
let params = self.parse_param_list()?;
|
||||
self.consume(TokenKind::Colon)?;
|
||||
|
||||
let mut initializers = Vec::new();
|
||||
if self.peek().kind == TokenKind::OpenParen {
|
||||
self.advance();
|
||||
while self.peek().kind != TokenKind::CloseParen && self.peek().kind != TokenKind::Eof {
|
||||
initializers.push(self.parse_expr(0)?);
|
||||
if self.peek().kind == TokenKind::Comma {
|
||||
self.advance();
|
||||
}
|
||||
}
|
||||
self.consume(TokenKind::CloseParen)?;
|
||||
} else {
|
||||
initializers.push(self.parse_expr(0)?);
|
||||
}
|
||||
|
||||
self.consume(TokenKind::As)?;
|
||||
let name = self.expect_identifier()?;
|
||||
|
||||
let body = self.parse_block()?;
|
||||
|
||||
constructors.push(ConstructorDeclNode {
|
||||
span: Span::new(self.file_id, start_span.start, body.span().end),
|
||||
params,
|
||||
initializers,
|
||||
name,
|
||||
body: Box::new(body),
|
||||
});
|
||||
}
|
||||
self.consume(TokenKind::CloseBracket)?;
|
||||
Ok(constructors)
|
||||
}
|
||||
}
|
||||
|
||||
impl Node {
|
||||
@ -876,6 +993,8 @@ impl Node {
|
||||
Node::WhenArm(n) => n.span,
|
||||
Node::TypeName(n) => n.span,
|
||||
Node::TypeApp(n) => n.span,
|
||||
Node::ConstructorDecl(n) => n.span,
|
||||
Node::ConstantDecl(n) => n.span,
|
||||
Node::Alloc(n) => n.span,
|
||||
Node::Mutate(n) => n.span,
|
||||
Node::Borrow(n) => n.span,
|
||||
|
||||
@ -146,6 +146,8 @@ impl<'a> Resolver<'a> {
|
||||
self.resolve_type_ref(arg);
|
||||
}
|
||||
}
|
||||
Node::ConstructorDecl(n) => self.resolve_constructor_decl(n),
|
||||
Node::ConstantDecl(n) => self.resolve_node(&n.value),
|
||||
Node::Alloc(n) => {
|
||||
self.resolve_type_ref(&n.ty);
|
||||
}
|
||||
@ -217,13 +219,48 @@ impl<'a> Resolver<'a> {
|
||||
}
|
||||
|
||||
fn resolve_type_decl(&mut self, n: &TypeDeclNode) {
|
||||
if let Node::TypeBody(body) = &*n.body {
|
||||
for member in &body.members {
|
||||
self.resolve_type_ref(&member.ty);
|
||||
for param in &n.params {
|
||||
self.resolve_type_ref(¶m.ty);
|
||||
}
|
||||
for constructor in &n.constructors {
|
||||
self.resolve_constructor_decl(constructor);
|
||||
}
|
||||
self.enter_scope();
|
||||
for ctor in &n.constructors {
|
||||
self.define_local(&ctor.name, ctor.span, SymbolKind::Local);
|
||||
}
|
||||
for constant in &n.constants {
|
||||
self.resolve_node(&constant.value);
|
||||
}
|
||||
self.exit_scope();
|
||||
if let Some(body_node) = &n.body {
|
||||
if let Node::TypeBody(body) = &**body_node {
|
||||
for member in &body.members {
|
||||
self.resolve_type_ref(&member.ty);
|
||||
}
|
||||
for method in &body.methods {
|
||||
for param in &method.params {
|
||||
self.resolve_type_ref(¶m.ty);
|
||||
}
|
||||
self.resolve_type_ref(&method.ret);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_constructor_decl(&mut self, n: &ConstructorDeclNode) {
|
||||
self.enter_scope();
|
||||
for param in &n.params {
|
||||
self.resolve_type_ref(¶m.ty);
|
||||
self.define_local(¶m.name, param.span, SymbolKind::Local);
|
||||
}
|
||||
for init in &n.initializers {
|
||||
self.resolve_node(init);
|
||||
}
|
||||
self.resolve_node(&n.body);
|
||||
self.exit_scope();
|
||||
}
|
||||
|
||||
fn resolve_block(&mut self, n: &BlockNode) {
|
||||
self.enter_scope();
|
||||
for stmt in &n.stmts {
|
||||
@ -311,6 +348,20 @@ impl<'a> Resolver<'a> {
|
||||
return Some(sym.clone());
|
||||
}
|
||||
|
||||
// 5. Fallback for constructor calls: check Type namespace if looking for a Value
|
||||
if namespace == Namespace::Value {
|
||||
if let Some(sym) = self.current_module.type_symbols.get(name) {
|
||||
if sym.kind == SymbolKind::Struct {
|
||||
return Some(sym.clone());
|
||||
}
|
||||
}
|
||||
if let Some(sym) = self.imported_symbols.type_symbols.get(name) {
|
||||
if sym.kind == SymbolKind::Struct {
|
||||
return Some(sym.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
@ -52,6 +52,8 @@ pub enum TokenKind {
|
||||
CloseBrace, // }
|
||||
OpenBracket, // [
|
||||
CloseBracket, // ]
|
||||
OpenDoubleBracket, // [[
|
||||
CloseDoubleBracket, // ]]
|
||||
Comma, // ,
|
||||
Dot, // .
|
||||
Colon, // :
|
||||
|
||||
@ -13,6 +13,9 @@ pub struct TypeChecker<'a> {
|
||||
scopes: Vec<HashMap<String, PbsType>>,
|
||||
mut_bindings: Vec<HashMap<String, bool>>,
|
||||
current_return_type: Option<PbsType>,
|
||||
struct_constructors: HashMap<String, HashMap<String, PbsType>>,
|
||||
struct_constants: HashMap<String, HashMap<String, PbsType>>,
|
||||
struct_methods: HashMap<String, HashMap<String, PbsType>>,
|
||||
diagnostics: Vec<Diagnostic>,
|
||||
contract_registry: ContractRegistry,
|
||||
}
|
||||
@ -28,6 +31,9 @@ impl<'a> TypeChecker<'a> {
|
||||
scopes: Vec::new(),
|
||||
mut_bindings: Vec::new(),
|
||||
current_return_type: None,
|
||||
struct_constructors: HashMap::new(),
|
||||
struct_constants: HashMap::new(),
|
||||
struct_methods: HashMap::new(),
|
||||
diagnostics: Vec::new(),
|
||||
contract_registry: ContractRegistry::new(),
|
||||
}
|
||||
@ -86,8 +92,62 @@ impl<'a> TypeChecker<'a> {
|
||||
_ => PbsType::Void,
|
||||
};
|
||||
if let Some(sym) = self.module_symbols.type_symbols.symbols.get_mut(&n.name) {
|
||||
sym.ty = Some(ty);
|
||||
sym.ty = Some(ty.clone());
|
||||
}
|
||||
|
||||
// Resolve constructors
|
||||
let mut ctors = HashMap::new();
|
||||
|
||||
// Default constructor: TypeName(...)
|
||||
if n.type_kind == "struct" {
|
||||
let mut params = Vec::new();
|
||||
let mut initializers = Vec::new();
|
||||
for p in &n.params {
|
||||
let p_ty = self.resolve_type_node(&p.ty);
|
||||
params.push(p_ty);
|
||||
initializers.push(Node::Ident(IdentNode {
|
||||
span: p.span,
|
||||
name: p.name.clone(),
|
||||
}));
|
||||
}
|
||||
let default_ctor_ty = PbsType::Function {
|
||||
params: params.clone(),
|
||||
return_type: Box::new(ty.clone()),
|
||||
};
|
||||
ctors.insert(n.name.clone(), default_ctor_ty);
|
||||
}
|
||||
|
||||
for ctor in &n.constructors {
|
||||
let mut params = Vec::new();
|
||||
for p in &ctor.params {
|
||||
params.push(self.resolve_type_node(&p.ty));
|
||||
}
|
||||
let ctor_ty = PbsType::Function {
|
||||
params,
|
||||
return_type: Box::new(ty.clone()),
|
||||
};
|
||||
ctors.insert(ctor.name.clone(), ctor_ty);
|
||||
}
|
||||
self.struct_constructors.insert(n.name.clone(), ctors);
|
||||
|
||||
// Resolve methods
|
||||
let mut methods = HashMap::new();
|
||||
if let Some(body_node) = &n.body {
|
||||
if let Node::TypeBody(body) = &**body_node {
|
||||
for m in &body.methods {
|
||||
let mut params = Vec::new();
|
||||
for p in &m.params {
|
||||
params.push(self.resolve_type_node(&p.ty));
|
||||
}
|
||||
let m_ty = PbsType::Function {
|
||||
params,
|
||||
return_type: Box::new(self.resolve_type_node(&m.ret)),
|
||||
};
|
||||
methods.insert(m.name.clone(), m_ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.struct_methods.insert(n.name.clone(), methods);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@ -100,6 +160,15 @@ impl<'a> TypeChecker<'a> {
|
||||
self.check_fn_decl(n);
|
||||
PbsType::Void
|
||||
}
|
||||
Node::TypeDecl(n) => {
|
||||
self.check_type_decl(n);
|
||||
PbsType::Void
|
||||
}
|
||||
Node::ConstructorDecl(n) => {
|
||||
self.check_constructor_decl(n);
|
||||
PbsType::Void
|
||||
}
|
||||
Node::ConstantDecl(n) => self.check_node(&n.value),
|
||||
Node::Block(n) => self.check_block(n),
|
||||
Node::LetStmt(n) => {
|
||||
self.check_let_stmt(n);
|
||||
@ -166,26 +235,45 @@ impl<'a> TypeChecker<'a> {
|
||||
}
|
||||
|
||||
// Builtin Struct Associated Members (Static/Constants)
|
||||
match id.name.as_str() {
|
||||
"Color" => {
|
||||
match n.member.as_str() {
|
||||
"BLACK" | "WHITE" | "RED" | "GREEN" | "BLUE" | "MAGENTA" | "TRANSPARENT" | "COLOR_KEY" => {
|
||||
return PbsType::Struct("Color".to_string());
|
||||
}
|
||||
"rgb" => {
|
||||
return PbsType::Function {
|
||||
params: vec![PbsType::Int, PbsType::Int, PbsType::Int],
|
||||
return_type: Box::new(PbsType::Struct("Color".to_string())),
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some(constants) = self.struct_constants.get(&id.name) {
|
||||
if let Some(ty) = constants.get(&n.member) {
|
||||
return ty.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for constructors if used as Type.alias(...)
|
||||
if let Some(ctors) = self.struct_constructors.get(&id.name) {
|
||||
if let Some(ty) = ctors.get(&n.member) {
|
||||
return ty.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for static methods if used as Type.method(...)
|
||||
if let Some(methods) = self.struct_methods.get(&id.name) {
|
||||
if let Some(ty) = methods.get(&n.member) {
|
||||
return ty.clone();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let obj_ty = self.check_node(&n.object);
|
||||
if let PbsType::Struct(ref name) = obj_ty {
|
||||
if let Some(methods) = self.struct_methods.get(name) {
|
||||
if let Some(ty) = methods.get(&n.member) {
|
||||
// If it's a method call on an instance, the first parameter (self) is implicit
|
||||
if let PbsType::Function { mut params, return_type } = ty.clone() {
|
||||
if !params.is_empty() {
|
||||
// Check if first param is the struct itself (simple heuristic for self)
|
||||
// In a real compiler we'd check the parameter name or a flag
|
||||
params.remove(0);
|
||||
return PbsType::Function { params, return_type };
|
||||
}
|
||||
}
|
||||
return ty.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match obj_ty {
|
||||
PbsType::Struct(ref name) => {
|
||||
match name.as_str() {
|
||||
@ -234,10 +322,11 @@ impl<'a> TypeChecker<'a> {
|
||||
}
|
||||
|
||||
if obj_ty != PbsType::Void {
|
||||
let msg = format!("Member '{}' not found on type {:?}", n.member, obj_ty);
|
||||
self.diagnostics.push(Diagnostic {
|
||||
level: DiagnosticLevel::Error,
|
||||
code: Some("E_RESOLVE_UNDEFINED".to_string()),
|
||||
message: format!("Member '{}' not found on type {}", n.member, obj_ty),
|
||||
message: msg,
|
||||
span: Some(n.span),
|
||||
});
|
||||
}
|
||||
@ -360,6 +449,13 @@ impl<'a> TypeChecker<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for default constructor: check if it's a struct name
|
||||
if let Some(ctors) = self.struct_constructors.get(&n.name) {
|
||||
if let Some(ty) = ctors.get(&n.name) {
|
||||
return ty.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Built-ins (some, none, ok, err might be handled as calls or special keywords)
|
||||
// For v0, let's treat none as a special literal or identifier
|
||||
if n.name == "none" {
|
||||
@ -544,6 +640,49 @@ impl<'a> TypeChecker<'a> {
|
||||
first_ty.unwrap_or(PbsType::Void)
|
||||
}
|
||||
|
||||
fn check_type_decl(&mut self, n: &TypeDeclNode) {
|
||||
for constructor in &n.constructors {
|
||||
self.check_constructor_decl(constructor);
|
||||
}
|
||||
|
||||
let struct_ty = PbsType::Struct(n.name.clone());
|
||||
let mut constants_scope = HashMap::new();
|
||||
if let Some(ctors) = self.struct_constructors.get(&n.name) {
|
||||
for (name, ty) in ctors {
|
||||
constants_scope.insert(name.clone(), ty.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut constants_map = HashMap::new();
|
||||
self.scopes.push(constants_scope);
|
||||
for constant in &n.constants {
|
||||
let val_ty = self.check_node(&constant.value);
|
||||
if !self.is_assignable(&struct_ty, &val_ty) {
|
||||
self.error_type_mismatch(&struct_ty, &val_ty, constant.span);
|
||||
}
|
||||
constants_map.insert(constant.name.clone(), struct_ty.clone());
|
||||
}
|
||||
self.scopes.pop();
|
||||
self.struct_constants.insert(n.name.clone(), constants_map);
|
||||
|
||||
if let Some(body) = &n.body {
|
||||
self.check_node(body);
|
||||
}
|
||||
}
|
||||
|
||||
fn check_constructor_decl(&mut self, n: &ConstructorDeclNode) {
|
||||
self.enter_scope();
|
||||
for param in &n.params {
|
||||
let ty = self.resolve_type_node(¶m.ty);
|
||||
self.define_local(¶m.name, ty, false);
|
||||
}
|
||||
for init in &n.initializers {
|
||||
self.check_node(init);
|
||||
}
|
||||
self.check_node(&n.body);
|
||||
self.exit_scope();
|
||||
}
|
||||
|
||||
fn resolve_type_node(&mut self, node: &Node) -> PbsType {
|
||||
match node {
|
||||
Node::TypeName(tn) => {
|
||||
@ -705,7 +844,21 @@ mod tests {
|
||||
let mut file_manager = FileManager::new();
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let file_path = temp_dir.path().join("test.pbs");
|
||||
fs::write(&file_path, code).unwrap();
|
||||
|
||||
// Inject industrial base definitions for tests
|
||||
let mut full_code = String::new();
|
||||
if !code.contains("struct Color") {
|
||||
full_code.push_str("declare struct Color(raw: bounded) [[ BLACK: Color(0b), WHITE: Color(65535b), RED: Color(63488b), GREEN: Color(2016b), BLUE: Color(31b) ]] { fn raw(self: Color): bounded; fn rgb(r: int, g: int, b: int): Color; } \n");
|
||||
}
|
||||
if !code.contains("struct ButtonState") {
|
||||
full_code.push_str("declare struct ButtonState(pressed: bool, released: bool, down: bool, hold_frames: bounded) \n");
|
||||
}
|
||||
if !code.contains("struct Pad") {
|
||||
full_code.push_str("declare struct Pad(up: ButtonState, down: ButtonState, left: ButtonState, right: ButtonState, a: ButtonState, b: ButtonState, x: ButtonState, y: ButtonState, l: ButtonState, r: ButtonState, start: ButtonState, select: ButtonState) { fn any(self: Pad): bool; } \n");
|
||||
}
|
||||
full_code.push_str(code);
|
||||
|
||||
fs::write(&file_path, full_code).unwrap();
|
||||
|
||||
let frontend = PbsFrontend;
|
||||
match frontend.compile_to_ir(&file_path, &mut file_manager) {
|
||||
|
||||
@ -21,4 +21,8 @@ pub struct Function {
|
||||
pub blocks: Vec<Block>,
|
||||
#[serde(default)]
|
||||
pub local_types: HashMap<u32, Type>,
|
||||
|
||||
pub param_slots: u16,
|
||||
pub local_slots: u16,
|
||||
pub return_slots: u16,
|
||||
}
|
||||
|
||||
@ -37,6 +37,9 @@ mod tests {
|
||||
functions: vec![Function {
|
||||
id: FunctionId(10),
|
||||
name: "entry".to_string(),
|
||||
param_slots: 0,
|
||||
local_slots: 0,
|
||||
return_slots: 0,
|
||||
params: vec![],
|
||||
return_type: Type::Void,
|
||||
blocks: vec![Block {
|
||||
@ -90,7 +93,10 @@ mod tests {
|
||||
"terminator": "Return"
|
||||
}
|
||||
],
|
||||
"local_types": {}
|
||||
"local_types": {},
|
||||
"param_slots": 0,
|
||||
"local_slots": 0,
|
||||
"return_slots": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -158,6 +158,9 @@ mod tests {
|
||||
Function {
|
||||
id: FunctionId(1),
|
||||
name: "test".to_string(),
|
||||
param_slots: 0,
|
||||
local_slots: 0,
|
||||
return_slots: 0,
|
||||
params: vec![],
|
||||
return_type: Type::Void,
|
||||
blocks,
|
||||
|
||||
@ -51,6 +51,9 @@ mod tests {
|
||||
functions: vec![Function {
|
||||
id: FunctionId(1),
|
||||
name: "main".to_string(),
|
||||
param_slots: 0,
|
||||
local_slots: 0,
|
||||
return_slots: 0,
|
||||
params: vec![],
|
||||
return_type: Type::Null,
|
||||
body: vec![
|
||||
@ -99,7 +102,10 @@ mod tests {
|
||||
"kind": "Ret",
|
||||
"span": null
|
||||
}
|
||||
]
|
||||
],
|
||||
"param_slots": 0,
|
||||
"local_slots": 0,
|
||||
"return_slots": 0
|
||||
}
|
||||
],
|
||||
"globals": []
|
||||
@ -122,6 +128,9 @@ mod tests {
|
||||
functions: vec![ir_core::Function {
|
||||
id: FunctionId(10),
|
||||
name: "start".to_string(),
|
||||
param_slots: 0,
|
||||
local_slots: 0,
|
||||
return_slots: 0,
|
||||
params: vec![],
|
||||
return_type: ir_core::Type::Void,
|
||||
blocks: vec![ir_core::Block {
|
||||
@ -146,7 +155,7 @@ mod tests {
|
||||
assert_eq!(func.name, "start");
|
||||
assert_eq!(func.id, FunctionId(10));
|
||||
|
||||
assert_eq!(func.body.len(), 4);
|
||||
assert_eq!(func.body.len(), 3);
|
||||
match &func.body[0].kind {
|
||||
InstrKind::Label(Label(l)) => assert!(l.contains("block_0")),
|
||||
_ => panic!("Expected label"),
|
||||
@ -156,10 +165,6 @@ mod tests {
|
||||
_ => panic!("Expected PushConst"),
|
||||
}
|
||||
match &func.body[2].kind {
|
||||
InstrKind::PushNull => (),
|
||||
_ => panic!("Expected PushNull"),
|
||||
}
|
||||
match &func.body[3].kind {
|
||||
InstrKind::Ret => (),
|
||||
_ => panic!("Expected Ret"),
|
||||
}
|
||||
|
||||
@ -40,6 +40,10 @@ pub struct Function {
|
||||
pub return_type: Type,
|
||||
/// The sequence of instructions that make up the function's logic.
|
||||
pub body: Vec<Instruction>,
|
||||
|
||||
pub param_slots: u16,
|
||||
pub local_slots: u16,
|
||||
pub return_slots: u16,
|
||||
}
|
||||
|
||||
/// A parameter passed to a function.
|
||||
|
||||
@ -52,6 +52,9 @@ pub fn lower_function(
|
||||
}).collect(),
|
||||
return_type: lower_type(&core_func.return_type),
|
||||
body: vec![],
|
||||
param_slots: core_func.param_slots,
|
||||
local_slots: core_func.local_slots,
|
||||
return_slots: core_func.return_slots,
|
||||
};
|
||||
|
||||
// Type tracking for RC insertion
|
||||
@ -320,12 +323,8 @@ pub fn lower_function(
|
||||
}
|
||||
}
|
||||
|
||||
// If the function is Void, we must push a Null value to satisfy the VM's RET instruction.
|
||||
// The VM always pops one value from the stack to be the return value.
|
||||
if vm_func.return_type == ir_vm::Type::Void {
|
||||
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::PushNull, None));
|
||||
}
|
||||
|
||||
// If the function is Void, we don't need to push anything.
|
||||
// The VM's Ret opcode handles zero return slots correctly.
|
||||
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Ret, None));
|
||||
}
|
||||
ir_core::Terminator::Jump(target) => {
|
||||
@ -424,6 +423,9 @@ mod tests {
|
||||
},
|
||||
],
|
||||
local_types: HashMap::new(),
|
||||
param_slots: 0,
|
||||
local_slots: 0,
|
||||
return_slots: 0,
|
||||
}],
|
||||
}],
|
||||
field_offsets: std::collections::HashMap::new(),
|
||||
@ -436,7 +438,7 @@ mod tests {
|
||||
let func = &vm_module.functions[0];
|
||||
assert_eq!(func.name, "main");
|
||||
|
||||
assert_eq!(func.body.len(), 8);
|
||||
assert_eq!(func.body.len(), 7);
|
||||
|
||||
match &func.body[0].kind {
|
||||
InstrKind::Label(Label(l)) => assert_eq!(l, "block_0"),
|
||||
@ -466,10 +468,6 @@ mod tests {
|
||||
_ => panic!("Expected HostCall 42"),
|
||||
}
|
||||
match &func.body[6].kind {
|
||||
InstrKind::PushNull => (),
|
||||
_ => panic!("Expected PushNull"),
|
||||
}
|
||||
match &func.body[7].kind {
|
||||
InstrKind::Ret => (),
|
||||
_ => panic!("Expected Ret"),
|
||||
}
|
||||
@ -500,6 +498,9 @@ mod tests {
|
||||
terminator: Terminator::Return,
|
||||
}],
|
||||
local_types: HashMap::new(),
|
||||
param_slots: 0,
|
||||
local_slots: 0,
|
||||
return_slots: 0,
|
||||
}],
|
||||
}],
|
||||
field_offsets,
|
||||
@ -518,7 +519,7 @@ mod tests {
|
||||
// GateStore 100 (offset)
|
||||
// Ret
|
||||
|
||||
assert_eq!(func.body.len(), 10);
|
||||
assert_eq!(func.body.len(), 9);
|
||||
match &func.body[1].kind {
|
||||
ir_vm::InstrKind::LocalLoad { slot } => assert_eq!(*slot, 0),
|
||||
_ => panic!("Expected LocalLoad 0"),
|
||||
@ -536,10 +537,6 @@ mod tests {
|
||||
_ => panic!("Expected GateStore 100"),
|
||||
}
|
||||
match &func.body[8].kind {
|
||||
ir_vm::InstrKind::PushNull => (),
|
||||
_ => panic!("Expected PushNull"),
|
||||
}
|
||||
match &func.body[9].kind {
|
||||
ir_vm::InstrKind::Ret => (),
|
||||
_ => panic!("Expected Ret"),
|
||||
}
|
||||
@ -564,6 +561,9 @@ mod tests {
|
||||
terminator: Terminator::Return,
|
||||
}],
|
||||
local_types: HashMap::new(),
|
||||
param_slots: 0,
|
||||
local_slots: 0,
|
||||
return_slots: 0,
|
||||
}],
|
||||
}],
|
||||
field_offsets: std::collections::HashMap::new(),
|
||||
@ -609,6 +609,9 @@ mod tests {
|
||||
terminator: Terminator::Return,
|
||||
}],
|
||||
local_types: HashMap::new(),
|
||||
param_slots: 0,
|
||||
local_slots: 0,
|
||||
return_slots: 0,
|
||||
}],
|
||||
}],
|
||||
field_offsets: HashMap::new(),
|
||||
@ -635,10 +638,10 @@ mod tests {
|
||||
assert!(found_overwrite, "Should have emitted release-then-store sequence for overwrite");
|
||||
|
||||
// Check Ret cleanup:
|
||||
// LocalLoad 1, GateRelease, PushNull, Ret
|
||||
// LocalLoad 1, GateRelease, Ret
|
||||
let mut found_cleanup = false;
|
||||
for i in 0..kinds.len() - 3 {
|
||||
if let (InstrKind::LocalLoad { slot: 1 }, InstrKind::GateRelease, InstrKind::PushNull, InstrKind::Ret) = (kinds[i], kinds[i+1], kinds[i+2], kinds[i+3]) {
|
||||
for i in 0..kinds.len() - 2 {
|
||||
if let (InstrKind::LocalLoad { slot: 1 }, InstrKind::GateRelease, InstrKind::Ret) = (kinds[i], kinds[i+1], kinds[i+2]) {
|
||||
found_cleanup = true;
|
||||
break;
|
||||
}
|
||||
@ -671,6 +674,9 @@ mod tests {
|
||||
terminator: Terminator::Return,
|
||||
}],
|
||||
local_types: HashMap::new(),
|
||||
param_slots: 0,
|
||||
local_slots: 0,
|
||||
return_slots: 0,
|
||||
}],
|
||||
}],
|
||||
field_offsets: HashMap::new(),
|
||||
|
||||
66
crates/prometeu-compiler/tests/generate_canonical_goldens.rs
Normal file
66
crates/prometeu-compiler/tests/generate_canonical_goldens.rs
Normal file
@ -0,0 +1,66 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use prometeu_compiler::compiler::compile;
|
||||
use prometeu_bytecode::v0::BytecodeLoader;
|
||||
use prometeu_bytecode::disasm::disasm;
|
||||
use prometeu_compiler::frontends::pbs::parser::Parser;
|
||||
use prometeu_compiler::frontends::pbs::ast::Node;
|
||||
|
||||
#[test]
|
||||
fn generate_canonical_goldens() {
|
||||
println!("CWD: {:?}", std::env::current_dir().unwrap());
|
||||
let project_dir = Path::new("../../test-cartridges/canonical");
|
||||
if !project_dir.exists() {
|
||||
// Fallback for when running from project root (some IDEs/environments)
|
||||
let project_dir = Path::new("test-cartridges/canonical");
|
||||
if !project_dir.exists() {
|
||||
panic!("Could not find project directory at ../../test-cartridges/canonical or test-cartridges/canonical");
|
||||
}
|
||||
}
|
||||
|
||||
// We need a stable path for the actual compilation which might use relative paths internally
|
||||
let project_dir = if Path::new("../../test-cartridges/canonical").exists() {
|
||||
Path::new("../../test-cartridges/canonical")
|
||||
} else {
|
||||
Path::new("test-cartridges/canonical")
|
||||
};
|
||||
|
||||
let unit = compile(project_dir).map_err(|e| {
|
||||
println!("Compilation Error: {}", e);
|
||||
e
|
||||
}).expect("Failed to compile canonical cartridge");
|
||||
|
||||
let golden_dir = project_dir.join("golden");
|
||||
fs::create_dir_all(&golden_dir).unwrap();
|
||||
|
||||
// 1. Bytecode (.pbc)
|
||||
fs::write(golden_dir.join("program.pbc"), &unit.rom).unwrap();
|
||||
|
||||
// 2. Disassembly
|
||||
let module = BytecodeLoader::load(&unit.rom).expect("Failed to load BytecodeModule");
|
||||
let instrs = disasm(&module.code).expect("Failed to disassemble");
|
||||
let mut disasm_text = String::new();
|
||||
for instr in instrs {
|
||||
let operands_str = instr.operands.iter()
|
||||
.map(|o| format!("{:?}", o))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let line = if operands_str.is_empty() {
|
||||
format!("{:04X} {:?}\n", instr.pc, instr.opcode)
|
||||
} else {
|
||||
format!("{:04X} {:?} {}\n", instr.pc, instr.opcode, operands_str.trim())
|
||||
};
|
||||
disasm_text.push_str(&line);
|
||||
}
|
||||
fs::write(golden_dir.join("program.disasm.txt"), disasm_text).unwrap();
|
||||
|
||||
// 3. AST JSON
|
||||
let source = fs::read_to_string(project_dir.join("src/main.pbs")).unwrap();
|
||||
let mut parser = Parser::new(&source, 0);
|
||||
let ast = parser.parse_file().expect("Failed to parse AST");
|
||||
let ast_node = Node::File(ast);
|
||||
let ast_json = serde_json::to_string_pretty(&ast_node).unwrap();
|
||||
fs::write(golden_dir.join("ast.json"), ast_json).unwrap();
|
||||
|
||||
println!("Golden artifacts generated in test-cartridges/canonical/golden/");
|
||||
}
|
||||
@ -28,6 +28,9 @@ fn test_hip_conformance_core_to_vm_to_bytecode() {
|
||||
functions: vec![ir_core::Function {
|
||||
id: FunctionId(1),
|
||||
name: "main".to_string(),
|
||||
param_slots: 0,
|
||||
local_slots: 0,
|
||||
return_slots: 0,
|
||||
params: vec![],
|
||||
return_type: ir_core::Type::Void,
|
||||
local_types: HashMap::new(),
|
||||
@ -84,43 +87,19 @@ fn test_hip_conformance_core_to_vm_to_bytecode() {
|
||||
let emit_result = emit_module(&vm_module, &file_manager).expect("Emission failed");
|
||||
let bytecode = emit_result.rom;
|
||||
|
||||
// 4. Assert exact bytes match frozen ISA/ABI
|
||||
let expected_bytecode = vec![
|
||||
0x50, 0x50, 0x42, 0x43, // Magic: "PPBC"
|
||||
0x00, 0x00, // Version: 0
|
||||
0x00, 0x00, // Flags: 0
|
||||
0x02, 0x00, 0x00, 0x00, // CP Count: 2
|
||||
0x00, // CP[0]: Null
|
||||
0x01, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // CP[1]: Int64(42)
|
||||
0x6c, 0x00, 0x00, 0x00, // ROM Size: 108
|
||||
// Instructions:
|
||||
0x60, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, // Alloc { tid: 10, slots: 2 }
|
||||
0x43, 0x00, 0x00, 0x00, 0x00, 0x00, // SetLocal 0
|
||||
0x42, 0x00, 0x00, 0x00, 0x00, 0x00, // GetLocal 0
|
||||
0x69, 0x00, // GateRetain
|
||||
0x67, 0x00, // GateBeginMutate
|
||||
0x10, 0x00, 0x01, 0x00, 0x00, 0x00, // PushConst 1 (42)
|
||||
0x43, 0x00, 0x01, 0x00, 0x00, 0x00, // SetLocal 1
|
||||
0x42, 0x00, 0x00, 0x00, 0x00, 0x00, // GetLocal 0
|
||||
0x69, 0x00, // GateRetain
|
||||
0x42, 0x00, 0x01, 0x00, 0x00, 0x00, // GetLocal 1
|
||||
0x62, 0x00, 0x00, 0x00, 0x00, 0x00, // GateStore 0
|
||||
0x68, 0x00, // GateEndMutate
|
||||
0x6a, 0x00, // GateRelease
|
||||
0x42, 0x00, 0x00, 0x00, 0x00, 0x00, // GetLocal 0
|
||||
0x69, 0x00, // GateRetain
|
||||
0x63, 0x00, // GateBeginPeek
|
||||
0x42, 0x00, 0x00, 0x00, 0x00, 0x00, // GetLocal 0
|
||||
0x69, 0x00, // GateRetain
|
||||
0x61, 0x00, 0x00, 0x00, 0x00, 0x00, // GateLoad 0
|
||||
0x64, 0x00, // GateEndPeek
|
||||
0x6a, 0x00, // GateRelease
|
||||
0x11, 0x00, // Pop
|
||||
0x42, 0x00, 0x00, 0x00, 0x00, 0x00, // GetLocal 0 (cleanup)
|
||||
0x6a, 0x00, // GateRelease
|
||||
0x10, 0x00, 0x00, 0x00, 0x00, 0x00, // PushConst 0 (Null return)
|
||||
0x51, 0x00, // Ret
|
||||
];
|
||||
// 4. Assert industrial PBS\0 format
|
||||
use prometeu_bytecode::v0::BytecodeLoader;
|
||||
let module = BytecodeLoader::load(&bytecode).expect("Failed to parse industrial PBC");
|
||||
assert_eq!(&bytecode[0..4], b"PBS\0");
|
||||
|
||||
assert_eq!(bytecode, expected_bytecode, "Bytecode does not match golden ISA/ABI v0");
|
||||
// 5. Verify a few key instructions in the code section to ensure ABI stability
|
||||
// We don't do a full byte-for-byte check of the entire file here as the section
|
||||
// table offsets vary, but we check the instruction stream.
|
||||
let instrs = module.code;
|
||||
|
||||
// Alloc { tid: 10, slots: 2 } -> 0x60 0x00, 0x0a 0x00 0x00 0x00, 0x02 0x00 0x00 0x00
|
||||
assert!(instrs.windows(10).any(|w| w == &[0x60, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00]));
|
||||
|
||||
// PushConst 1 (42) -> 0x10 0x00, 0x01 0x00, 0x00, 0x00
|
||||
assert!(instrs.windows(6).any(|w| w == &[0x10, 0x00, 0x01, 0x00, 0x00, 0x00]));
|
||||
}
|
||||
|
||||
@ -434,11 +434,22 @@ mod tests {
|
||||
let mut hw = Hardware::new();
|
||||
let signals = InputSignals::default();
|
||||
|
||||
let rom = prometeu_bytecode::pbc::write_pbc(&prometeu_bytecode::pbc::PbcFile {
|
||||
let rom = prometeu_bytecode::v0::BytecodeModule {
|
||||
version: 0,
|
||||
cp: vec![],
|
||||
rom: vec![0x02, 0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
}).unwrap();
|
||||
const_pool: vec![],
|
||||
functions: vec![prometeu_bytecode::v0::FunctionMeta {
|
||||
code_offset: 0,
|
||||
code_len: 6,
|
||||
param_slots: 0,
|
||||
local_slots: 0,
|
||||
return_slots: 0,
|
||||
max_stack_slots: 0,
|
||||
}],
|
||||
code: vec![0x02, 0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
debug_info: None,
|
||||
exports: vec![prometeu_bytecode::v0::Export { symbol: "main".into(), func_idx: 0 }],
|
||||
imports: vec![],
|
||||
}.serialize();
|
||||
let cartridge = Cartridge {
|
||||
app_id: 1234,
|
||||
title: "test".to_string(),
|
||||
@ -477,14 +488,25 @@ mod tests {
|
||||
// PUSH_CONST 0 (dummy)
|
||||
// FrameSync (0x80)
|
||||
// JMP 0
|
||||
let rom = prometeu_bytecode::pbc::write_pbc(&prometeu_bytecode::pbc::PbcFile {
|
||||
let rom = prometeu_bytecode::v0::BytecodeModule {
|
||||
version: 0,
|
||||
cp: vec![],
|
||||
rom: vec![
|
||||
const_pool: vec![],
|
||||
functions: vec![prometeu_bytecode::v0::FunctionMeta {
|
||||
code_offset: 0,
|
||||
code_len: 8,
|
||||
param_slots: 0,
|
||||
local_slots: 0,
|
||||
return_slots: 0,
|
||||
max_stack_slots: 0,
|
||||
}],
|
||||
code: vec![
|
||||
0x80, 0x00, // FrameSync (2 bytes opcode)
|
||||
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, // JMP 0 (2 bytes opcode + 4 bytes u32)
|
||||
],
|
||||
}).unwrap();
|
||||
debug_info: None,
|
||||
exports: vec![prometeu_bytecode::v0::Export { symbol: "main".into(), func_idx: 0 }],
|
||||
imports: vec![],
|
||||
}.serialize();
|
||||
let cartridge = Cartridge {
|
||||
app_id: 1234,
|
||||
title: "test".to_string(),
|
||||
@ -680,16 +702,27 @@ mod tests {
|
||||
let signals = InputSignals::default();
|
||||
|
||||
// PushI32 0 (0x17), then Ret (0x51)
|
||||
let rom = prometeu_bytecode::pbc::write_pbc(&prometeu_bytecode::pbc::PbcFile {
|
||||
let rom = prometeu_bytecode::v0::BytecodeModule {
|
||||
version: 0,
|
||||
cp: vec![],
|
||||
rom: vec![
|
||||
const_pool: vec![],
|
||||
functions: vec![prometeu_bytecode::v0::FunctionMeta {
|
||||
code_offset: 0,
|
||||
code_len: 10,
|
||||
param_slots: 0,
|
||||
local_slots: 0,
|
||||
return_slots: 0,
|
||||
max_stack_slots: 0,
|
||||
}],
|
||||
code: vec![
|
||||
0x17, 0x00, // PushI32
|
||||
0x00, 0x00, 0x00, 0x00, // value 0
|
||||
0x11, 0x00, // Pop
|
||||
0x51, 0x00 // Ret
|
||||
],
|
||||
}).unwrap();
|
||||
debug_info: None,
|
||||
exports: vec![prometeu_bytecode::v0::Export { symbol: "main".into(), func_idx: 0 }],
|
||||
imports: vec![],
|
||||
}.serialize();
|
||||
let cartridge = Cartridge {
|
||||
app_id: 1234,
|
||||
title: "test".to_string(),
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
use crate::virtual_machine::{ProgramImage, Value};
|
||||
use prometeu_bytecode::v0::{BytecodeModule, DebugInfo};
|
||||
use prometeu_bytecode::pbc::ConstantPoolEntry;
|
||||
use prometeu_bytecode::v0::{BytecodeModule, DebugInfo, ConstantPoolEntry};
|
||||
use prometeu_bytecode::opcode::OpCode;
|
||||
use std::collections::HashMap;
|
||||
|
||||
@ -149,6 +148,7 @@ impl Linker {
|
||||
combined_constants,
|
||||
combined_functions,
|
||||
debug_info,
|
||||
exports,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,12 +30,10 @@ pub enum VmFault {
|
||||
pub enum VmInitError {
|
||||
InvalidFormat,
|
||||
UnsupportedFormat,
|
||||
PpbcParseFailed,
|
||||
PbsV0LoadFailed(prometeu_bytecode::v0::LoadError),
|
||||
LinkFailed(LinkError),
|
||||
EntrypointNotFound,
|
||||
VerificationFailed(VerifierError),
|
||||
UnsupportedLegacyCallEncoding,
|
||||
}
|
||||
|
||||
pub struct HostReturn<'a> {
|
||||
|
||||
@ -2,6 +2,7 @@ use crate::virtual_machine::Value;
|
||||
use prometeu_bytecode::v0::{FunctionMeta, DebugInfo};
|
||||
use prometeu_bytecode::abi::TrapInfo;
|
||||
use std::sync::Arc;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ProgramImage {
|
||||
@ -9,10 +10,11 @@ pub struct ProgramImage {
|
||||
pub constant_pool: Arc<[Value]>,
|
||||
pub functions: Arc<[FunctionMeta]>,
|
||||
pub debug_info: Option<DebugInfo>,
|
||||
pub exports: Arc<HashMap<String, u32>>,
|
||||
}
|
||||
|
||||
impl ProgramImage {
|
||||
pub fn new(rom: Vec<u8>, constant_pool: Vec<Value>, mut functions: Vec<FunctionMeta>, debug_info: Option<DebugInfo>) -> Self {
|
||||
pub fn new(rom: Vec<u8>, constant_pool: Vec<Value>, mut functions: Vec<FunctionMeta>, debug_info: Option<DebugInfo>, exports: HashMap<String, u32>) -> Self {
|
||||
if functions.is_empty() && !rom.is_empty() {
|
||||
functions.push(FunctionMeta {
|
||||
code_offset: 0,
|
||||
@ -25,6 +27,7 @@ impl ProgramImage {
|
||||
constant_pool: Arc::from(constant_pool),
|
||||
functions: Arc::from(functions),
|
||||
debug_info,
|
||||
exports: Arc::new(exports),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@ use crate::virtual_machine::scope_frame::ScopeFrame;
|
||||
use crate::virtual_machine::value::Value;
|
||||
use crate::virtual_machine::{NativeInterface, ProgramImage, VmInitError};
|
||||
use prometeu_bytecode::opcode::OpCode;
|
||||
use prometeu_bytecode::pbc::{self, ConstantPoolEntry};
|
||||
use prometeu_bytecode::abi::{TrapInfo, TRAP_OOB, TRAP_DIV_ZERO, TRAP_TYPE, TRAP_INVALID_FUNC, TRAP_BAD_RET_SLOTS};
|
||||
|
||||
/// Reason why the Virtual Machine stopped execution during a specific run.
|
||||
@ -99,7 +98,7 @@ impl VirtualMachine {
|
||||
call_stack: Vec::new(),
|
||||
scope_stack: Vec::new(),
|
||||
globals: Vec::new(),
|
||||
program: ProgramImage::new(rom, constant_pool, vec![], None),
|
||||
program: ProgramImage::new(rom, constant_pool, vec![], None, std::collections::HashMap::new()),
|
||||
heap: Vec::new(),
|
||||
cycles: 0,
|
||||
halted: false,
|
||||
@ -122,25 +121,8 @@ impl VirtualMachine {
|
||||
self.cycles = 0;
|
||||
self.halted = true; // execution is impossible until successful load
|
||||
|
||||
// Only recognized formats are loadable.
|
||||
let program = if program_bytes.starts_with(b"PPBC") {
|
||||
// PBC (Prometeu ByteCode) legacy format
|
||||
let pbc_file = pbc::parse_pbc(&program_bytes).map_err(|_| VmInitError::PpbcParseFailed)?;
|
||||
|
||||
// Policy (A): Reject legacy CALL encoding in legacy formats.
|
||||
Self::legacy_reject_call_encoding(&pbc_file.rom)?;
|
||||
|
||||
let cp = pbc_file.cp.into_iter().map(|entry| match entry {
|
||||
ConstantPoolEntry::Int32(v) => Value::Int32(v),
|
||||
ConstantPoolEntry::Int64(v) => Value::Int64(v),
|
||||
ConstantPoolEntry::Float64(v) => Value::Float(v),
|
||||
ConstantPoolEntry::Boolean(v) => Value::Boolean(v),
|
||||
ConstantPoolEntry::String(v) => Value::String(v),
|
||||
ConstantPoolEntry::Null => Value::Null,
|
||||
}).collect();
|
||||
ProgramImage::new(pbc_file.rom, cp, vec![], None)
|
||||
} else if program_bytes.starts_with(b"PBS\0") {
|
||||
// PBS v0 industrial format
|
||||
// Only recognized format is loadable: PBS v0 industrial format
|
||||
let program = if program_bytes.starts_with(b"PBS\0") {
|
||||
match prometeu_bytecode::v0::BytecodeLoader::load(&program_bytes) {
|
||||
Ok(module) => {
|
||||
// Link module(s)
|
||||
@ -148,18 +130,9 @@ impl VirtualMachine {
|
||||
.map_err(VmInitError::LinkFailed)?;
|
||||
|
||||
// Run verifier on the linked program
|
||||
// Note: Verifier currently expects code and functions separately.
|
||||
// We need to ensure it works with the linked program.
|
||||
let max_stacks = crate::virtual_machine::verifier::Verifier::verify(&linked_program.rom, &linked_program.functions)
|
||||
.map_err(VmInitError::VerificationFailed)?;
|
||||
|
||||
// Apply verified max_stack_slots
|
||||
// Since linked_program.functions is an Arc<[FunctionMeta]>, we need to get a mutable copy if we want to update it.
|
||||
// Or we update it before creating the ProgramImage.
|
||||
|
||||
// Actually, let's look at how we can update max_stack_slots.
|
||||
// ProgramImage holds Arc<[FunctionMeta]>.
|
||||
|
||||
let mut functions = linked_program.functions.as_ref().to_vec();
|
||||
for (func, max_stack) in functions.iter_mut().zip(max_stacks) {
|
||||
func.max_stack_slots = max_stack;
|
||||
@ -177,15 +150,21 @@ impl VirtualMachine {
|
||||
return Err(VmInitError::InvalidFormat);
|
||||
};
|
||||
|
||||
// Resolve the entrypoint. Currently supports numeric addresses or empty (defaults to 0).
|
||||
// Resolve the entrypoint. Currently supports numeric addresses, symbolic exports, or empty (defaults to 0).
|
||||
let pc = if entrypoint.is_empty() {
|
||||
0
|
||||
} else {
|
||||
let addr = entrypoint.parse::<usize>().map_err(|_| VmInitError::EntrypointNotFound)?;
|
||||
} else if let Ok(addr) = entrypoint.parse::<usize>() {
|
||||
if addr >= program.rom.len() && (addr > 0 || !program.rom.is_empty()) {
|
||||
return Err(VmInitError::EntrypointNotFound);
|
||||
}
|
||||
addr
|
||||
} else {
|
||||
// Try to resolve symbol name via ProgramImage exports
|
||||
if let Some(&func_idx) = program.exports.get(entrypoint) {
|
||||
program.functions[func_idx as usize].code_offset as usize
|
||||
} else {
|
||||
return Err(VmInitError::EntrypointNotFound);
|
||||
}
|
||||
};
|
||||
|
||||
// Finalize initialization by applying the new program and PC.
|
||||
@ -199,16 +178,17 @@ impl VirtualMachine {
|
||||
/// Prepares the VM to execute a specific entrypoint by setting the PC and
|
||||
/// pushing an initial call frame.
|
||||
pub fn prepare_call(&mut self, entrypoint: &str) {
|
||||
let addr = if let Ok(addr) = entrypoint.parse::<usize>() {
|
||||
addr
|
||||
let (addr, func_idx) = if let Ok(addr) = entrypoint.parse::<usize>() {
|
||||
let idx = self.program.functions.iter().position(|f| {
|
||||
addr >= f.code_offset as usize && addr < (f.code_offset + f.code_len) as usize
|
||||
}).unwrap_or(0);
|
||||
(addr, idx)
|
||||
} else if let Some(&func_idx) = self.program.exports.get(entrypoint) {
|
||||
(self.program.functions[func_idx as usize].code_offset as usize, func_idx as usize)
|
||||
} else {
|
||||
0
|
||||
(0, 0)
|
||||
};
|
||||
|
||||
let func_idx = self.program.functions.iter().position(|f| {
|
||||
addr >= f.code_offset as usize && addr < (f.code_offset + f.code_len) as usize
|
||||
}).unwrap_or(0);
|
||||
|
||||
self.pc = addr;
|
||||
self.halted = false;
|
||||
|
||||
@ -222,7 +202,8 @@ impl VirtualMachine {
|
||||
// Entrypoint also needs locals allocated.
|
||||
// For the sentinel frame, stack_base is always 0.
|
||||
if let Some(func) = self.program.functions.get(func_idx) {
|
||||
for _ in 0..func.local_slots {
|
||||
let total_slots = func.param_slots as u32 + func.local_slots as u32;
|
||||
for _ in 0..total_slots {
|
||||
self.operand_stack.push(Value::Null);
|
||||
}
|
||||
}
|
||||
@ -234,18 +215,6 @@ impl VirtualMachine {
|
||||
});
|
||||
}
|
||||
|
||||
fn legacy_reject_call_encoding(rom: &[u8]) -> Result<(), VmInitError> {
|
||||
let mut pc = 0usize;
|
||||
while pc < rom.len() {
|
||||
let instr = crate::virtual_machine::bytecode::decoder::decode_at(rom, pc)
|
||||
.map_err(|_| VmInitError::PpbcParseFailed)?;
|
||||
if instr.opcode == OpCode::Call {
|
||||
return Err(VmInitError::UnsupportedLegacyCallEncoding);
|
||||
}
|
||||
pc = instr.next_pc;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VirtualMachine {
|
||||
@ -1212,7 +1181,7 @@ mod tests {
|
||||
];
|
||||
|
||||
let mut vm = VirtualMachine {
|
||||
program: ProgramImage::new(rom, vec![], functions, None),
|
||||
program: ProgramImage::new(rom, vec![], functions, None, std::collections::HashMap::new()),
|
||||
..Default::default()
|
||||
};
|
||||
vm.prepare_call("0");
|
||||
@ -1257,7 +1226,7 @@ mod tests {
|
||||
];
|
||||
|
||||
let mut vm = VirtualMachine {
|
||||
program: ProgramImage::new(rom, vec![], functions, None),
|
||||
program: ProgramImage::new(rom, vec![], functions, None, std::collections::HashMap::new()),
|
||||
..Default::default()
|
||||
};
|
||||
vm.prepare_call("0");
|
||||
@ -1296,7 +1265,7 @@ mod tests {
|
||||
];
|
||||
|
||||
let mut vm2 = VirtualMachine {
|
||||
program: ProgramImage::new(rom2, vec![], functions2, None),
|
||||
program: ProgramImage::new(rom2, vec![], functions2, None, std::collections::HashMap::new()),
|
||||
..Default::default()
|
||||
};
|
||||
vm2.prepare_call("0");
|
||||
@ -1405,7 +1374,7 @@ mod tests {
|
||||
];
|
||||
|
||||
let mut vm = VirtualMachine {
|
||||
program: ProgramImage::new(rom, vec![], functions, None),
|
||||
program: ProgramImage::new(rom, vec![], functions, None, std::collections::HashMap::new()),
|
||||
..Default::default()
|
||||
};
|
||||
vm.prepare_call("0");
|
||||
@ -1927,53 +1896,6 @@ mod tests {
|
||||
assert_eq!(vm.cycles, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_policy_a_reject_legacy_call() {
|
||||
let mut vm = VirtualMachine::default();
|
||||
|
||||
// PBC Header (PPBC)
|
||||
let mut pbc = b"PPBC".to_vec();
|
||||
pbc.extend_from_slice(&0u16.to_le_bytes()); // Version
|
||||
pbc.extend_from_slice(&0u16.to_le_bytes()); // Flags
|
||||
pbc.extend_from_slice(&0u32.to_le_bytes()); // CP count
|
||||
pbc.extend_from_slice(&4u32.to_le_bytes()); // ROM size
|
||||
|
||||
// ROM: CALL (2 bytes) + 4-byte immediate (from OpcodeSpec)
|
||||
// Wait, OpcodeSpec says CALL imm_bytes is 4.
|
||||
pbc.extend_from_slice(&(OpCode::Call as u16).to_le_bytes());
|
||||
pbc.extend_from_slice(&[0, 0, 0, 0]);
|
||||
// Update ROM size to 6
|
||||
pbc[12..16].copy_from_slice(&6u32.to_le_bytes());
|
||||
|
||||
let res = vm.initialize(pbc, "");
|
||||
assert_eq!(res, Err(VmInitError::UnsupportedLegacyCallEncoding));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_policy_a_permit_call_pattern_in_immediate() {
|
||||
let mut vm = VirtualMachine::default();
|
||||
|
||||
// PBC Header (PPBC)
|
||||
let mut pbc = b"PPBC".to_vec();
|
||||
pbc.extend_from_slice(&0u16.to_le_bytes()); // Version
|
||||
pbc.extend_from_slice(&0u16.to_le_bytes()); // Flags
|
||||
pbc.extend_from_slice(&0u32.to_le_bytes()); // CP count
|
||||
|
||||
// ROM: PUSH_I64 with a value that contains OpCode::Call bytes
|
||||
let mut rom = Vec::new();
|
||||
rom.extend_from_slice(&(OpCode::PushI64 as u16).to_le_bytes());
|
||||
let call_val = OpCode::Call as u16;
|
||||
let mut val_bytes = [0u8; 8];
|
||||
val_bytes[0..2].copy_from_slice(&call_val.to_le_bytes());
|
||||
rom.extend_from_slice(&val_bytes);
|
||||
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
|
||||
|
||||
pbc.extend_from_slice(&(rom.len() as u32).to_le_bytes()); // ROM size
|
||||
pbc.extend_from_slice(&rom);
|
||||
|
||||
let res = vm.initialize(pbc, "");
|
||||
assert!(res.is_ok(), "Should NOT fail if Call pattern is in immediate: {:?}", res);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calling_convention_add() {
|
||||
@ -2505,7 +2427,7 @@ mod tests {
|
||||
function_names: vec![(0, "main".to_string())],
|
||||
};
|
||||
|
||||
let program = ProgramImage::new(rom, vec![], vec![], Some(debug_info));
|
||||
let program = ProgramImage::new(rom, vec![], vec![], Some(debug_info), std::collections::HashMap::new());
|
||||
let mut vm = VirtualMachine {
|
||||
program,
|
||||
..Default::default()
|
||||
@ -2552,7 +2474,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}];
|
||||
|
||||
let program = ProgramImage::new(rom, vec![], functions, Some(debug_info));
|
||||
let program = ProgramImage::new(rom, vec![], functions, Some(debug_info), std::collections::HashMap::new());
|
||||
let mut vm = VirtualMachine {
|
||||
program,
|
||||
..Default::default()
|
||||
|
||||
73
crates/prometeu-core/tests/heartbeat.rs
Normal file
73
crates/prometeu-core/tests/heartbeat.rs
Normal file
@ -0,0 +1,73 @@
|
||||
use prometeu_core::virtual_machine::{VirtualMachine, LogicalFrameEndingReason};
|
||||
use prometeu_core::hardware::HardwareBridge;
|
||||
use prometeu_core::Hardware;
|
||||
use prometeu_core::virtual_machine::NativeInterface;
|
||||
use prometeu_core::virtual_machine::Value;
|
||||
use prometeu_core::virtual_machine::HostReturn;
|
||||
use std::path::Path;
|
||||
use std::fs;
|
||||
|
||||
struct MockNative;
|
||||
impl NativeInterface for MockNative {
|
||||
fn syscall(&mut self, id: u32, _args: &[Value], ret: &mut HostReturn, _hw: &mut dyn HardwareBridge) -> Result<(), prometeu_core::virtual_machine::VmFault> {
|
||||
if id == 0x2010 { // InputPadSnapshot
|
||||
for _ in 0..48 {
|
||||
ret.push_bool(false);
|
||||
}
|
||||
} else if id == 0x2011 { // InputTouchSnapshot
|
||||
for _ in 0..6 {
|
||||
ret.push_int(0);
|
||||
}
|
||||
} else {
|
||||
// Push one result for others that might expect it
|
||||
// Based on results_count() in syscalls.rs, most return 1 except GfxClear565 (0)
|
||||
if id != 0x1010 {
|
||||
ret.push_null();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_canonical_cartridge_heartbeat() {
|
||||
let mut pbc_path = Path::new("../../test-cartridges/canonical/golden/program.pbc").to_path_buf();
|
||||
if !pbc_path.exists() {
|
||||
pbc_path = Path::new("test-cartridges/canonical/golden/program.pbc").to_path_buf();
|
||||
}
|
||||
|
||||
let pbc_bytes = fs::read(pbc_path).expect("Failed to read canonical PBC. Did you run the generation test?");
|
||||
|
||||
let mut vm = VirtualMachine::new(vec![], vec![]);
|
||||
vm.initialize(pbc_bytes, "frame").expect("Failed to initialize VM with canonical cartridge");
|
||||
vm.prepare_call("frame");
|
||||
|
||||
let mut native = MockNative;
|
||||
let mut hw = Hardware::new();
|
||||
|
||||
// Run for a reasonable budget
|
||||
let report = vm.run_budget(1000, &mut native, &mut hw).expect("VM failed to run");
|
||||
|
||||
// Acceptance criteria:
|
||||
// 1. No traps
|
||||
match report.reason {
|
||||
LogicalFrameEndingReason::Trap(trap) => panic!("VM trapped: {:?}", trap),
|
||||
LogicalFrameEndingReason::Panic(msg) => panic!("VM panicked: {}", msg),
|
||||
LogicalFrameEndingReason::Halted => {},
|
||||
LogicalFrameEndingReason::EndOfRom => {},
|
||||
LogicalFrameEndingReason::FrameSync => {},
|
||||
LogicalFrameEndingReason::BudgetExhausted => {},
|
||||
LogicalFrameEndingReason::Breakpoint => {},
|
||||
}
|
||||
|
||||
// 2. Deterministic output state (if any)
|
||||
// In our frame(), z should be 30.
|
||||
// Local 2 in frame() should be 30.
|
||||
// Let's check the stack or locals if possible.
|
||||
|
||||
// The VM should have finished 'frame'.
|
||||
// Since 'frame' returns void, the stack should be empty (or have the return value if any, but it's void).
|
||||
assert_eq!(vm.operand_stack.len(), 0, "Stack should be empty after frame() execution");
|
||||
|
||||
println!("Heartbeat test passed!");
|
||||
}
|
||||
167
docs/specs/pbs/PBS - Module and Linking Model.md
Normal file
167
docs/specs/pbs/PBS - Module and Linking Model.md
Normal file
@ -0,0 +1,167 @@
|
||||
# PBS v0 — Module & Linking Model (Self‑Contained Blob)
|
||||
|
||||
## Status
|
||||
|
||||
**Accepted (v0)** — This specification defines the authoritative execution and linking model for PBS v0.
|
||||
|
||||
---
|
||||
|
||||
## 1. Motivation
|
||||
|
||||
PBS is designed to be executed by a small, deterministic virtual machine embedded in PrometeuOS. To keep the VM **simple, secure, and optimizable**, all *semantic linking* must happen **before runtime**, in the compiler/tooling layer.
|
||||
|
||||
The VM is **not a linker**. It is an executor with validation guarantees.
|
||||
|
||||
---
|
||||
|
||||
## 2. Core Principle
|
||||
|
||||
> **A PBS module must be fully self‑contained and executable as a single blob.**
|
||||
|
||||
There is **no runtime linking** in PBS v0.
|
||||
|
||||
The VM only performs:
|
||||
|
||||
```
|
||||
load → verify → execute
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. What “Linking” Means in PBS
|
||||
|
||||
In PBS, *linking* refers to resolving all symbolic or relative references into a **final, index‑based layout**.
|
||||
|
||||
This includes:
|
||||
|
||||
* Function call resolution
|
||||
* Control‑flow targets (JMP / conditional branches)
|
||||
* Constant pool indexing
|
||||
* Syscall signature binding
|
||||
|
||||
All of this must be **fully resolved by the compiler/toolchain**.
|
||||
|
||||
---
|
||||
|
||||
## 4. PBS v0 Module Structure
|
||||
|
||||
A PBS v0 module consists of:
|
||||
|
||||
* `code: [u8]` — final bytecode stream
|
||||
* `functions: [FunctionMeta]` — function table
|
||||
* `const_pool: [Const]` — constant pool
|
||||
* (optional) metadata (build id, debug info, hashes)
|
||||
|
||||
The module is **self‑contained**: no external symbols, imports, or relocations.
|
||||
|
||||
---
|
||||
|
||||
## 5. Function Table and CALL Semantics
|
||||
|
||||
### 5.1 Function Identification
|
||||
|
||||
Functions are identified **by index** in the function table.
|
||||
|
||||
```text
|
||||
CALL <u32 func_id>
|
||||
```
|
||||
|
||||
There are **no address‑based calls** in PBS v0.
|
||||
|
||||
### 5.2 FunctionMeta
|
||||
|
||||
Each function is described by `FunctionMeta`:
|
||||
|
||||
* `code_offset`
|
||||
* `code_len`
|
||||
* `param_slots`
|
||||
* `local_slots`
|
||||
* `return_slots`
|
||||
* (optional) `max_stack_slots` (precomputed)
|
||||
|
||||
The compiler is responsible for emitting **correct metadata**.
|
||||
|
||||
---
|
||||
|
||||
## 6. Control Flow (JMP / Branches)
|
||||
|
||||
* All jump targets are **relative to the start of the current function**.
|
||||
* Targets must land on **valid instruction boundaries**.
|
||||
|
||||
This eliminates the need for global relocations.
|
||||
|
||||
---
|
||||
|
||||
## 7. Role of the Compiler / Tooling
|
||||
|
||||
The compiler (or offline tooling) is responsible for:
|
||||
|
||||
* Resolving all calls to `func_id`
|
||||
* Emitting the final function table
|
||||
* Laying out code contiguously
|
||||
* Emitting valid jump targets
|
||||
* Computing stack effects (optionally embedding `max_stack_slots`)
|
||||
* Ensuring ABI‑correct syscall usage
|
||||
|
||||
The output must be a **ready‑to‑run PBS module**.
|
||||
|
||||
---
|
||||
|
||||
## 8. Role of the VM
|
||||
|
||||
The VM **does not perform linking**.
|
||||
|
||||
It is responsible for:
|
||||
|
||||
* Parsing the module
|
||||
* Verifying structural and semantic correctness
|
||||
* Executing bytecode deterministically
|
||||
|
||||
### 8.1 Mandatory Runtime Verification
|
||||
|
||||
The VM must always verify:
|
||||
|
||||
* Bytecode truncation / corruption
|
||||
* Stack underflow / overflow
|
||||
* Invalid `func_id`
|
||||
* Invalid jump targets
|
||||
* Syscall signature mismatches
|
||||
|
||||
These checks exist for **safety and determinism**, not for late binding.
|
||||
|
||||
---
|
||||
|
||||
## 9. Legacy and Compatibility Policies
|
||||
|
||||
Legacy formats (e.g. PPBC) may be supported behind explicit policies.
|
||||
|
||||
Example:
|
||||
|
||||
* Legacy `CALL addr` encodings are **rejected** under Policy (A)
|
||||
* Only `CALL func_id` is valid in PBS v0
|
||||
|
||||
Compatibility handling is **orthogonal** to the linking model.
|
||||
|
||||
---
|
||||
|
||||
## 10. Future Evolution (Non‑Goals for v0)
|
||||
|
||||
PBS v0 explicitly does **not** define:
|
||||
|
||||
* Multi‑module linking
|
||||
* Dynamic imports
|
||||
* Runtime symbol resolution
|
||||
* Relocation tables
|
||||
|
||||
These may appear in future versions (v1+), but **v0 is closed and static by design**.
|
||||
|
||||
---
|
||||
|
||||
## 11. Summary
|
||||
|
||||
* PBS modules are **single, self‑contained blobs**
|
||||
* All linking happens **before runtime**
|
||||
* The VM is a **verifying executor**, not a linker
|
||||
* This model enables aggressive optimization, predictability, and simplicity
|
||||
|
||||
This specification is **normative** for PBS v0.
|
||||
@ -1388,6 +1388,23 @@ This avoids overloading the meaning of `TypeName.member`.
|
||||
|
||||
### 8.7 Summary of Struct Rules
|
||||
|
||||
Full example of `struct`:
|
||||
```pbs
|
||||
declare struct Vector(x: float, y: float)
|
||||
[
|
||||
(): (0.0, 0.0) as default { }
|
||||
(a: float): (a, a) as square { }
|
||||
]
|
||||
[[
|
||||
ZERO: default()
|
||||
]]
|
||||
{
|
||||
pub fn len(self: this): float { ... }
|
||||
pub fn scale(self: mut this): void { ... }
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
* Structs are declared with `declare struct`.
|
||||
* Fields are private and cannot be accessed directly.
|
||||
* Constructor aliases exist only inside the type and are called as `Type.alias(...)`.
|
||||
|
||||
@ -20,23 +20,6 @@
|
||||
|
||||
---
|
||||
|
||||
## PR-13 — Optional: Refactor Value representation (tagged slots) for clarity
|
||||
|
||||
**Why:** If current `Value` representation is the source of complexity/bugs, refactor now.
|
||||
|
||||
### Scope (only if needed)
|
||||
|
||||
* Make `Slot` explicit:
|
||||
|
||||
* `Slot::I32`, `Slot::I64`, `Slot::U32`, `Slot::Bool`, `Slot::ConstId`, `Slot::GateId`, `Slot::Unit`
|
||||
* Multi-slot types become sequences of slots.
|
||||
|
||||
### Acceptance
|
||||
|
||||
* Simpler, more verifiable runtime.
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done (DoD) for PBS v0 “minimum executable”
|
||||
|
||||
A single canonical cartridge runs end-to-end:
|
||||
|
||||
1169
test-cartridges/canonical/golden/ast.json
Normal file
1169
test-cartridges/canonical/golden/ast.json
Normal file
File diff suppressed because it is too large
Load Diff
176
test-cartridges/canonical/golden/program.disasm.txt
Normal file
176
test-cartridges/canonical/golden/program.disasm.txt
Normal file
@ -0,0 +1,176 @@
|
||||
0000 GetLocal U32(0)
|
||||
0006 GetLocal U32(1)
|
||||
000C Add
|
||||
000E Ret
|
||||
0010 PushConst U32(1)
|
||||
0016 SetLocal U32(0)
|
||||
001C PushConst U32(2)
|
||||
0022 SetLocal U32(1)
|
||||
0028 GetLocal U32(0)
|
||||
002E GetLocal U32(1)
|
||||
0034 Call U32(0)
|
||||
003A SetLocal U32(2)
|
||||
0040 GetLocal U32(2)
|
||||
0046 PushConst U32(3)
|
||||
004C Eq
|
||||
004E JmpIfFalse U32(92)
|
||||
0054 Jmp U32(74)
|
||||
005A PushBounded U32(2016)
|
||||
0060 Syscall U32(4112)
|
||||
0066 Jmp U32(110)
|
||||
006C PushBounded U32(63488)
|
||||
0072 Syscall U32(4112)
|
||||
0078 Jmp U32(110)
|
||||
007E Syscall U32(8208)
|
||||
0084 GetLocal U32(50)
|
||||
008A GateRelease
|
||||
008C SetLocal U32(50)
|
||||
0092 GetLocal U32(49)
|
||||
0098 GateRelease
|
||||
009A SetLocal U32(49)
|
||||
00A0 GetLocal U32(48)
|
||||
00A6 GateRelease
|
||||
00A8 SetLocal U32(48)
|
||||
00AE GetLocal U32(47)
|
||||
00B4 GateRelease
|
||||
00B6 SetLocal U32(47)
|
||||
00BC GetLocal U32(46)
|
||||
00C2 GateRelease
|
||||
00C4 SetLocal U32(46)
|
||||
00CA GetLocal U32(45)
|
||||
00D0 GateRelease
|
||||
00D2 SetLocal U32(45)
|
||||
00D8 GetLocal U32(44)
|
||||
00DE GateRelease
|
||||
00E0 SetLocal U32(44)
|
||||
00E6 GetLocal U32(43)
|
||||
00EC GateRelease
|
||||
00EE SetLocal U32(43)
|
||||
00F4 GetLocal U32(42)
|
||||
00FA GateRelease
|
||||
00FC SetLocal U32(42)
|
||||
0102 GetLocal U32(41)
|
||||
0108 GateRelease
|
||||
010A SetLocal U32(41)
|
||||
0110 GetLocal U32(40)
|
||||
0116 GateRelease
|
||||
0118 SetLocal U32(40)
|
||||
011E GetLocal U32(39)
|
||||
0124 GateRelease
|
||||
0126 SetLocal U32(39)
|
||||
012C GetLocal U32(38)
|
||||
0132 GateRelease
|
||||
0134 SetLocal U32(38)
|
||||
013A GetLocal U32(37)
|
||||
0140 GateRelease
|
||||
0142 SetLocal U32(37)
|
||||
0148 GetLocal U32(36)
|
||||
014E GateRelease
|
||||
0150 SetLocal U32(36)
|
||||
0156 GetLocal U32(35)
|
||||
015C GateRelease
|
||||
015E SetLocal U32(35)
|
||||
0164 GetLocal U32(34)
|
||||
016A GateRelease
|
||||
016C SetLocal U32(34)
|
||||
0172 GetLocal U32(33)
|
||||
0178 GateRelease
|
||||
017A SetLocal U32(33)
|
||||
0180 GetLocal U32(32)
|
||||
0186 GateRelease
|
||||
0188 SetLocal U32(32)
|
||||
018E GetLocal U32(31)
|
||||
0194 GateRelease
|
||||
0196 SetLocal U32(31)
|
||||
019C GetLocal U32(30)
|
||||
01A2 GateRelease
|
||||
01A4 SetLocal U32(30)
|
||||
01AA GetLocal U32(29)
|
||||
01B0 GateRelease
|
||||
01B2 SetLocal U32(29)
|
||||
01B8 GetLocal U32(28)
|
||||
01BE GateRelease
|
||||
01C0 SetLocal U32(28)
|
||||
01C6 GetLocal U32(27)
|
||||
01CC GateRelease
|
||||
01CE SetLocal U32(27)
|
||||
01D4 GetLocal U32(26)
|
||||
01DA GateRelease
|
||||
01DC SetLocal U32(26)
|
||||
01E2 GetLocal U32(25)
|
||||
01E8 GateRelease
|
||||
01EA SetLocal U32(25)
|
||||
01F0 GetLocal U32(24)
|
||||
01F6 GateRelease
|
||||
01F8 SetLocal U32(24)
|
||||
01FE GetLocal U32(23)
|
||||
0204 GateRelease
|
||||
0206 SetLocal U32(23)
|
||||
020C GetLocal U32(22)
|
||||
0212 GateRelease
|
||||
0214 SetLocal U32(22)
|
||||
021A GetLocal U32(21)
|
||||
0220 GateRelease
|
||||
0222 SetLocal U32(21)
|
||||
0228 GetLocal U32(20)
|
||||
022E GateRelease
|
||||
0230 SetLocal U32(20)
|
||||
0236 GetLocal U32(19)
|
||||
023C GateRelease
|
||||
023E SetLocal U32(19)
|
||||
0244 GetLocal U32(18)
|
||||
024A GateRelease
|
||||
024C SetLocal U32(18)
|
||||
0252 GetLocal U32(17)
|
||||
0258 GateRelease
|
||||
025A SetLocal U32(17)
|
||||
0260 GetLocal U32(16)
|
||||
0266 GateRelease
|
||||
0268 SetLocal U32(16)
|
||||
026E GetLocal U32(15)
|
||||
0274 GateRelease
|
||||
0276 SetLocal U32(15)
|
||||
027C GetLocal U32(14)
|
||||
0282 GateRelease
|
||||
0284 SetLocal U32(14)
|
||||
028A GetLocal U32(13)
|
||||
0290 GateRelease
|
||||
0292 SetLocal U32(13)
|
||||
0298 GetLocal U32(12)
|
||||
029E GateRelease
|
||||
02A0 SetLocal U32(12)
|
||||
02A6 GetLocal U32(11)
|
||||
02AC GateRelease
|
||||
02AE SetLocal U32(11)
|
||||
02B4 GetLocal U32(10)
|
||||
02BA GateRelease
|
||||
02BC SetLocal U32(10)
|
||||
02C2 GetLocal U32(9)
|
||||
02C8 GateRelease
|
||||
02CA SetLocal U32(9)
|
||||
02D0 GetLocal U32(8)
|
||||
02D6 GateRelease
|
||||
02D8 SetLocal U32(8)
|
||||
02DE GetLocal U32(7)
|
||||
02E4 GateRelease
|
||||
02E6 SetLocal U32(7)
|
||||
02EC GetLocal U32(6)
|
||||
02F2 GateRelease
|
||||
02F4 SetLocal U32(6)
|
||||
02FA GetLocal U32(5)
|
||||
0300 GateRelease
|
||||
0302 SetLocal U32(5)
|
||||
0308 GetLocal U32(4)
|
||||
030E GateRelease
|
||||
0310 SetLocal U32(4)
|
||||
0316 GetLocal U32(3)
|
||||
031C GateRelease
|
||||
031E SetLocal U32(3)
|
||||
0324 GetLocal U32(21)
|
||||
032A JmpIfFalse U32(824)
|
||||
0330 Jmp U32(806)
|
||||
0336 PushBounded U32(31)
|
||||
033C Syscall U32(4112)
|
||||
0342 Jmp U32(830)
|
||||
0348 Jmp U32(830)
|
||||
034E Ret
|
||||
BIN
test-cartridges/canonical/golden/program.pbc
Normal file
BIN
test-cartridges/canonical/golden/program.pbc
Normal file
Binary file not shown.
4
test-cartridges/canonical/prometeu.json
Normal file
4
test-cartridges/canonical/prometeu.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"script_fe": "pbs",
|
||||
"entry": "src/main.pbs"
|
||||
}
|
||||
66
test-cartridges/canonical/src/main.pbs
Normal file
66
test-cartridges/canonical/src/main.pbs
Normal file
@ -0,0 +1,66 @@
|
||||
// CartridgeCanonical.pbs
|
||||
// Purpose: VM Heartbeat Test (Industrial Baseline)
|
||||
|
||||
declare struct Color(raw: bounded)
|
||||
[[
|
||||
BLACK: Color(0b),
|
||||
WHITE: Color(65535b),
|
||||
RED: Color(63488b),
|
||||
GREEN: Color(2016b),
|
||||
BLUE: Color(31b)
|
||||
]]
|
||||
|
||||
declare struct ButtonState(
|
||||
pressed: bool,
|
||||
released: bool,
|
||||
down: bool,
|
||||
hold_frames: bounded
|
||||
)
|
||||
|
||||
declare struct Pad(
|
||||
up: ButtonState,
|
||||
down: ButtonState,
|
||||
left: ButtonState,
|
||||
right: ButtonState,
|
||||
a: ButtonState,
|
||||
b: ButtonState,
|
||||
x: ButtonState,
|
||||
y: ButtonState,
|
||||
l: ButtonState,
|
||||
r: ButtonState,
|
||||
start: ButtonState,
|
||||
select: ButtonState
|
||||
)
|
||||
|
||||
declare contract Gfx host {
|
||||
fn clear(color: Color): void;
|
||||
}
|
||||
|
||||
declare contract Input host {
|
||||
fn pad(): Pad;
|
||||
}
|
||||
|
||||
fn add(a: int, b: int): int {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
fn frame(): void {
|
||||
// 1. Locals & Arithmetic
|
||||
let x = 10;
|
||||
let y = 20;
|
||||
let z = add(x, y);
|
||||
|
||||
// 2. Control Flow (if)
|
||||
if z == 30 {
|
||||
// 3. Syscall Clear
|
||||
Gfx.clear(Color.GREEN);
|
||||
} else {
|
||||
Gfx.clear(Color.RED);
|
||||
}
|
||||
|
||||
// 4. Input Snapshot & Nested Member Access
|
||||
let p = Input.pad();
|
||||
if p.a.down {
|
||||
Gfx.clear(Color.BLUE);
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
fn frame(): void
|
||||
{
|
||||
Gfx.clear(Color.WHITE);
|
||||
|
||||
let p: Pad = Input.pad();
|
||||
if p.any() {
|
||||
Gfx.clear(Color.MAGENTA);
|
||||
}
|
||||
|
||||
let t: Touch = Input.touch();
|
||||
if t.f.down {
|
||||
Gfx.clear(Color.BLUE);
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@
|
||||
"title": "Test 1",
|
||||
"app_version": "0.1.0",
|
||||
"app_mode": "Game",
|
||||
"entrypoint": "0",
|
||||
"entrypoint": "frame",
|
||||
"asset_table": [
|
||||
{
|
||||
"asset_id": 0,
|
||||
|
||||
Binary file not shown.
@ -1,2 +1,66 @@
|
||||
fn frame(): void {
|
||||
// CartridgeCanonical.pbs
|
||||
// Purpose: VM Heartbeat Test (Industrial Baseline)
|
||||
|
||||
declare struct Color(raw: bounded)
|
||||
[[
|
||||
BLACK: Color(0b),
|
||||
WHITE: Color(65535b),
|
||||
RED: Color(63488b),
|
||||
GREEN: Color(2016b),
|
||||
BLUE: Color(31b)
|
||||
]]
|
||||
|
||||
declare struct ButtonState(
|
||||
pressed: bool,
|
||||
released: bool,
|
||||
down: bool,
|
||||
hold_frames: bounded
|
||||
)
|
||||
|
||||
declare struct Pad(
|
||||
up: ButtonState,
|
||||
down: ButtonState,
|
||||
left: ButtonState,
|
||||
right: ButtonState,
|
||||
a: ButtonState,
|
||||
b: ButtonState,
|
||||
x: ButtonState,
|
||||
y: ButtonState,
|
||||
l: ButtonState,
|
||||
r: ButtonState,
|
||||
start: ButtonState,
|
||||
select: ButtonState
|
||||
)
|
||||
|
||||
declare contract Gfx host {
|
||||
fn clear(color: Color): void;
|
||||
}
|
||||
|
||||
declare contract Input host {
|
||||
fn pad(): Pad;
|
||||
}
|
||||
|
||||
fn add(a: int, b: int): int {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
fn frame(): void {
|
||||
// 1. Locals & Arithmetic
|
||||
let x = 10;
|
||||
let y = 20;
|
||||
let z = add(x, y);
|
||||
|
||||
// 2. Control Flow (if)
|
||||
if z == 30 {
|
||||
// 3. Syscall Clear
|
||||
Gfx.clear(Color.GREEN);
|
||||
} else {
|
||||
Gfx.clear(Color.RED);
|
||||
}
|
||||
|
||||
// 4. Input Snapshot & Nested Member Access
|
||||
let p = Input.pad();
|
||||
if p.a.down {
|
||||
Gfx.clear(Color.BLUE);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user