This commit is contained in:
Nilton Constantino 2026-01-31 20:39:05 +00:00
parent 83e24920b4
commit ff4dcad5dc
No known key found for this signature in database
42 changed files with 2942 additions and 604 deletions

View File

@ -17,6 +17,8 @@ pub enum Operand {
Bool(bool), Bool(bool),
/// A symbolic label that will be resolved to an absolute PC address. /// A symbolic label that will be resolved to an absolute PC address.
Label(String), 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). /// 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; let mut pcp: u32 = initial_pc;
for operand in operands { for operand in operands {
match operand { 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::I64(_) | Operand::F64(_) => pcp += 8,
Operand::Bool(_) => pcp += 1, 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))?; let addr = labels.get(name).ok_or(format!("Undefined label: {}", name))?;
write_u32_le(&mut rom, *addr).map_err(|e| e.to_string())?; 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())?;
}
} }
} }
} }

View File

@ -41,24 +41,20 @@ pub fn disasm(rom: &[u8]) -> Result<Vec<Instr>, String> {
match opcode { match opcode {
OpCode::PushConst | OpCode::PushI32 | OpCode::PushBounded | OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue OpCode::PushConst | OpCode::PushI32 | OpCode::PushBounded | OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue
| OpCode::GetGlobal | OpCode::SetGlobal | OpCode::GetLocal | OpCode::SetLocal | 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())?; let v = read_u32_le(&mut cursor).map_err(|e| e.to_string())?;
operands.push(DisasmOperand::U32(v)); operands.push(DisasmOperand::U32(v));
} }
OpCode::PushI64 => { OpCode::PushI64 | OpCode::PushF64 => {
let v = read_i64_le(&mut cursor).map_err(|e| e.to_string())?; let v = read_i64_le(&mut cursor).map_err(|e| e.to_string())?;
operands.push(DisasmOperand::I64(v)); 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 => { OpCode::PushBool => {
let mut b_buf = [0u8; 1]; let mut b_buf = [0u8; 1];
cursor.read_exact(&mut b_buf).map_err(|e| e.to_string())?; cursor.read_exact(&mut b_buf).map_err(|e| e.to_string())?;
operands.push(DisasmOperand::Bool(b_buf[0] != 0)); 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 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())?; let v2 = read_u32_le(&mut cursor).map_err(|e| e.to_string())?;
operands.push(DisasmOperand::U32(v1)); operands.push(DisasmOperand::U32(v1));

View File

@ -8,7 +8,6 @@
//! //!
//! ## Core Components: //! ## Core Components:
//! - [`opcode`]: Defines the available instructions and their performance characteristics. //! - [`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. //! - [`abi`]: Specifies the binary rules for operands and stack behavior.
//! - [`asm`]: Provides a programmatic Assembler to convert high-level instructions to bytes. //! - [`asm`]: Provides a programmatic Assembler to convert high-level instructions to bytes.
//! - [`disasm`]: Provides a Disassembler to inspect compiled bytecode. //! - [`disasm`]: Provides a Disassembler to inspect compiled bytecode.
@ -16,7 +15,6 @@
pub mod opcode; pub mod opcode;
pub mod abi; pub mod abi;
pub mod pbc;
pub mod readwrite; pub mod readwrite;
pub mod asm; pub mod asm;
pub mod disasm; pub mod disasm;

View File

@ -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);
}
}

View File

@ -1,7 +1,27 @@
use crate::pbc::ConstantPoolEntry;
use crate::opcode::OpCode; use crate::opcode::OpCode;
use crate::abi::SourceSpan; 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)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum LoadError { pub enum LoadError {
InvalidMagic, InvalidMagic,
@ -56,6 +76,152 @@ pub struct BytecodeModule {
pub imports: Vec<Import>, 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(&current_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; pub struct BytecodeLoader;
impl BytecodeLoader { impl BytecodeLoader {

View File

@ -1,6 +1,7 @@
use crate::common::symbols::Symbol; use crate::common::symbols::Symbol;
use anyhow::{anyhow, Context, Result}; use anyhow::{Context, Result};
use prometeu_bytecode::disasm::disasm; use prometeu_bytecode::disasm::disasm;
use prometeu_bytecode::v0::BytecodeLoader;
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
@ -29,14 +30,14 @@ impl Artifacts {
if emit_disasm { if emit_disasm {
let disasm_path = out.with_extension("disasm.txt"); let disasm_path = out.with_extension("disasm.txt");
// Extract the actual bytecode (stripping the PBC header if present) // Extract the actual bytecode (stripping the industrial PBS\0 header)
let rom_to_disasm = if let Ok(pbc) = prometeu_bytecode::pbc::parse_pbc(&self.rom) { let rom_to_disasm = if let Ok(module) = BytecodeLoader::load(&self.rom) {
pbc.rom module.code
} else { } else {
self.rom.clone() 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(); let mut disasm_text = String::new();
for instr in instructions { for instr in instructions {

View File

@ -15,7 +15,8 @@ use crate::ir_core::ConstantValue;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use prometeu_bytecode::asm::{assemble, update_pc_by_operand, Asm, Operand}; use prometeu_bytecode::asm::{assemble, update_pc_by_operand, Asm, Operand};
use prometeu_bytecode::opcode::OpCode; 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. /// The final output of the code generation phase.
pub struct EmitResult { pub struct EmitResult {
@ -68,34 +69,30 @@ impl<'a> BytecodeEmitter<'a> {
self.add_constant(entry) self.add_constant(entry)
} }
/// Transforms an IR module into a binary PBC file. fn lower_instrs<'b>(
fn emit(&mut self, module: &ir_vm::Module) -> Result<EmitResult> { &mut self,
let mut asm_instrs = Vec::new(); module: &'b ir_vm::Module,
let mut ir_instr_map = Vec::new(); // Maps Asm index to IR instruction (for symbols) asm_instrs: &mut Vec<Asm>,
ir_instr_map: &mut Vec<Option<&'b ir_vm::Instruction>>,
// Pre-populate constant pool from IR and create a mapping for ConstIds mapped_const_ids: &[u32]
let mut mapped_const_ids = Vec::with_capacity(module.const_pool.constants.len()); ) -> Result<Vec<(usize, usize)>> {
for val in &module.const_pool.constants {
mapped_const_ids.push(self.add_ir_constant(val));
}
// Map FunctionIds to names for call resolution
let mut func_names = std::collections::HashMap::new(); let mut func_names = std::collections::HashMap::new();
for func in &module.functions { for func in &module.functions {
func_names.insert(func.id, func.name.clone()); 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 { for function in &module.functions {
let start_idx = asm_instrs.len();
// Each function starts with a label for its entry point. // Each function starts with a label for its entry point.
asm_instrs.push(Asm::Label(function.name.clone())); asm_instrs.push(Asm::Label(function.name.clone()));
ir_instr_map.push(None); ir_instr_map.push(None);
for instr in &function.body { 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. // Translate each IR instruction to its equivalent Bytecode OpCode.
// Note: IR instructions are high-level, while Bytecode is low-level.
match &instr.kind { match &instr.kind {
InstrKind::Nop => asm_instrs.push(Asm::Op(OpCode::Nop, vec![])), InstrKind::Nop => asm_instrs.push(Asm::Op(OpCode::Nop, vec![])),
InstrKind::Halt => asm_instrs.push(Asm::Op(OpCode::Halt, 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)])); asm_instrs.push(Asm::Op(OpCode::SetGlobal, vec![Operand::U32(*slot)]));
} }
InstrKind::Jmp(label) => { 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) => { 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) => { InstrKind::Label(label) => {
asm_instrs.push(Asm::Label(label.0.clone())); 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))?; 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::Ret => asm_instrs.push(Asm::Op(OpCode::Ret, vec![])),
InstrKind::Syscall(id) => { InstrKind::Syscall(id) => {
@ -183,24 +180,75 @@ impl<'a> BytecodeEmitter<'a> {
InstrKind::GateRelease => asm_instrs.push(Asm::Op(OpCode::GateRelease, vec![])), InstrKind::GateRelease => asm_instrs.push(Asm::Op(OpCode::GateRelease, vec![])),
} }
let end_idx = asm_instrs.len(); let op_end_idx = asm_instrs.len();
for _ in start_idx..end_idx { for _ in op_start_idx..op_end_idx {
ir_instr_map.push(Some(instr)); 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) --- let mut asm_instrs = Vec::new();
// Converts the list of Ops and Labels into raw bytes, calculating jump offsets. 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))?; let bytecode = assemble(&asm_instrs).map_err(|e| anyhow!(e))?;
// --- PHASE 3: Symbol Generation --- let mut functions = Vec::new();
// Associates each bytecode offset with a line/column in the source file. 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 symbols = Vec::new();
let mut current_pc = 0u32; for (i, instr_opt) in ir_instr_map.iter().enumerate() {
for (i, asm) in asm_instrs.iter().enumerate() { let current_pc = pcs[i];
if let Some(ir_instr) = ir_instr_map[i] { if let Some(instr) = instr_opt {
if let Some(span) = ir_instr.span { 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 (line, col) = self.file_manager.lookup_pos(span.file_id, span.start);
let file_path = self.file_manager.get_path(span.file_id) let file_path = self.file_manager.get_path(span.file_id)
.map(|p| p.to_string_lossy().to_string()) .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. let mut exports = Vec::new();
match asm { let mut function_names = Vec::new();
Asm::Label(_) => {} for (i, func) in module.functions.iter().enumerate() {
Asm::Op(_opcode, operands) => { exports.push(prometeu_bytecode::v0::Export {
// Each OpCode takes 2 bytes (1 for opcode, 1 for padding/metadata) symbol: func.name.clone(),
current_pc += 2; func_idx: i as u32,
// Operands take additional space depending on their type. });
current_pc = update_pc_by_operand(current_pc, operands); function_names.push((i as u32, func.name.clone()));
}
}
} }
// --- PHASE 4: Serialization --- let bytecode_module = BytecodeModule {
// Packages the constant pool and bytecode into the final PBC format.
let pbc = PbcFile {
version: 0, version: 0,
cp: self.constant_pool.clone(), const_pool: self.constant_pool.clone(),
rom: bytecode, 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 { Ok(EmitResult {
rom: out, rom: bytecode_module.serialize(),
symbols, symbols,
}) })
} }
@ -252,7 +305,7 @@ mod tests {
use crate::ir_core::ids::FunctionId; use crate::ir_core::ids::FunctionId;
use crate::ir_core::const_pool::ConstantValue; use crate::ir_core::const_pool::ConstantValue;
use crate::common::files::FileManager; use crate::common::files::FileManager;
use prometeu_bytecode::pbc::{parse_pbc, ConstantPoolEntry}; use prometeu_bytecode::v0::{BytecodeLoader, ConstantPoolEntry};
#[test] #[test]
fn test_emit_module_with_const_pool() { 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::PushConst(ir_vm::ConstId(id_str.0)), None),
Instruction::new(InstrKind::Ret, None), Instruction::new(InstrKind::Ret, None),
], ],
param_slots: 0,
local_slots: 0,
return_slots: 0,
}; };
module.functions.push(function); module.functions.push(function);
@ -278,11 +334,11 @@ mod tests {
let file_manager = FileManager::new(); let file_manager = FileManager::new();
let result = emit_module(&module, &file_manager).expect("Failed to emit module"); 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.const_pool.len(), 3);
assert_eq!(pbc.cp[0], ConstantPoolEntry::Null); assert_eq!(pbc.const_pool[0], ConstantPoolEntry::Null);
assert_eq!(pbc.cp[1], ConstantPoolEntry::Int64(12345)); assert_eq!(pbc.const_pool[1], ConstantPoolEntry::Int64(12345));
assert_eq!(pbc.cp[2], ConstantPoolEntry::String("hello".to_string())); assert_eq!(pbc.const_pool[2], ConstantPoolEntry::String("hello".to_string()));
} }
} }

View File

@ -2,3 +2,5 @@ pub mod emit_bytecode;
pub mod artifacts; pub mod artifacts;
pub use emit_bytecode::emit_module; pub use emit_bytecode::emit_module;
pub use artifacts::Artifacts;
pub use emit_bytecode::EmitResult;

View File

@ -106,7 +106,7 @@ mod tests {
use super::*; use super::*;
use std::fs; use std::fs;
use tempfile::tempdir; use tempfile::tempdir;
use prometeu_bytecode::pbc::parse_pbc; use prometeu_bytecode::v0::BytecodeLoader;
use prometeu_bytecode::disasm::disasm; use prometeu_bytecode::disasm::disasm;
use prometeu_bytecode::opcode::OpCode; use prometeu_bytecode::opcode::OpCode;
@ -152,8 +152,8 @@ mod tests {
fs::write(project_dir.join("main.pbs"), code).unwrap(); fs::write(project_dir.join("main.pbs"), code).unwrap();
let unit = compile(project_dir).expect("Failed to compile"); let unit = compile(project_dir).expect("Failed to compile");
let pbc = parse_pbc(&unit.rom).expect("Failed to parse PBC"); let pbc = BytecodeLoader::load(&unit.rom).expect("Failed to parse PBC");
let instrs = disasm(&pbc.rom).expect("Failed to disassemble"); let instrs = disasm(&pbc.code).expect("Failed to disassemble");
let opcodes: Vec<_> = instrs.iter().map(|i| i.opcode).collect(); 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(); fs::write(project_dir.join("main.pbs"), code).unwrap();
let unit = compile(project_dir).expect("Failed to compile"); let unit = compile(project_dir).expect("Failed to compile");
let pbc = parse_pbc(&unit.rom).expect("Failed to parse PBC"); let pbc = BytecodeLoader::load(&unit.rom).expect("Failed to parse PBC");
let instrs = disasm(&pbc.rom).expect("Failed to disassemble"); let instrs = disasm(&pbc.code).expect("Failed to disassemble");
let mut disasm_text = String::new(); let mut disasm_text = String::new();
for instr in instrs { for instr in instrs {
@ -230,37 +230,36 @@ mod tests {
0028 GetLocal U32(0) 0028 GetLocal U32(0)
002E PushConst U32(4) 002E PushConst U32(4)
0034 Gt 0034 Gt
0036 JmpIfFalse U32(94) 0036 JmpIfFalse U32(74)
003C Jmp U32(66) 003C Jmp U32(50)
0042 GetLocal U32(0) 0042 GetLocal U32(0)
0048 Call U32(0) U32(1) 0048 Call U32(0)
0052 SetLocal U32(1) 004E SetLocal U32(1)
0058 Jmp U32(100) 0054 Jmp U32(80)
005E Jmp U32(100) 005A Jmp U32(80)
0064 Alloc U32(2) U32(1) 0060 Alloc U32(2) U32(1)
006E SetLocal U32(1) 006A SetLocal U32(1)
0074 GetLocal U32(1) 0070 GetLocal U32(1)
007A GateRetain 0076 GateRetain
007C SetLocal U32(2) 0078 SetLocal U32(2)
0082 GetLocal U32(2) 007E GetLocal U32(2)
0088 GateRetain 0084 GateRetain
008A GateBeginMutate 0086 GateBeginMutate
008C GetLocal U32(2) 0088 GetLocal U32(2)
0092 GateRetain 008E GateRetain
0094 GateLoad U32(0) 0090 GateLoad U32(0)
009A SetLocal U32(3) 0096 SetLocal U32(3)
00A0 GetLocal U32(3) 009C GetLocal U32(3)
00A6 PushConst U32(5) 00A2 PushConst U32(5)
00AC Add 00A8 Add
00AE SetLocal U32(4) 00AA SetLocal U32(4)
00B4 GateEndMutate 00B0 GateEndMutate
00B6 GateRelease 00B2 GateRelease
00B8 GetLocal U32(1) 00B4 GetLocal U32(1)
00BE GateRelease 00BA GateRelease
00C0 GetLocal U32(2) 00BC GetLocal U32(2)
00C6 GateRelease 00C2 GateRelease
00C8 PushConst U32(0) 00C4 Ret
00CE Ret
"#; "#;
assert_eq!(disasm_text, expected_disasm); assert_eq!(disasm_text, expected_disasm);
@ -269,7 +268,6 @@ mod tests {
#[test] #[test]
fn test_hip_conformance_v0() { fn test_hip_conformance_v0() {
use crate::ir_core::*; use crate::ir_core::*;
use crate::ir_core::ids::*;
use crate::lowering::lower_program; use crate::lowering::lower_program;
use crate::backend; use crate::backend;
use std::collections::HashMap; use std::collections::HashMap;
@ -294,6 +292,9 @@ mod tests {
functions: vec![Function { functions: vec![Function {
id: FunctionId(1), id: FunctionId(1),
name: "main".to_string(), name: "main".to_string(),
param_slots: 0,
local_slots: 0,
return_slots: 0,
params: vec![], params: vec![],
return_type: Type::Void, return_type: Type::Void,
blocks: vec![Block { blocks: vec![Block {
@ -351,23 +352,22 @@ mod tests {
let rom = emit_result.rom; let rom = emit_result.rom;
// --- 5. ASSERT GOLDEN BYTECODE (Exact Bytes) --- // --- 5. ASSERT INDUSTRIAL FORMAT ---
// Header: PPBC, Version: 0, Flags: 0 use prometeu_bytecode::v0::BytecodeLoader;
assert_eq!(&rom[0..4], b"PPBC"); let pbc = BytecodeLoader::load(&rom).expect("Failed to parse industrial PBC");
assert_eq!(rom[4..6], [0, 0]); // Version 0
assert_eq!(rom[6..8], [0, 0]); // Flags 0
// CP Count: 2 (Null, 42) assert_eq!(&rom[0..4], b"PBS\0");
assert_eq!(rom[8..12], [2, 0, 0, 0]); assert_eq!(pbc.const_pool.len(), 2); // Null, 42
// ROM Data contains HIP opcodes: // ROM Data contains HIP opcodes:
assert!(rom.contains(&0x60), "Bytecode must contain Alloc (0x60)"); let code = pbc.code;
assert!(rom.contains(&0x67), "Bytecode must contain GateBeginMutate (0x67)"); assert!(code.iter().any(|&b| b == 0x60), "Bytecode must contain Alloc (0x60)");
assert!(rom.contains(&0x62), "Bytecode must contain GateStore (0x62)"); assert!(code.iter().any(|&b| b == 0x67), "Bytecode must contain GateBeginMutate (0x67)");
assert!(rom.contains(&0x63), "Bytecode must contain GateBeginPeek (0x63)"); assert!(code.iter().any(|&b| b == 0x62), "Bytecode must contain GateStore (0x62)");
assert!(rom.contains(&0x61), "Bytecode must contain GateLoad (0x61)"); assert!(code.iter().any(|&b| b == 0x63), "Bytecode must contain GateBeginPeek (0x63)");
assert!(rom.contains(&0x69), "Bytecode must contain GateRetain (0x69)"); assert!(code.iter().any(|&b| b == 0x61), "Bytecode must contain GateLoad (0x61)");
assert!(rom.contains(&0x6A), "Bytecode must contain GateRelease (0x6A)"); 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] #[test]

View File

@ -30,6 +30,8 @@ pub enum Node {
WhenArm(WhenArmNode), WhenArm(WhenArmNode),
TypeName(TypeNameNode), TypeName(TypeNameNode),
TypeApp(TypeAppNode), TypeApp(TypeAppNode),
ConstructorDecl(ConstructorDeclNode),
ConstantDecl(ConstantDeclNode),
Alloc(AllocNode), Alloc(AllocNode),
Mutate(MutateNode), Mutate(MutateNode),
Borrow(BorrowNode), Borrow(BorrowNode),
@ -98,13 +100,33 @@ pub struct TypeDeclNode {
pub type_kind: String, // "struct" | "contract" | "error" pub type_kind: String, // "struct" | "contract" | "error"
pub name: String, pub name: String,
pub is_host: bool, 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)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TypeBodyNode { pub struct TypeBodyNode {
pub span: Span, pub span: Span,
pub members: Vec<TypeMemberNode>, pub members: Vec<TypeMemberNode>,
pub methods: Vec<ServiceFnSigNode>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]

View File

@ -74,8 +74,22 @@ impl<'a> Lexer<'a> {
')' => TokenKind::CloseParen, ')' => TokenKind::CloseParen,
'{' => TokenKind::OpenBrace, '{' => TokenKind::OpenBrace,
'}' => TokenKind::CloseBrace, '}' => 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::Comma,
'.' => TokenKind::Dot, '.' => TokenKind::Dot,
':' => TokenKind::Colon, ':' => TokenKind::Colon,

View File

@ -26,8 +26,12 @@ pub struct Lowerer<'a> {
function_ids: HashMap<String, FunctionId>, function_ids: HashMap<String, FunctionId>,
type_ids: HashMap<String, TypeId>, type_ids: HashMap<String, TypeId>,
struct_slots: HashMap<String, u32>, 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, contract_registry: ContractRegistry,
diagnostics: Vec<Diagnostic>, diagnostics: Vec<Diagnostic>,
max_slots_used: u32,
} }
impl<'a> Lowerer<'a> { impl<'a> Lowerer<'a> {
@ -58,8 +62,12 @@ impl<'a> Lowerer<'a> {
function_ids: HashMap::new(), function_ids: HashMap::new(),
type_ids: HashMap::new(), type_ids: HashMap::new(),
struct_slots, struct_slots,
struct_constructors: HashMap::new(),
type_constants: HashMap::new(),
current_type_context: None,
contract_registry: ContractRegistry::new(), contract_registry: ContractRegistry::new(),
diagnostics: Vec::new(), diagnostics: Vec::new(),
max_slots_used: 0,
} }
} }
@ -84,15 +92,92 @@ impl<'a> Lowerer<'a> {
let id = TypeId(self.next_type_id); let id = TypeId(self.next_type_id);
self.next_type_id += 1; self.next_type_id += 1;
self.type_ids.insert(n.name.clone(), id); 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 n.type_kind == "struct" {
if let Node::TypeBody(body) = &*n.body { struct_nodes.insert(n.name.clone(), n);
self.struct_slots.insert(n.name.clone(), body.members.len() as u32); }
}
}
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(&param.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 { let mut module = Module {
name: module_name.to_string(), name: module_name.to_string(),
functions: Vec::new(), functions: Vec::new(),
@ -118,24 +203,32 @@ impl<'a> Lowerer<'a> {
let func_id = *self.function_ids.get(&n.name).unwrap(); let func_id = *self.function_ids.get(&n.name).unwrap();
self.next_block_id = 0; self.next_block_id = 0;
self.local_vars = vec![HashMap::new()]; self.local_vars = vec![HashMap::new()];
self.max_slots_used = 0;
let mut params = Vec::new(); let mut params = Vec::new();
let mut local_types = HashMap::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(&param.ty); let ty = self.lower_type_node(&param.ty);
let slots = self.get_type_slots(&ty);
params.push(Param { params.push(Param {
name: param.name.clone(), name: param.name.clone(),
ty: ty.clone(), ty: ty.clone(),
}); });
self.local_vars[0].insert(param.name.clone(), LocalInfo { slot: i as u32, ty: ty.clone() }); self.local_vars[0].insert(param.name.clone(), LocalInfo { slot: param_slots, ty: ty.clone() });
local_types.insert(i as u32, ty); 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 { let ret_ty = if let Some(ret) = &n.ret {
self.lower_type_node(ret) self.lower_type_node(ret)
} else { } else {
Type::Void Type::Void
}; };
let return_slots = self.get_type_slots(&ret_ty);
let func = Function { let func = Function {
id: func_id, id: func_id,
@ -144,6 +237,9 @@ impl<'a> Lowerer<'a> {
return_type: ret_ty, return_type: ret_ty,
blocks: Vec::new(), blocks: Vec::new(),
local_types, local_types,
param_slots: param_slots as u16,
local_slots: 0,
return_slots: return_slots as u16,
}; };
self.current_function = Some(func); 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<(), ()> { fn lower_node(&mut self, node: &Node) -> Result<(), ()> {
@ -258,8 +356,7 @@ impl<'a> Lowerer<'a> {
self.lower_node(&n.target)?; self.lower_node(&n.target)?;
// 2. Preserve gate identity // 2. Preserve gate identity
let gate_slot = self.get_next_local_slot(); let gate_slot = self.add_local_to_scope(format!("$gate_{}", self.get_next_local_slot()), Type::Int);
self.local_vars.last_mut().unwrap().insert(format!("$gate_{}", gate_slot), LocalInfo { slot: gate_slot, ty: Type::Int });
self.emit(Instr::SetLocal(gate_slot)); self.emit(Instr::SetLocal(gate_slot));
// 3. Begin Operation // 3. Begin Operation
@ -268,8 +365,7 @@ impl<'a> Lowerer<'a> {
// 4. Bind view to local // 4. Bind view to local
self.local_vars.push(HashMap::new()); self.local_vars.push(HashMap::new());
let view_slot = self.get_next_local_slot(); let view_slot = self.add_local_to_scope(n.binding.to_string(), Type::Int);
self.local_vars.last_mut().unwrap().insert(n.binding.to_string(), LocalInfo { slot: view_slot, ty: Type::Int });
self.emit(Instr::SetLocal(view_slot)); self.emit(Instr::SetLocal(view_slot));
// 5. Body // 5. Body
@ -287,8 +383,7 @@ impl<'a> Lowerer<'a> {
self.lower_node(&n.target)?; self.lower_node(&n.target)?;
// 2. Preserve gate identity // 2. Preserve gate identity
let gate_slot = self.get_next_local_slot(); let gate_slot = self.add_local_to_scope(format!("$gate_{}", self.get_next_local_slot()), Type::Int);
self.local_vars.last_mut().unwrap().insert(format!("$gate_{}", gate_slot), LocalInfo { slot: gate_slot, ty: Type::Int });
self.emit(Instr::SetLocal(gate_slot)); self.emit(Instr::SetLocal(gate_slot));
// 3. Begin Operation // 3. Begin Operation
@ -297,8 +392,7 @@ impl<'a> Lowerer<'a> {
// 4. Bind view to local // 4. Bind view to local
self.local_vars.push(HashMap::new()); self.local_vars.push(HashMap::new());
let view_slot = self.get_next_local_slot(); let view_slot = self.add_local_to_scope(n.binding.to_string(), Type::Int);
self.local_vars.last_mut().unwrap().insert(n.binding.to_string(), LocalInfo { slot: view_slot, ty: Type::Int });
self.emit(Instr::SetLocal(view_slot)); self.emit(Instr::SetLocal(view_slot));
// 5. Body // 5. Body
@ -316,8 +410,7 @@ impl<'a> Lowerer<'a> {
self.lower_node(&n.target)?; self.lower_node(&n.target)?;
// 2. Preserve gate identity // 2. Preserve gate identity
let gate_slot = self.get_next_local_slot(); let gate_slot = self.add_local_to_scope(format!("$gate_{}", self.get_next_local_slot()), Type::Int);
self.local_vars.last_mut().unwrap().insert(format!("$gate_{}", gate_slot), LocalInfo { slot: gate_slot, ty: Type::Int });
self.emit(Instr::SetLocal(gate_slot)); self.emit(Instr::SetLocal(gate_slot));
// 3. Begin Operation // 3. Begin Operation
@ -326,8 +419,7 @@ impl<'a> Lowerer<'a> {
// 4. Bind view to local // 4. Bind view to local
self.local_vars.push(HashMap::new()); self.local_vars.push(HashMap::new());
let view_slot = self.get_next_local_slot(); let view_slot = self.add_local_to_scope(n.binding.to_string(), Type::Int);
self.local_vars.last_mut().unwrap().insert(n.binding.to_string(), LocalInfo { slot: view_slot, ty: Type::Int });
self.emit(Instr::SetLocal(view_slot)); self.emit(Instr::SetLocal(view_slot));
// 5. Body // 5. Body
@ -372,10 +464,8 @@ impl<'a> Lowerer<'a> {
} else { Type::Int } } else { Type::Int }
}; };
let slot = self.get_next_local_slot();
let slots = self.get_type_slots(&ty); let slots = self.get_type_slots(&ty);
let slot = self.add_local_to_scope(n.name.clone(), ty);
self.local_vars.last_mut().unwrap().insert(n.name.clone(), LocalInfo { slot, ty });
for i in (0..slots).rev() { for i in (0..slots).rev() {
self.emit(Instr::SetLocal(slot + i)); self.emit(Instr::SetLocal(slot + i));
@ -434,6 +524,15 @@ impl<'a> Lowerer<'a> {
fn lower_member_access(&mut self, n: &MemberAccessNode) -> Result<(), ()> { fn lower_member_access(&mut self, n: &MemberAccessNode) -> Result<(), ()> {
if let Node::Ident(id) = &*n.object { 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" { if id.name == "Color" {
let val = match n.member.as_str() { let val = match n.member.as_str() {
"BLACK" => 0x0000, "BLACK" => 0x0000,
@ -539,6 +638,25 @@ impl<'a> Lowerer<'a> {
fn lower_call(&mut self, n: &CallNode) -> Result<(), ()> { fn lower_call(&mut self, n: &CallNode) -> Result<(), ()> {
match &*n.callee { match &*n.callee {
Node::Ident(id_node) => { 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 { for arg in &n.args {
self.lower_node(arg)?; self.lower_node(arg)?;
} }
@ -559,6 +677,19 @@ impl<'a> Lowerer<'a> {
} }
} }
Node::MemberAccess(ma) => { 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() // Check for Pad.any()
if ma.member == "any" { if ma.member == "any" {
if let Node::Ident(obj_id) = &*ma.object { 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, &param_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) { fn lower_pad_any(&mut self, base_slot: u32) {
for i in 0..12 { for i in 0..12 {
let btn_base = base_slot + (i * 4); 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() 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> { fn find_local(&self, name: &str) -> Option<LocalInfo> {
for scope in self.local_vars.iter().rev() { for scope in self.local_vars.iter().rev() {
if let Some(info) = scope.get(name) { if let Some(info) = scope.get(name) {
@ -817,10 +1016,26 @@ impl<'a> Lowerer<'a> {
None 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 { fn get_type_slots(&self, ty: &Type) -> u32 {
match ty { match ty {
Type::Void => 0, 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, Type::Array(_, size) => *size,
_ => 1, _ => 1,
} }
@ -1127,11 +1342,7 @@ mod tests {
#[test] #[test]
fn test_alloc_struct_slots() { fn test_alloc_struct_slots() {
let code = " let code = "
declare struct Vec3 { declare struct Vec3(x: int, y: int, z: int)
x: int,
y: int,
z: int
}
fn main() { fn main() {
let v = alloc Vec3; let v = alloc Vec3;
} }

View File

@ -225,49 +225,107 @@ impl Parser {
}; };
let name = self.expect_identifier()?; 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; let mut is_host = false;
if self.peek().kind == TokenKind::Host { if self.peek().kind == TokenKind::Host {
self.advance(); self.advance();
is_host = true; 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 { 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, vis,
type_kind, type_kind,
name, name,
is_host, is_host,
body: Box::new(body), params,
constructors,
constants,
body,
})) }))
} }
fn parse_type_body(&mut self) -> Result<Node, DiagnosticBundle> { fn parse_type_body(&mut self) -> Result<Node, DiagnosticBundle> {
let start_span = self.consume(TokenKind::OpenBrace)?.span; let start_span = self.consume(TokenKind::OpenBrace)?.span;
let mut members = Vec::new(); let mut members = Vec::new();
let mut methods = Vec::new();
while self.peek().kind != TokenKind::CloseBrace && self.peek().kind != TokenKind::Eof { while self.peek().kind != TokenKind::CloseBrace && self.peek().kind != TokenKind::Eof {
let m_start = self.peek().span.start; if self.peek().kind == TokenKind::Fn {
let name = self.expect_identifier()?; let sig_node = self.parse_service_member()?;
self.consume(TokenKind::Colon)?; if let Node::ServiceFnSig(sig) = sig_node {
let ty = self.parse_type_ref()?; methods.push(sig);
let m_end = ty.span().end; }
members.push(TypeMemberNode { if self.peek().kind == TokenKind::Semicolon {
span: Span::new(self.file_id, m_start, m_end), self.advance();
name, }
ty: Box::new(ty)
});
if self.peek().kind == TokenKind::Comma {
self.advance();
} else { } 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; let end_span = self.consume(TokenKind::CloseBrace)?.span;
Ok(Node::TypeBody(TypeBodyNode { Ok(Node::TypeBody(TypeBodyNode {
span: Span::new(self.file_id, start_span.start, end_span.end), span: Span::new(self.file_id, start_span.start, end_span.end),
members, members,
methods,
})) }))
} }
@ -344,6 +402,22 @@ impl Parser {
self.advance(); self.advance();
"bounded".to_string() "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"))), _ => return Err(self.error_with_code("Expected type name", Some("E_PARSE_EXPECTED_TOKEN"))),
}; };
let mut node = if self.peek().kind == TokenKind::Lt { let mut node = if self.peek().kind == TokenKind::Lt {
@ -819,6 +893,10 @@ impl Parser {
self.advance(); self.advance();
Ok("err".to_string()) Ok("err".to_string())
} }
TokenKind::Bounded => {
self.advance();
Ok("bounded".to_string())
}
TokenKind::Invalid(msg) => { TokenKind::Invalid(msg) => {
let code = if msg.contains("Unterminated string") { let code = if msg.contains("Unterminated string") {
"E_LEX_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(&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()); self.errors.push(diag.clone());
DiagnosticBundle::from(diag) 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 { impl Node {
@ -876,6 +993,8 @@ impl Node {
Node::WhenArm(n) => n.span, Node::WhenArm(n) => n.span,
Node::TypeName(n) => n.span, Node::TypeName(n) => n.span,
Node::TypeApp(n) => n.span, Node::TypeApp(n) => n.span,
Node::ConstructorDecl(n) => n.span,
Node::ConstantDecl(n) => n.span,
Node::Alloc(n) => n.span, Node::Alloc(n) => n.span,
Node::Mutate(n) => n.span, Node::Mutate(n) => n.span,
Node::Borrow(n) => n.span, Node::Borrow(n) => n.span,

View File

@ -146,6 +146,8 @@ impl<'a> Resolver<'a> {
self.resolve_type_ref(arg); self.resolve_type_ref(arg);
} }
} }
Node::ConstructorDecl(n) => self.resolve_constructor_decl(n),
Node::ConstantDecl(n) => self.resolve_node(&n.value),
Node::Alloc(n) => { Node::Alloc(n) => {
self.resolve_type_ref(&n.ty); self.resolve_type_ref(&n.ty);
} }
@ -217,13 +219,48 @@ impl<'a> Resolver<'a> {
} }
fn resolve_type_decl(&mut self, n: &TypeDeclNode) { fn resolve_type_decl(&mut self, n: &TypeDeclNode) {
if let Node::TypeBody(body) = &*n.body { for param in &n.params {
for member in &body.members { self.resolve_type_ref(&param.ty);
self.resolve_type_ref(&member.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(&param.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(&param.ty);
self.define_local(&param.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) { fn resolve_block(&mut self, n: &BlockNode) {
self.enter_scope(); self.enter_scope();
for stmt in &n.stmts { for stmt in &n.stmts {
@ -311,6 +348,20 @@ impl<'a> Resolver<'a> {
return Some(sym.clone()); 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 None
} }

View File

@ -52,6 +52,8 @@ pub enum TokenKind {
CloseBrace, // } CloseBrace, // }
OpenBracket, // [ OpenBracket, // [
CloseBracket, // ] CloseBracket, // ]
OpenDoubleBracket, // [[
CloseDoubleBracket, // ]]
Comma, // , Comma, // ,
Dot, // . Dot, // .
Colon, // : Colon, // :

View File

@ -13,6 +13,9 @@ pub struct TypeChecker<'a> {
scopes: Vec<HashMap<String, PbsType>>, scopes: Vec<HashMap<String, PbsType>>,
mut_bindings: Vec<HashMap<String, bool>>, mut_bindings: Vec<HashMap<String, bool>>,
current_return_type: Option<PbsType>, 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>, diagnostics: Vec<Diagnostic>,
contract_registry: ContractRegistry, contract_registry: ContractRegistry,
} }
@ -28,6 +31,9 @@ impl<'a> TypeChecker<'a> {
scopes: Vec::new(), scopes: Vec::new(),
mut_bindings: Vec::new(), mut_bindings: Vec::new(),
current_return_type: None, current_return_type: None,
struct_constructors: HashMap::new(),
struct_constants: HashMap::new(),
struct_methods: HashMap::new(),
diagnostics: Vec::new(), diagnostics: Vec::new(),
contract_registry: ContractRegistry::new(), contract_registry: ContractRegistry::new(),
} }
@ -86,8 +92,62 @@ impl<'a> TypeChecker<'a> {
_ => PbsType::Void, _ => PbsType::Void,
}; };
if let Some(sym) = self.module_symbols.type_symbols.symbols.get_mut(&n.name) { 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); self.check_fn_decl(n);
PbsType::Void 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::Block(n) => self.check_block(n),
Node::LetStmt(n) => { Node::LetStmt(n) => {
self.check_let_stmt(n); self.check_let_stmt(n);
@ -166,26 +235,45 @@ impl<'a> TypeChecker<'a> {
} }
// Builtin Struct Associated Members (Static/Constants) // Builtin Struct Associated Members (Static/Constants)
match id.name.as_str() { if let Some(constants) = self.struct_constants.get(&id.name) {
"Color" => { if let Some(ty) = constants.get(&n.member) {
match n.member.as_str() { return ty.clone();
"BLACK" | "WHITE" | "RED" | "GREEN" | "BLUE" | "MAGENTA" | "TRANSPARENT" | "COLOR_KEY" => { }
return PbsType::Struct("Color".to_string()); }
}
"rgb" => { // Fallback for constructors if used as Type.alias(...)
return PbsType::Function { if let Some(ctors) = self.struct_constructors.get(&id.name) {
params: vec![PbsType::Int, PbsType::Int, PbsType::Int], if let Some(ty) = ctors.get(&n.member) {
return_type: Box::new(PbsType::Struct("Color".to_string())), 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); 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 { match obj_ty {
PbsType::Struct(ref name) => { PbsType::Struct(ref name) => {
match name.as_str() { match name.as_str() {
@ -234,10 +322,11 @@ impl<'a> TypeChecker<'a> {
} }
if obj_ty != PbsType::Void { if obj_ty != PbsType::Void {
let msg = format!("Member '{}' not found on type {:?}", n.member, obj_ty);
self.diagnostics.push(Diagnostic { self.diagnostics.push(Diagnostic {
level: DiagnosticLevel::Error, level: DiagnosticLevel::Error,
code: Some("E_RESOLVE_UNDEFINED".to_string()), code: Some("E_RESOLVE_UNDEFINED".to_string()),
message: format!("Member '{}' not found on type {}", n.member, obj_ty), message: msg,
span: Some(n.span), 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) // 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 // For v0, let's treat none as a special literal or identifier
if n.name == "none" { if n.name == "none" {
@ -544,6 +640,49 @@ impl<'a> TypeChecker<'a> {
first_ty.unwrap_or(PbsType::Void) 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(&param.ty);
self.define_local(&param.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 { fn resolve_type_node(&mut self, node: &Node) -> PbsType {
match node { match node {
Node::TypeName(tn) => { Node::TypeName(tn) => {
@ -705,7 +844,21 @@ mod tests {
let mut file_manager = FileManager::new(); let mut file_manager = FileManager::new();
let temp_dir = tempfile::tempdir().unwrap(); let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test.pbs"); 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; let frontend = PbsFrontend;
match frontend.compile_to_ir(&file_path, &mut file_manager) { match frontend.compile_to_ir(&file_path, &mut file_manager) {

View File

@ -21,4 +21,8 @@ pub struct Function {
pub blocks: Vec<Block>, pub blocks: Vec<Block>,
#[serde(default)] #[serde(default)]
pub local_types: HashMap<u32, Type>, pub local_types: HashMap<u32, Type>,
pub param_slots: u16,
pub local_slots: u16,
pub return_slots: u16,
} }

View File

@ -37,6 +37,9 @@ mod tests {
functions: vec![Function { functions: vec![Function {
id: FunctionId(10), id: FunctionId(10),
name: "entry".to_string(), name: "entry".to_string(),
param_slots: 0,
local_slots: 0,
return_slots: 0,
params: vec![], params: vec![],
return_type: Type::Void, return_type: Type::Void,
blocks: vec![Block { blocks: vec![Block {
@ -90,7 +93,10 @@ mod tests {
"terminator": "Return" "terminator": "Return"
} }
], ],
"local_types": {} "local_types": {},
"param_slots": 0,
"local_slots": 0,
"return_slots": 0
} }
] ]
} }

View File

@ -158,6 +158,9 @@ mod tests {
Function { Function {
id: FunctionId(1), id: FunctionId(1),
name: "test".to_string(), name: "test".to_string(),
param_slots: 0,
local_slots: 0,
return_slots: 0,
params: vec![], params: vec![],
return_type: Type::Void, return_type: Type::Void,
blocks, blocks,

View File

@ -51,6 +51,9 @@ mod tests {
functions: vec![Function { functions: vec![Function {
id: FunctionId(1), id: FunctionId(1),
name: "main".to_string(), name: "main".to_string(),
param_slots: 0,
local_slots: 0,
return_slots: 0,
params: vec![], params: vec![],
return_type: Type::Null, return_type: Type::Null,
body: vec![ body: vec![
@ -99,7 +102,10 @@ mod tests {
"kind": "Ret", "kind": "Ret",
"span": null "span": null
} }
] ],
"param_slots": 0,
"local_slots": 0,
"return_slots": 0
} }
], ],
"globals": [] "globals": []
@ -122,6 +128,9 @@ mod tests {
functions: vec![ir_core::Function { functions: vec![ir_core::Function {
id: FunctionId(10), id: FunctionId(10),
name: "start".to_string(), name: "start".to_string(),
param_slots: 0,
local_slots: 0,
return_slots: 0,
params: vec![], params: vec![],
return_type: ir_core::Type::Void, return_type: ir_core::Type::Void,
blocks: vec![ir_core::Block { blocks: vec![ir_core::Block {
@ -146,7 +155,7 @@ mod tests {
assert_eq!(func.name, "start"); assert_eq!(func.name, "start");
assert_eq!(func.id, FunctionId(10)); assert_eq!(func.id, FunctionId(10));
assert_eq!(func.body.len(), 4); assert_eq!(func.body.len(), 3);
match &func.body[0].kind { match &func.body[0].kind {
InstrKind::Label(Label(l)) => assert!(l.contains("block_0")), InstrKind::Label(Label(l)) => assert!(l.contains("block_0")),
_ => panic!("Expected label"), _ => panic!("Expected label"),
@ -156,10 +165,6 @@ mod tests {
_ => panic!("Expected PushConst"), _ => panic!("Expected PushConst"),
} }
match &func.body[2].kind { match &func.body[2].kind {
InstrKind::PushNull => (),
_ => panic!("Expected PushNull"),
}
match &func.body[3].kind {
InstrKind::Ret => (), InstrKind::Ret => (),
_ => panic!("Expected Ret"), _ => panic!("Expected Ret"),
} }

View File

@ -40,6 +40,10 @@ pub struct Function {
pub return_type: Type, pub return_type: Type,
/// The sequence of instructions that make up the function's logic. /// The sequence of instructions that make up the function's logic.
pub body: Vec<Instruction>, pub body: Vec<Instruction>,
pub param_slots: u16,
pub local_slots: u16,
pub return_slots: u16,
} }
/// A parameter passed to a function. /// A parameter passed to a function.

View File

@ -52,6 +52,9 @@ pub fn lower_function(
}).collect(), }).collect(),
return_type: lower_type(&core_func.return_type), return_type: lower_type(&core_func.return_type),
body: vec![], 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 // 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. // If the function is Void, we don't need to push anything.
// The VM always pops one value from the stack to be the return value. // The VM's Ret opcode handles zero return slots correctly.
if vm_func.return_type == ir_vm::Type::Void {
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::PushNull, None));
}
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Ret, None)); vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Ret, None));
} }
ir_core::Terminator::Jump(target) => { ir_core::Terminator::Jump(target) => {
@ -424,6 +423,9 @@ mod tests {
}, },
], ],
local_types: HashMap::new(), local_types: HashMap::new(),
param_slots: 0,
local_slots: 0,
return_slots: 0,
}], }],
}], }],
field_offsets: std::collections::HashMap::new(), field_offsets: std::collections::HashMap::new(),
@ -436,7 +438,7 @@ mod tests {
let func = &vm_module.functions[0]; let func = &vm_module.functions[0];
assert_eq!(func.name, "main"); assert_eq!(func.name, "main");
assert_eq!(func.body.len(), 8); assert_eq!(func.body.len(), 7);
match &func.body[0].kind { match &func.body[0].kind {
InstrKind::Label(Label(l)) => assert_eq!(l, "block_0"), InstrKind::Label(Label(l)) => assert_eq!(l, "block_0"),
@ -466,10 +468,6 @@ mod tests {
_ => panic!("Expected HostCall 42"), _ => panic!("Expected HostCall 42"),
} }
match &func.body[6].kind { match &func.body[6].kind {
InstrKind::PushNull => (),
_ => panic!("Expected PushNull"),
}
match &func.body[7].kind {
InstrKind::Ret => (), InstrKind::Ret => (),
_ => panic!("Expected Ret"), _ => panic!("Expected Ret"),
} }
@ -500,6 +498,9 @@ mod tests {
terminator: Terminator::Return, terminator: Terminator::Return,
}], }],
local_types: HashMap::new(), local_types: HashMap::new(),
param_slots: 0,
local_slots: 0,
return_slots: 0,
}], }],
}], }],
field_offsets, field_offsets,
@ -518,7 +519,7 @@ mod tests {
// GateStore 100 (offset) // GateStore 100 (offset)
// Ret // Ret
assert_eq!(func.body.len(), 10); assert_eq!(func.body.len(), 9);
match &func.body[1].kind { match &func.body[1].kind {
ir_vm::InstrKind::LocalLoad { slot } => assert_eq!(*slot, 0), ir_vm::InstrKind::LocalLoad { slot } => assert_eq!(*slot, 0),
_ => panic!("Expected LocalLoad 0"), _ => panic!("Expected LocalLoad 0"),
@ -536,10 +537,6 @@ mod tests {
_ => panic!("Expected GateStore 100"), _ => panic!("Expected GateStore 100"),
} }
match &func.body[8].kind { match &func.body[8].kind {
ir_vm::InstrKind::PushNull => (),
_ => panic!("Expected PushNull"),
}
match &func.body[9].kind {
ir_vm::InstrKind::Ret => (), ir_vm::InstrKind::Ret => (),
_ => panic!("Expected Ret"), _ => panic!("Expected Ret"),
} }
@ -564,6 +561,9 @@ mod tests {
terminator: Terminator::Return, terminator: Terminator::Return,
}], }],
local_types: HashMap::new(), local_types: HashMap::new(),
param_slots: 0,
local_slots: 0,
return_slots: 0,
}], }],
}], }],
field_offsets: std::collections::HashMap::new(), field_offsets: std::collections::HashMap::new(),
@ -609,6 +609,9 @@ mod tests {
terminator: Terminator::Return, terminator: Terminator::Return,
}], }],
local_types: HashMap::new(), local_types: HashMap::new(),
param_slots: 0,
local_slots: 0,
return_slots: 0,
}], }],
}], }],
field_offsets: HashMap::new(), field_offsets: HashMap::new(),
@ -635,10 +638,10 @@ mod tests {
assert!(found_overwrite, "Should have emitted release-then-store sequence for overwrite"); assert!(found_overwrite, "Should have emitted release-then-store sequence for overwrite");
// Check Ret cleanup: // Check Ret cleanup:
// LocalLoad 1, GateRelease, PushNull, Ret // LocalLoad 1, GateRelease, Ret
let mut found_cleanup = false; let mut found_cleanup = false;
for i in 0..kinds.len() - 3 { for i in 0..kinds.len() - 2 {
if let (InstrKind::LocalLoad { slot: 1 }, InstrKind::GateRelease, InstrKind::PushNull, InstrKind::Ret) = (kinds[i], kinds[i+1], kinds[i+2], kinds[i+3]) { if let (InstrKind::LocalLoad { slot: 1 }, InstrKind::GateRelease, InstrKind::Ret) = (kinds[i], kinds[i+1], kinds[i+2]) {
found_cleanup = true; found_cleanup = true;
break; break;
} }
@ -671,6 +674,9 @@ mod tests {
terminator: Terminator::Return, terminator: Terminator::Return,
}], }],
local_types: HashMap::new(), local_types: HashMap::new(),
param_slots: 0,
local_slots: 0,
return_slots: 0,
}], }],
}], }],
field_offsets: HashMap::new(), field_offsets: HashMap::new(),

View 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/");
}

View File

@ -28,6 +28,9 @@ fn test_hip_conformance_core_to_vm_to_bytecode() {
functions: vec![ir_core::Function { functions: vec![ir_core::Function {
id: FunctionId(1), id: FunctionId(1),
name: "main".to_string(), name: "main".to_string(),
param_slots: 0,
local_slots: 0,
return_slots: 0,
params: vec![], params: vec![],
return_type: ir_core::Type::Void, return_type: ir_core::Type::Void,
local_types: HashMap::new(), 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 emit_result = emit_module(&vm_module, &file_manager).expect("Emission failed");
let bytecode = emit_result.rom; let bytecode = emit_result.rom;
// 4. Assert exact bytes match frozen ISA/ABI // 4. Assert industrial PBS\0 format
let expected_bytecode = vec![ use prometeu_bytecode::v0::BytecodeLoader;
0x50, 0x50, 0x42, 0x43, // Magic: "PPBC" let module = BytecodeLoader::load(&bytecode).expect("Failed to parse industrial PBC");
0x00, 0x00, // Version: 0 assert_eq!(&bytecode[0..4], b"PBS\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
];
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]));
} }

View File

@ -434,11 +434,22 @@ mod tests {
let mut hw = Hardware::new(); let mut hw = Hardware::new();
let signals = InputSignals::default(); let signals = InputSignals::default();
let rom = prometeu_bytecode::pbc::write_pbc(&prometeu_bytecode::pbc::PbcFile { let rom = prometeu_bytecode::v0::BytecodeModule {
version: 0, version: 0,
cp: vec![], const_pool: vec![],
rom: vec![0x02, 0x00, 0x00, 0x00, 0x00, 0x00], functions: vec![prometeu_bytecode::v0::FunctionMeta {
}).unwrap(); 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 { let cartridge = Cartridge {
app_id: 1234, app_id: 1234,
title: "test".to_string(), title: "test".to_string(),
@ -477,14 +488,25 @@ mod tests {
// PUSH_CONST 0 (dummy) // PUSH_CONST 0 (dummy)
// FrameSync (0x80) // FrameSync (0x80)
// JMP 0 // JMP 0
let rom = prometeu_bytecode::pbc::write_pbc(&prometeu_bytecode::pbc::PbcFile { let rom = prometeu_bytecode::v0::BytecodeModule {
version: 0, version: 0,
cp: vec![], const_pool: vec![],
rom: 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) 0x80, 0x00, // FrameSync (2 bytes opcode)
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, // JMP 0 (2 bytes opcode + 4 bytes u32) 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 { let cartridge = Cartridge {
app_id: 1234, app_id: 1234,
title: "test".to_string(), title: "test".to_string(),
@ -680,16 +702,27 @@ mod tests {
let signals = InputSignals::default(); let signals = InputSignals::default();
// PushI32 0 (0x17), then Ret (0x51) // 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, version: 0,
cp: vec![], const_pool: vec![],
rom: 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 0x17, 0x00, // PushI32
0x00, 0x00, 0x00, 0x00, // value 0 0x00, 0x00, 0x00, 0x00, // value 0
0x11, 0x00, // Pop 0x11, 0x00, // Pop
0x51, 0x00 // Ret 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 { let cartridge = Cartridge {
app_id: 1234, app_id: 1234,
title: "test".to_string(), title: "test".to_string(),

View File

@ -1,6 +1,5 @@
use crate::virtual_machine::{ProgramImage, Value}; use crate::virtual_machine::{ProgramImage, Value};
use prometeu_bytecode::v0::{BytecodeModule, DebugInfo}; use prometeu_bytecode::v0::{BytecodeModule, DebugInfo, ConstantPoolEntry};
use prometeu_bytecode::pbc::ConstantPoolEntry;
use prometeu_bytecode::opcode::OpCode; use prometeu_bytecode::opcode::OpCode;
use std::collections::HashMap; use std::collections::HashMap;
@ -149,6 +148,7 @@ impl Linker {
combined_constants, combined_constants,
combined_functions, combined_functions,
debug_info, debug_info,
exports,
)) ))
} }
} }

View File

@ -30,12 +30,10 @@ pub enum VmFault {
pub enum VmInitError { pub enum VmInitError {
InvalidFormat, InvalidFormat,
UnsupportedFormat, UnsupportedFormat,
PpbcParseFailed,
PbsV0LoadFailed(prometeu_bytecode::v0::LoadError), PbsV0LoadFailed(prometeu_bytecode::v0::LoadError),
LinkFailed(LinkError), LinkFailed(LinkError),
EntrypointNotFound, EntrypointNotFound,
VerificationFailed(VerifierError), VerificationFailed(VerifierError),
UnsupportedLegacyCallEncoding,
} }
pub struct HostReturn<'a> { pub struct HostReturn<'a> {

View File

@ -2,6 +2,7 @@ use crate::virtual_machine::Value;
use prometeu_bytecode::v0::{FunctionMeta, DebugInfo}; use prometeu_bytecode::v0::{FunctionMeta, DebugInfo};
use prometeu_bytecode::abi::TrapInfo; use prometeu_bytecode::abi::TrapInfo;
use std::sync::Arc; use std::sync::Arc;
use std::collections::HashMap;
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ProgramImage { pub struct ProgramImage {
@ -9,10 +10,11 @@ pub struct ProgramImage {
pub constant_pool: Arc<[Value]>, pub constant_pool: Arc<[Value]>,
pub functions: Arc<[FunctionMeta]>, pub functions: Arc<[FunctionMeta]>,
pub debug_info: Option<DebugInfo>, pub debug_info: Option<DebugInfo>,
pub exports: Arc<HashMap<String, u32>>,
} }
impl ProgramImage { 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() { if functions.is_empty() && !rom.is_empty() {
functions.push(FunctionMeta { functions.push(FunctionMeta {
code_offset: 0, code_offset: 0,
@ -25,6 +27,7 @@ impl ProgramImage {
constant_pool: Arc::from(constant_pool), constant_pool: Arc::from(constant_pool),
functions: Arc::from(functions), functions: Arc::from(functions),
debug_info, debug_info,
exports: Arc::new(exports),
} }
} }

View File

@ -4,7 +4,6 @@ use crate::virtual_machine::scope_frame::ScopeFrame;
use crate::virtual_machine::value::Value; use crate::virtual_machine::value::Value;
use crate::virtual_machine::{NativeInterface, ProgramImage, VmInitError}; use crate::virtual_machine::{NativeInterface, ProgramImage, VmInitError};
use prometeu_bytecode::opcode::OpCode; 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}; 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. /// Reason why the Virtual Machine stopped execution during a specific run.
@ -99,7 +98,7 @@ impl VirtualMachine {
call_stack: Vec::new(), call_stack: Vec::new(),
scope_stack: Vec::new(), scope_stack: Vec::new(),
globals: 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(), heap: Vec::new(),
cycles: 0, cycles: 0,
halted: false, halted: false,
@ -122,25 +121,8 @@ impl VirtualMachine {
self.cycles = 0; self.cycles = 0;
self.halted = true; // execution is impossible until successful load self.halted = true; // execution is impossible until successful load
// Only recognized formats are loadable. // Only recognized format is loadable: PBS v0 industrial format
let program = if program_bytes.starts_with(b"PPBC") { let program = if program_bytes.starts_with(b"PBS\0") {
// 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
match prometeu_bytecode::v0::BytecodeLoader::load(&program_bytes) { match prometeu_bytecode::v0::BytecodeLoader::load(&program_bytes) {
Ok(module) => { Ok(module) => {
// Link module(s) // Link module(s)
@ -148,18 +130,9 @@ impl VirtualMachine {
.map_err(VmInitError::LinkFailed)?; .map_err(VmInitError::LinkFailed)?;
// Run verifier on the linked program // 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) let max_stacks = crate::virtual_machine::verifier::Verifier::verify(&linked_program.rom, &linked_program.functions)
.map_err(VmInitError::VerificationFailed)?; .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(); let mut functions = linked_program.functions.as_ref().to_vec();
for (func, max_stack) in functions.iter_mut().zip(max_stacks) { for (func, max_stack) in functions.iter_mut().zip(max_stacks) {
func.max_stack_slots = max_stack; func.max_stack_slots = max_stack;
@ -177,15 +150,21 @@ impl VirtualMachine {
return Err(VmInitError::InvalidFormat); 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() { let pc = if entrypoint.is_empty() {
0 0
} else { } else if let Ok(addr) = entrypoint.parse::<usize>() {
let addr = entrypoint.parse::<usize>().map_err(|_| VmInitError::EntrypointNotFound)?;
if addr >= program.rom.len() && (addr > 0 || !program.rom.is_empty()) { if addr >= program.rom.len() && (addr > 0 || !program.rom.is_empty()) {
return Err(VmInitError::EntrypointNotFound); return Err(VmInitError::EntrypointNotFound);
} }
addr 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. // 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 /// Prepares the VM to execute a specific entrypoint by setting the PC and
/// pushing an initial call frame. /// pushing an initial call frame.
pub fn prepare_call(&mut self, entrypoint: &str) { pub fn prepare_call(&mut self, entrypoint: &str) {
let addr = if let Ok(addr) = entrypoint.parse::<usize>() { let (addr, func_idx) = if let Ok(addr) = entrypoint.parse::<usize>() {
addr 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 { } 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.pc = addr;
self.halted = false; self.halted = false;
@ -222,7 +202,8 @@ impl VirtualMachine {
// Entrypoint also needs locals allocated. // Entrypoint also needs locals allocated.
// For the sentinel frame, stack_base is always 0. // For the sentinel frame, stack_base is always 0.
if let Some(func) = self.program.functions.get(func_idx) { 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); 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 { impl Default for VirtualMachine {
@ -1212,7 +1181,7 @@ mod tests {
]; ];
let mut vm = VirtualMachine { let mut vm = VirtualMachine {
program: ProgramImage::new(rom, vec![], functions, None), program: ProgramImage::new(rom, vec![], functions, None, std::collections::HashMap::new()),
..Default::default() ..Default::default()
}; };
vm.prepare_call("0"); vm.prepare_call("0");
@ -1257,7 +1226,7 @@ mod tests {
]; ];
let mut vm = VirtualMachine { let mut vm = VirtualMachine {
program: ProgramImage::new(rom, vec![], functions, None), program: ProgramImage::new(rom, vec![], functions, None, std::collections::HashMap::new()),
..Default::default() ..Default::default()
}; };
vm.prepare_call("0"); vm.prepare_call("0");
@ -1296,7 +1265,7 @@ mod tests {
]; ];
let mut vm2 = VirtualMachine { let mut vm2 = VirtualMachine {
program: ProgramImage::new(rom2, vec![], functions2, None), program: ProgramImage::new(rom2, vec![], functions2, None, std::collections::HashMap::new()),
..Default::default() ..Default::default()
}; };
vm2.prepare_call("0"); vm2.prepare_call("0");
@ -1405,7 +1374,7 @@ mod tests {
]; ];
let mut vm = VirtualMachine { let mut vm = VirtualMachine {
program: ProgramImage::new(rom, vec![], functions, None), program: ProgramImage::new(rom, vec![], functions, None, std::collections::HashMap::new()),
..Default::default() ..Default::default()
}; };
vm.prepare_call("0"); vm.prepare_call("0");
@ -1927,53 +1896,6 @@ mod tests {
assert_eq!(vm.cycles, 0); 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] #[test]
fn test_calling_convention_add() { fn test_calling_convention_add() {
@ -2505,7 +2427,7 @@ mod tests {
function_names: vec![(0, "main".to_string())], 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 { let mut vm = VirtualMachine {
program, program,
..Default::default() ..Default::default()
@ -2552,7 +2474,7 @@ mod tests {
..Default::default() ..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 { let mut vm = VirtualMachine {
program, program,
..Default::default() ..Default::default()

View 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!");
}

View File

@ -0,0 +1,167 @@
# PBS v0 — Module & Linking Model (SelfContained 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 selfcontained 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, indexbased layout**.
This includes:
* Function call resolution
* Controlflow 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 **selfcontained**: 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 addressbased 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 ABIcorrect syscall usage
The output must be a **readytorun 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 (NonGoals for v0)
PBS v0 explicitly does **not** define:
* Multimodule 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, selfcontained 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.

View File

@ -1388,6 +1388,23 @@ This avoids overloading the meaning of `TypeName.member`.
### 8.7 Summary of Struct Rules ### 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`. * Structs are declared with `declare struct`.
* Fields are private and cannot be accessed directly. * Fields are private and cannot be accessed directly.
* Constructor aliases exist only inside the type and are called as `Type.alias(...)`. * Constructor aliases exist only inside the type and are called as `Type.alias(...)`.

View File

@ -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” ## Definition of Done (DoD) for PBS v0 “minimum executable”
A single canonical cartridge runs end-to-end: A single canonical cartridge runs end-to-end:

File diff suppressed because it is too large Load Diff

View 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

Binary file not shown.

View File

@ -0,0 +1,4 @@
{
"script_fe": "pbs",
"entry": "src/main.pbs"
}

View 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);
}
}

View File

@ -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);
}
}

View File

@ -5,7 +5,7 @@
"title": "Test 1", "title": "Test 1",
"app_version": "0.1.0", "app_version": "0.1.0",
"app_mode": "Game", "app_mode": "Game",
"entrypoint": "0", "entrypoint": "frame",
"asset_table": [ "asset_table": [
{ {
"asset_id": 0, "asset_id": 0,

View File

@ -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);
}
} }