fix structs
This commit is contained in:
parent
5cf77359fb
commit
d994005f8b
@ -58,14 +58,9 @@ pub fn emit_module(module: &ir_vm::Module) -> Result<EmitResult> {
|
|||||||
pub fn emit_fragments(module: &ir_vm::Module) -> Result<EmitFragments> {
|
pub fn emit_fragments(module: &ir_vm::Module) -> Result<EmitFragments> {
|
||||||
let mut emitter = BytecodeEmitter::new();
|
let mut emitter = BytecodeEmitter::new();
|
||||||
|
|
||||||
let mut mapped_const_ids = Vec::with_capacity(module.const_pool.constants.len());
|
|
||||||
for val in &module.const_pool.constants {
|
|
||||||
mapped_const_ids.push(emitter.add_ir_constant(val));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut asm_instrs = Vec::new();
|
let mut asm_instrs = Vec::new();
|
||||||
let mut ir_instr_map = Vec::new();
|
let mut ir_instr_map = Vec::new();
|
||||||
let function_ranges = emitter.lower_instrs(module, &mut asm_instrs, &mut ir_instr_map, &mapped_const_ids)?;
|
let function_ranges = emitter.lower_instrs(module, &mut asm_instrs, &mut ir_instr_map)?;
|
||||||
|
|
||||||
let pcs = BytecodeEmitter::calculate_pcs(&asm_instrs);
|
let pcs = BytecodeEmitter::calculate_pcs(&asm_instrs);
|
||||||
let assemble_res = prometeu_bytecode::asm::assemble_with_unresolved(&asm_instrs).map_err(|e| anyhow!(e))?;
|
let assemble_res = prometeu_bytecode::asm::assemble_with_unresolved(&asm_instrs).map_err(|e| anyhow!(e))?;
|
||||||
@ -155,11 +150,15 @@ impl BytecodeEmitter {
|
|||||||
&mut self,
|
&mut self,
|
||||||
module: &'b ir_vm::Module,
|
module: &'b ir_vm::Module,
|
||||||
asm_instrs: &mut Vec<Asm>,
|
asm_instrs: &mut Vec<Asm>,
|
||||||
ir_instr_map: &mut Vec<Option<&'b ir_vm::Instruction>>,
|
ir_instr_map: &mut Vec<Option<&'b ir_vm::Instruction>>
|
||||||
mapped_const_ids: &[u32]
|
|
||||||
) -> Result<Vec<(usize, usize)>> {
|
) -> Result<Vec<(usize, usize)>> {
|
||||||
|
// Cache to map VM IR const ids to emitted constant pool ids
|
||||||
|
let mut const_id_map: std::collections::HashMap<ir_vm::types::ConstId, u32> = std::collections::HashMap::new();
|
||||||
|
// Build a mapping from VM function id -> index into module.functions
|
||||||
|
let mut id_to_index = std::collections::HashMap::new();
|
||||||
let mut func_names = std::collections::HashMap::new();
|
let mut func_names = std::collections::HashMap::new();
|
||||||
for func in &module.functions {
|
for (idx, func) in module.functions.iter().enumerate() {
|
||||||
|
id_to_index.insert(func.id, idx as u32);
|
||||||
func_names.insert(func.id, func.name.clone());
|
func_names.insert(func.id, func.name.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,6 +169,8 @@ impl BytecodeEmitter {
|
|||||||
// 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);
|
||||||
|
// Track an approximate stack height for this function
|
||||||
|
let mut stack_height: i32 = 0;
|
||||||
|
|
||||||
for instr in &function.body {
|
for instr in &function.body {
|
||||||
let op_start_idx = asm_instrs.len();
|
let op_start_idx = asm_instrs.len();
|
||||||
@ -179,32 +180,55 @@ impl BytecodeEmitter {
|
|||||||
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![])),
|
||||||
InstrKind::PushConst(id) => {
|
InstrKind::PushConst(id) => {
|
||||||
let mapped_id = mapped_const_ids[id.0 as usize];
|
// Map VM const id to emitted const pool id on-demand
|
||||||
|
let mapped_id = if let Some(mid) = const_id_map.get(id) {
|
||||||
|
*mid
|
||||||
|
} else {
|
||||||
|
let idx = id.0 as usize;
|
||||||
|
let val = module
|
||||||
|
.const_pool
|
||||||
|
.constants
|
||||||
|
.get(idx)
|
||||||
|
.ok_or_else(|| anyhow!("Invalid const id {} (pool len {})", id.0, module.const_pool.constants.len()))?;
|
||||||
|
let mid = self.add_ir_constant(val);
|
||||||
|
const_id_map.insert(*id, mid);
|
||||||
|
mid
|
||||||
|
};
|
||||||
asm_instrs.push(Asm::Op(OpCode::PushConst, vec![Operand::U32(mapped_id)]));
|
asm_instrs.push(Asm::Op(OpCode::PushConst, vec![Operand::U32(mapped_id)]));
|
||||||
|
stack_height += 1;
|
||||||
}
|
}
|
||||||
InstrKind::PushBounded(val) => {
|
InstrKind::PushBounded(val) => {
|
||||||
asm_instrs.push(Asm::Op(OpCode::PushBounded, vec![Operand::U32(*val)]));
|
asm_instrs.push(Asm::Op(OpCode::PushBounded, vec![Operand::U32(*val)]));
|
||||||
|
stack_height += 1;
|
||||||
}
|
}
|
||||||
InstrKind::PushBool(v) => {
|
InstrKind::PushBool(v) => {
|
||||||
asm_instrs.push(Asm::Op(OpCode::PushBool, vec![Operand::Bool(*v)]));
|
asm_instrs.push(Asm::Op(OpCode::PushBool, vec![Operand::Bool(*v)]));
|
||||||
|
stack_height += 1;
|
||||||
}
|
}
|
||||||
InstrKind::PushNull => {
|
InstrKind::PushNull => {
|
||||||
asm_instrs.push(Asm::Op(OpCode::PushConst, vec![Operand::U32(0)]));
|
asm_instrs.push(Asm::Op(OpCode::PushConst, vec![Operand::U32(0)]));
|
||||||
|
stack_height += 1;
|
||||||
|
}
|
||||||
|
InstrKind::Pop => {
|
||||||
|
asm_instrs.push(Asm::Op(OpCode::Pop, vec![]));
|
||||||
|
stack_height = (stack_height - 1).max(0);
|
||||||
|
}
|
||||||
|
InstrKind::Dup => {
|
||||||
|
asm_instrs.push(Asm::Op(OpCode::Dup, vec![]));
|
||||||
|
stack_height += 1;
|
||||||
}
|
}
|
||||||
InstrKind::Pop => asm_instrs.push(Asm::Op(OpCode::Pop, vec![])),
|
|
||||||
InstrKind::Dup => asm_instrs.push(Asm::Op(OpCode::Dup, vec![])),
|
|
||||||
InstrKind::Swap => asm_instrs.push(Asm::Op(OpCode::Swap, vec![])),
|
InstrKind::Swap => asm_instrs.push(Asm::Op(OpCode::Swap, vec![])),
|
||||||
InstrKind::Add => asm_instrs.push(Asm::Op(OpCode::Add, vec![])),
|
InstrKind::Add => { asm_instrs.push(Asm::Op(OpCode::Add, vec![])); stack_height = (stack_height - 1).max(0); }
|
||||||
InstrKind::Sub => asm_instrs.push(Asm::Op(OpCode::Sub, vec![])),
|
InstrKind::Sub => { asm_instrs.push(Asm::Op(OpCode::Sub, vec![])); stack_height = (stack_height - 1).max(0); }
|
||||||
InstrKind::Mul => asm_instrs.push(Asm::Op(OpCode::Mul, vec![])),
|
InstrKind::Mul => { asm_instrs.push(Asm::Op(OpCode::Mul, vec![])); stack_height = (stack_height - 1).max(0); }
|
||||||
InstrKind::Div => asm_instrs.push(Asm::Op(OpCode::Div, vec![])),
|
InstrKind::Div => { asm_instrs.push(Asm::Op(OpCode::Div, vec![])); stack_height = (stack_height - 1).max(0); }
|
||||||
InstrKind::Neg => asm_instrs.push(Asm::Op(OpCode::Neg, vec![])),
|
InstrKind::Neg => { asm_instrs.push(Asm::Op(OpCode::Neg, vec![])); /* unary */ }
|
||||||
InstrKind::Eq => asm_instrs.push(Asm::Op(OpCode::Eq, vec![])),
|
InstrKind::Eq => { asm_instrs.push(Asm::Op(OpCode::Eq, vec![])); stack_height = (stack_height - 1).max(0); }
|
||||||
InstrKind::Neq => asm_instrs.push(Asm::Op(OpCode::Neq, vec![])),
|
InstrKind::Neq => { asm_instrs.push(Asm::Op(OpCode::Neq, vec![])); stack_height = (stack_height - 1).max(0); }
|
||||||
InstrKind::Lt => asm_instrs.push(Asm::Op(OpCode::Lt, vec![])),
|
InstrKind::Lt => { asm_instrs.push(Asm::Op(OpCode::Lt, vec![])); stack_height = (stack_height - 1).max(0); }
|
||||||
InstrKind::Gt => asm_instrs.push(Asm::Op(OpCode::Gt, vec![])),
|
InstrKind::Gt => { asm_instrs.push(Asm::Op(OpCode::Gt, vec![])); stack_height = (stack_height - 1).max(0); }
|
||||||
InstrKind::Lte => asm_instrs.push(Asm::Op(OpCode::Lte, vec![])),
|
InstrKind::Lte => { asm_instrs.push(Asm::Op(OpCode::Lte, vec![])); stack_height = (stack_height - 1).max(0); }
|
||||||
InstrKind::Gte => asm_instrs.push(Asm::Op(OpCode::Gte, vec![])),
|
InstrKind::Gte => { asm_instrs.push(Asm::Op(OpCode::Gte, vec![])); stack_height = (stack_height - 1).max(0); }
|
||||||
InstrKind::And => asm_instrs.push(Asm::Op(OpCode::And, vec![])),
|
InstrKind::And => asm_instrs.push(Asm::Op(OpCode::And, vec![])),
|
||||||
InstrKind::Or => asm_instrs.push(Asm::Op(OpCode::Or, vec![])),
|
InstrKind::Or => asm_instrs.push(Asm::Op(OpCode::Or, vec![])),
|
||||||
InstrKind::Not => asm_instrs.push(Asm::Op(OpCode::Not, vec![])),
|
InstrKind::Not => asm_instrs.push(Asm::Op(OpCode::Not, vec![])),
|
||||||
@ -215,46 +239,76 @@ impl BytecodeEmitter {
|
|||||||
InstrKind::Shr => asm_instrs.push(Asm::Op(OpCode::Shr, vec![])),
|
InstrKind::Shr => asm_instrs.push(Asm::Op(OpCode::Shr, vec![])),
|
||||||
InstrKind::LocalLoad { slot } => {
|
InstrKind::LocalLoad { slot } => {
|
||||||
asm_instrs.push(Asm::Op(OpCode::GetLocal, vec![Operand::U32(*slot)]));
|
asm_instrs.push(Asm::Op(OpCode::GetLocal, vec![Operand::U32(*slot)]));
|
||||||
|
stack_height += 1;
|
||||||
}
|
}
|
||||||
InstrKind::LocalStore { slot } => {
|
InstrKind::LocalStore { slot } => {
|
||||||
asm_instrs.push(Asm::Op(OpCode::SetLocal, vec![Operand::U32(*slot)]));
|
asm_instrs.push(Asm::Op(OpCode::SetLocal, vec![Operand::U32(*slot)]));
|
||||||
|
stack_height = (stack_height - 1).max(0);
|
||||||
}
|
}
|
||||||
InstrKind::GetGlobal(slot) => {
|
InstrKind::GetGlobal(slot) => {
|
||||||
asm_instrs.push(Asm::Op(OpCode::GetGlobal, vec![Operand::U32(*slot)]));
|
asm_instrs.push(Asm::Op(OpCode::GetGlobal, vec![Operand::U32(*slot)]));
|
||||||
|
stack_height += 1;
|
||||||
}
|
}
|
||||||
InstrKind::SetGlobal(slot) => {
|
InstrKind::SetGlobal(slot) => {
|
||||||
asm_instrs.push(Asm::Op(OpCode::SetGlobal, vec![Operand::U32(*slot)]));
|
asm_instrs.push(Asm::Op(OpCode::SetGlobal, vec![Operand::U32(*slot)]));
|
||||||
|
stack_height = (stack_height - 1).max(0);
|
||||||
}
|
}
|
||||||
InstrKind::Jmp(label) => {
|
InstrKind::Jmp(label) => {
|
||||||
asm_instrs.push(Asm::Op(OpCode::Jmp, vec![Operand::RelLabel(label.0.clone(), function.name.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::RelLabel(label.0.clone(), function.name.clone())]));
|
asm_instrs.push(Asm::Op(OpCode::JmpIfFalse, vec![Operand::RelLabel(label.0.clone(), function.name.clone())]));
|
||||||
|
// VM consumes the condition for JmpIfFalse
|
||||||
|
stack_height = (stack_height - 1).max(0);
|
||||||
}
|
}
|
||||||
InstrKind::Label(label) => {
|
InstrKind::Label(label) => {
|
||||||
asm_instrs.push(Asm::Label(label.0.clone()));
|
asm_instrs.push(Asm::Label(label.0.clone()));
|
||||||
|
// Each labeled block in VM code is a fresh basic block.
|
||||||
|
// Our IR lowering (core_to_vm) assumes empty evaluation stack at
|
||||||
|
// block boundaries. Ensure the emitter's internal accounting
|
||||||
|
// matches that assumption to avoid inserting balancing Pops
|
||||||
|
// carried over from previous fallthrough paths.
|
||||||
|
stack_height = 0;
|
||||||
}
|
}
|
||||||
InstrKind::Call { func_id, .. } => {
|
InstrKind::Call { func_id, arg_count } => {
|
||||||
|
// Translate call by function index within this module
|
||||||
|
if let Some(idx) = id_to_index.get(func_id) {
|
||||||
|
asm_instrs.push(Asm::Op(OpCode::Call, vec![Operand::U32(*idx)]));
|
||||||
|
stack_height = (stack_height - (*arg_count as i32)).max(0);
|
||||||
|
} else {
|
||||||
|
// As a fallback, if we can resolve by name (cross-module import label)
|
||||||
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())]));
|
asm_instrs.push(Asm::Op(OpCode::Call, vec![Operand::Label(name.clone())]));
|
||||||
|
stack_height = (stack_height - (*arg_count as i32)).max(0);
|
||||||
}
|
}
|
||||||
InstrKind::ImportCall { dep_alias, module_path, symbol_name, .. } => {
|
}
|
||||||
|
InstrKind::ImportCall { dep_alias, module_path, symbol_name, arg_count } => {
|
||||||
let label = format!("@{}::{}:{}", dep_alias, module_path, symbol_name);
|
let label = format!("@{}::{}:{}", dep_alias, module_path, symbol_name);
|
||||||
asm_instrs.push(Asm::Op(OpCode::Call, vec![Operand::Label(label)]));
|
asm_instrs.push(Asm::Op(OpCode::Call, vec![Operand::Label(label)]));
|
||||||
|
stack_height = (stack_height - (*arg_count as i32)).max(0);
|
||||||
|
}
|
||||||
|
InstrKind::Ret => {
|
||||||
|
// Do not emit balancing Pops here. The VM verifier operates on the real
|
||||||
|
// runtime stack height, and extra Pops can cause StackUnderflow on some
|
||||||
|
// control-flow paths. Ret should appear when the stack is already in the
|
||||||
|
// correct state.
|
||||||
|
asm_instrs.push(Asm::Op(OpCode::Ret, vec![]));
|
||||||
}
|
}
|
||||||
InstrKind::Ret => asm_instrs.push(Asm::Op(OpCode::Ret, vec![])),
|
|
||||||
InstrKind::Syscall(id) => {
|
InstrKind::Syscall(id) => {
|
||||||
asm_instrs.push(Asm::Op(OpCode::Syscall, vec![Operand::U32(*id)]));
|
asm_instrs.push(Asm::Op(OpCode::Syscall, vec![Operand::U32(*id)]));
|
||||||
}
|
}
|
||||||
InstrKind::FrameSync => asm_instrs.push(Asm::Op(OpCode::FrameSync, vec![])),
|
InstrKind::FrameSync => asm_instrs.push(Asm::Op(OpCode::FrameSync, vec![])),
|
||||||
InstrKind::Alloc { type_id, slots } => {
|
InstrKind::Alloc { type_id, slots } => {
|
||||||
asm_instrs.push(Asm::Op(OpCode::Alloc, vec![Operand::U32(type_id.0), Operand::U32(*slots)]));
|
asm_instrs.push(Asm::Op(OpCode::Alloc, vec![Operand::U32(type_id.0), Operand::U32(*slots)]));
|
||||||
|
stack_height += *slots as i32;
|
||||||
}
|
}
|
||||||
InstrKind::GateLoad { offset } => {
|
InstrKind::GateLoad { offset } => {
|
||||||
asm_instrs.push(Asm::Op(OpCode::GateLoad, vec![Operand::U32(*offset)]));
|
asm_instrs.push(Asm::Op(OpCode::GateLoad, vec![Operand::U32(*offset)]));
|
||||||
|
stack_height += 1;
|
||||||
}
|
}
|
||||||
InstrKind::GateStore { offset } => {
|
InstrKind::GateStore { offset } => {
|
||||||
asm_instrs.push(Asm::Op(OpCode::GateStore, vec![Operand::U32(*offset)]));
|
asm_instrs.push(Asm::Op(OpCode::GateStore, vec![Operand::U32(*offset)]));
|
||||||
|
stack_height = (stack_height - 1).max(0);
|
||||||
}
|
}
|
||||||
InstrKind::GateBeginPeek => asm_instrs.push(Asm::Op(OpCode::GateBeginPeek, vec![])),
|
InstrKind::GateBeginPeek => asm_instrs.push(Asm::Op(OpCode::GateBeginPeek, vec![])),
|
||||||
InstrKind::GateEndPeek => asm_instrs.push(Asm::Op(OpCode::GateEndPeek, vec![])),
|
InstrKind::GateEndPeek => asm_instrs.push(Asm::Op(OpCode::GateEndPeek, vec![])),
|
||||||
@ -303,6 +357,7 @@ mod tests {
|
|||||||
use crate::ir_vm::module::{Function, Module};
|
use crate::ir_vm::module::{Function, Module};
|
||||||
use crate::ir_vm::types::Type;
|
use crate::ir_vm::types::Type;
|
||||||
use prometeu_bytecode::{BytecodeLoader, ConstantPoolEntry};
|
use prometeu_bytecode::{BytecodeLoader, ConstantPoolEntry};
|
||||||
|
use prometeu_bytecode::disasm::disasm;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_emit_module_with_const_pool() {
|
fn test_emit_module_with_const_pool() {
|
||||||
@ -337,4 +392,122 @@ mod tests {
|
|||||||
assert_eq!(pbc.const_pool[1], ConstantPoolEntry::Int64(12345));
|
assert_eq!(pbc.const_pool[1], ConstantPoolEntry::Int64(12345));
|
||||||
assert_eq!(pbc.const_pool[2], ConstantPoolEntry::String("hello".to_string()));
|
assert_eq!(pbc.const_pool[2], ConstantPoolEntry::String("hello".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn test_stack_is_reset_on_label_boundaries() {
|
||||||
|
// // This builds a function with control flow that would leave a value
|
||||||
|
// // on the stack before a jump. The target block starts with a Label
|
||||||
|
// // and immediately returns. The emitter must reset its internal stack
|
||||||
|
// // height at the label so it does NOT insert a Pop before Ret.
|
||||||
|
//
|
||||||
|
// let mut module = Module::new("label_stack_reset".to_string());
|
||||||
|
//
|
||||||
|
// // constants are not needed here
|
||||||
|
//
|
||||||
|
// let func = Function {
|
||||||
|
// id: FunctionId(1),
|
||||||
|
// name: "main".to_string(),
|
||||||
|
// params: vec![],
|
||||||
|
// return_type: Type::Void,
|
||||||
|
// body: vec![
|
||||||
|
// // entry block (block_0)
|
||||||
|
// Instruction::new(InstrKind::PushConst(ir_vm::ConstId(module.const_pool.insert(ConstantValue::Int(1)).0)), None),
|
||||||
|
// // jump to else, leaving one value on the emitter's stack accounting
|
||||||
|
// Instruction::new(InstrKind::Jmp(ir_vm::Label("else".to_string())), None),
|
||||||
|
// // then block (unreachable, but included for shape)
|
||||||
|
// Instruction::new(InstrKind::Label(ir_vm::Label("then".to_string())), None),
|
||||||
|
// Instruction::new(InstrKind::Ret, None),
|
||||||
|
// // else block: must not start with an extra Pop
|
||||||
|
// Instruction::new(InstrKind::Label(ir_vm::Label("else".to_string())), None),
|
||||||
|
// Instruction::new(InstrKind::Ret, None),
|
||||||
|
// ],
|
||||||
|
// param_slots: 0,
|
||||||
|
// local_slots: 0,
|
||||||
|
// return_slots: 0,
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// module.functions.push(func);
|
||||||
|
//
|
||||||
|
// let result = emit_module(&module).expect("emit failed");
|
||||||
|
// let pbc = BytecodeLoader::load(&result.rom).expect("pbc load failed");
|
||||||
|
// let instrs = disasm(&pbc.code).expect("disasm failed");
|
||||||
|
//
|
||||||
|
// // Find the 'else' label and ensure the next non-label opcode is Ret, not Pop
|
||||||
|
// let mut saw_else = false;
|
||||||
|
// for i in 0..instrs.len() - 1 {
|
||||||
|
// if let prometeu_bytecode::disasm::DisasmOp { opcode, .. } = &instrs[i] {
|
||||||
|
// if format!("{:?}", opcode) == "Label(\"else\")" {
|
||||||
|
// saw_else = true;
|
||||||
|
// // scan ahead to first non-label
|
||||||
|
// let mut j = i + 1;
|
||||||
|
// while j < instrs.len() && format!("{:?}", instrs[j].opcode).starts_with("Label(") {
|
||||||
|
// j += 1;
|
||||||
|
// }
|
||||||
|
// assert!(j < instrs.len(), "No instruction after else label");
|
||||||
|
// let next = format!("{:?}", instrs[j].opcode);
|
||||||
|
// assert_ne!(next.as_str(), "Pop", "Emitter must not insert Pop at start of a labeled block");
|
||||||
|
// assert_eq!(next.as_str(), "Ret", "Expected Ret directly after label when function is void");
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// assert!(saw_else, "Expected to find else label in emitted bytecode");
|
||||||
|
// }
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_jmp_if_false_does_not_emit_pop() {
|
||||||
|
// Build a tiny VM IR module with a conditional jump.
|
||||||
|
// The emitter must NOT insert an explicit Pop around JmpIfFalse; the VM consumes
|
||||||
|
// the condition implicitly.
|
||||||
|
|
||||||
|
let mut module = Module::new("test_jif".to_string());
|
||||||
|
|
||||||
|
// Prepare constants for a simple comparison (2 > 1)
|
||||||
|
let cid_two = module.const_pool.insert(ConstantValue::Int(2));
|
||||||
|
let cid_one = module.const_pool.insert(ConstantValue::Int(1));
|
||||||
|
|
||||||
|
let func = Function {
|
||||||
|
id: FunctionId(1),
|
||||||
|
name: "main".to_string(),
|
||||||
|
params: vec![],
|
||||||
|
return_type: Type::Void,
|
||||||
|
body: vec![
|
||||||
|
// cond: 2 > 1
|
||||||
|
Instruction::new(InstrKind::PushConst(ir_vm::ConstId(cid_two.0)), None),
|
||||||
|
Instruction::new(InstrKind::PushConst(ir_vm::ConstId(cid_one.0)), None),
|
||||||
|
Instruction::new(InstrKind::Gt, None),
|
||||||
|
// if !cond -> else
|
||||||
|
Instruction::new(InstrKind::JmpIfFalse(ir_vm::Label("else".to_string())), None),
|
||||||
|
// then: jump to merge
|
||||||
|
Instruction::new(InstrKind::Jmp(ir_vm::Label("then".to_string())), None),
|
||||||
|
// else block
|
||||||
|
Instruction::new(InstrKind::Label(ir_vm::Label("else".to_string())), None),
|
||||||
|
Instruction::new(InstrKind::Ret, None),
|
||||||
|
// then block
|
||||||
|
Instruction::new(InstrKind::Label(ir_vm::Label("then".to_string())), None),
|
||||||
|
Instruction::new(InstrKind::Ret, None),
|
||||||
|
],
|
||||||
|
param_slots: 0,
|
||||||
|
local_slots: 0,
|
||||||
|
return_slots: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
module.functions.push(func);
|
||||||
|
|
||||||
|
let result = emit_module(&module).expect("Failed to emit module");
|
||||||
|
let pbc = BytecodeLoader::load(&result.rom).expect("Failed to parse emitted PBC");
|
||||||
|
let instrs = disasm(&pbc.code).expect("Failed to disassemble emitted bytecode");
|
||||||
|
|
||||||
|
// Find JmpIfFalse in the listing and assert the very next opcode is NOT Pop.
|
||||||
|
let mut found_jif = false;
|
||||||
|
for i in 0..instrs.len().saturating_sub(1) {
|
||||||
|
if format!("{:?}", instrs[i].opcode) == "JmpIfFalse" {
|
||||||
|
found_jif = true;
|
||||||
|
let next_op = format!("{:?}", instrs[i + 1].opcode);
|
||||||
|
assert_ne!(next_op.as_str(), "Pop", "Emitter must not insert Pop after JmpIfFalse");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(found_jif, "Expected JmpIfFalse in emitted code but none was found");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,11 @@ pub struct Lowerer<'a> {
|
|||||||
struct_constructors: HashMap<String, HashMap<String, NodeId>>,
|
struct_constructors: HashMap<String, HashMap<String, NodeId>>,
|
||||||
type_constants: HashMap<String, HashMap<String, NodeId>>,
|
type_constants: HashMap<String, HashMap<String, NodeId>>,
|
||||||
current_type_context: Option<String>,
|
current_type_context: Option<String>,
|
||||||
|
// Campos de structs definidos pelo usuário
|
||||||
|
user_struct_field_offsets: HashMap<String, HashMap<String, u32>>,
|
||||||
|
user_struct_field_types: HashMap<String, HashMap<String, Type>>,
|
||||||
|
// Slot base do parâmetro `self` durante o lowering de métodos
|
||||||
|
method_self_slot: Option<u32>,
|
||||||
contract_registry: ContractRegistry,
|
contract_registry: ContractRegistry,
|
||||||
diagnostics: Vec<Diagnostic>,
|
diagnostics: Vec<Diagnostic>,
|
||||||
max_slots_used: u32,
|
max_slots_used: u32,
|
||||||
@ -78,6 +83,9 @@ impl<'a> Lowerer<'a> {
|
|||||||
struct_constructors: HashMap::new(),
|
struct_constructors: HashMap::new(),
|
||||||
type_constants: HashMap::new(),
|
type_constants: HashMap::new(),
|
||||||
current_type_context: None,
|
current_type_context: None,
|
||||||
|
user_struct_field_offsets: HashMap::new(),
|
||||||
|
user_struct_field_types: HashMap::new(),
|
||||||
|
method_self_slot: None,
|
||||||
contract_registry: ContractRegistry::new(),
|
contract_registry: ContractRegistry::new(),
|
||||||
diagnostics: Vec::new(),
|
diagnostics: Vec::new(),
|
||||||
max_slots_used: 0,
|
max_slots_used: 0,
|
||||||
@ -96,6 +104,9 @@ impl<'a> Lowerer<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn lower_file(mut self, root: NodeId, module_name: &str) -> Result<Program, DiagnosticBundle> {
|
pub fn lower_file(mut self, root: NodeId, module_name: &str) -> Result<Program, DiagnosticBundle> {
|
||||||
|
// Ensure per-module function id space starts clean and small
|
||||||
|
self.next_func_id = 1;
|
||||||
|
self.function_ids.clear();
|
||||||
let file = match self.arena.kind(root) {
|
let file = match self.arena.kind(root) {
|
||||||
NodeKind::File(file) => file,
|
NodeKind::File(file) => file,
|
||||||
_ => {
|
_ => {
|
||||||
@ -120,6 +131,21 @@ impl<'a> Lowerer<'a> {
|
|||||||
self.next_type_id += 1;
|
self.next_type_id += 1;
|
||||||
self.type_ids
|
self.type_ids
|
||||||
.insert(self.interner.resolve(n.name).to_string(), id);
|
.insert(self.interner.resolve(n.name).to_string(), id);
|
||||||
|
|
||||||
|
// Pré-scan de métodos dentro do tipo (apenas FnDecl com corpo)
|
||||||
|
let type_name = self.interner.resolve(n.name).to_string();
|
||||||
|
if let Some(body_id) = n.body {
|
||||||
|
if let NodeKind::TypeBody(tb) = self.arena.kind(body_id) {
|
||||||
|
for m in &tb.methods {
|
||||||
|
if let NodeKind::FnDecl(md) = self.arena.kind(*m) {
|
||||||
|
let full_name = format!("{}.{}", type_name, self.interner.resolve(md.name));
|
||||||
|
let id = FunctionId(self.next_func_id);
|
||||||
|
self.next_func_id += 1;
|
||||||
|
self.function_ids.insert(full_name, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@ -204,6 +230,42 @@ impl<'a> Lowerer<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calcular offsets e tipos de campos para structs de usuário
|
||||||
|
for decl in &file.decls {
|
||||||
|
if let NodeKind::TypeDecl(n) = self.arena.kind(*decl) {
|
||||||
|
if n.type_kind == "struct" {
|
||||||
|
let type_name = self.interner.resolve(n.name).to_string();
|
||||||
|
let mut offsets = HashMap::new();
|
||||||
|
let mut types = HashMap::new();
|
||||||
|
let mut acc: u32 = 0;
|
||||||
|
|
||||||
|
// Campos do cabeçalho
|
||||||
|
for p in &n.params {
|
||||||
|
let ty = self.lower_type_node(p.ty);
|
||||||
|
let slots = self.get_type_slots(&ty);
|
||||||
|
offsets.insert(self.interner.resolve(p.name).to_string(), acc);
|
||||||
|
types.insert(self.interner.resolve(p.name).to_string(), ty);
|
||||||
|
acc += slots;
|
||||||
|
}
|
||||||
|
// Campos adicionais (members) no corpo
|
||||||
|
if let Some(body_id) = n.body {
|
||||||
|
if let NodeKind::TypeBody(tb) = self.arena.kind(body_id) {
|
||||||
|
for m in &tb.members {
|
||||||
|
let ty = self.lower_type_node(m.ty);
|
||||||
|
let slots = self.get_type_slots(&ty);
|
||||||
|
offsets.insert(self.interner.resolve(m.name).to_string(), acc);
|
||||||
|
types.insert(self.interner.resolve(m.name).to_string(), ty);
|
||||||
|
acc += slots;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.user_struct_field_offsets.insert(type_name.clone(), offsets);
|
||||||
|
self.user_struct_field_types.insert(type_name, types);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut module = Module {
|
let mut module = Module {
|
||||||
name: module_name.to_string(),
|
name: module_name.to_string(),
|
||||||
functions: Vec::new(),
|
functions: Vec::new(),
|
||||||
@ -218,6 +280,25 @@ impl<'a> Lowerer<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Baixar métodos de structs
|
||||||
|
for decl in &file.decls {
|
||||||
|
if let NodeKind::TypeDecl(n) = self.arena.kind(*decl) {
|
||||||
|
let type_name = self.interner.resolve(n.name).to_string();
|
||||||
|
if let Some(body_id) = n.body {
|
||||||
|
if let NodeKind::TypeBody(tb) = self.arena.kind(body_id) {
|
||||||
|
for m in &tb.methods {
|
||||||
|
if let NodeKind::FnDecl(_) = self.arena.kind(*m) {
|
||||||
|
let func = self.lower_method_function(&type_name, *m).map_err(|_| DiagnosticBundle {
|
||||||
|
diagnostics: self.diagnostics.clone(),
|
||||||
|
})?;
|
||||||
|
module.functions.push(func);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.program.modules.push(module);
|
self.program.modules.push(module);
|
||||||
Ok(self.program)
|
Ok(self.program)
|
||||||
}
|
}
|
||||||
@ -515,6 +596,23 @@ impl<'a> Lowerer<'a> {
|
|||||||
} else {
|
} else {
|
||||||
Type::Int
|
Type::Int
|
||||||
}
|
}
|
||||||
|
} else if let NodeKind::MemberAccess(ma) = self.arena.kind(n.init) {
|
||||||
|
// Inferência para constantes de struct: Type.CONST
|
||||||
|
if let NodeKind::Ident(obj) = self.arena.kind(ma.object) {
|
||||||
|
let obj_name = self.interner.resolve(obj.name);
|
||||||
|
if let Some(consts) = self.type_constants.get(obj_name) {
|
||||||
|
let member_name = self.interner.resolve(ma.member);
|
||||||
|
if consts.contains_key(member_name) {
|
||||||
|
Type::Struct(obj_name.to_string())
|
||||||
|
} else {
|
||||||
|
Type::Int
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Type::Int
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Type::Int
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Type::Int
|
Type::Int
|
||||||
}
|
}
|
||||||
@ -546,6 +644,27 @@ impl<'a> Lowerer<'a> {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
|
// Se estamos no corpo de um método, permitir acessar campos de `self` implicitamente
|
||||||
|
if let (Some(struct_name), Some(self_slot)) = (self.current_type_context.as_ref(), self.method_self_slot) {
|
||||||
|
let maybe_off = self
|
||||||
|
.user_struct_field_offsets
|
||||||
|
.get(struct_name)
|
||||||
|
.and_then(|m| m.get(name_str).cloned());
|
||||||
|
if let Some(off_val) = maybe_off {
|
||||||
|
let ty = self
|
||||||
|
.user_struct_field_types
|
||||||
|
.get(struct_name)
|
||||||
|
.and_then(|m| m.get(name_str))
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(Type::Int);
|
||||||
|
let slots = self.get_type_slots(&ty);
|
||||||
|
for i in 0..slots {
|
||||||
|
self.emit(InstrKind::GetLocal(self_slot + off_val + i));
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check for special identifiers
|
// Check for special identifiers
|
||||||
match name_str {
|
match name_str {
|
||||||
"true" => {
|
"true" => {
|
||||||
@ -664,6 +783,11 @@ impl<'a> Lowerer<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_field_offset(&self, struct_name: &str, field_name: &str) -> u32 {
|
fn get_field_offset(&self, struct_name: &str, field_name: &str) -> u32 {
|
||||||
|
if let Some(map) = self.user_struct_field_offsets.get(struct_name) {
|
||||||
|
if let Some(off) = map.get(field_name) {
|
||||||
|
return *off;
|
||||||
|
}
|
||||||
|
}
|
||||||
match struct_name {
|
match struct_name {
|
||||||
"ButtonState" => match field_name {
|
"ButtonState" => match field_name {
|
||||||
"pressed" => 0,
|
"pressed" => 0,
|
||||||
@ -698,6 +822,11 @@ impl<'a> Lowerer<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_field_type(&self, struct_name: &str, field_name: &str) -> Type {
|
fn get_field_type(&self, struct_name: &str, field_name: &str) -> Type {
|
||||||
|
if let Some(map) = self.user_struct_field_types.get(struct_name) {
|
||||||
|
if let Some(ty) = map.get(field_name) {
|
||||||
|
return ty.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
match struct_name {
|
match struct_name {
|
||||||
"Pad" => Type::Struct("ButtonState".to_string()),
|
"Pad" => Type::Struct("ButtonState".to_string()),
|
||||||
"ButtonState" => match field_name {
|
"ButtonState" => match field_name {
|
||||||
@ -925,6 +1054,47 @@ impl<'a> Lowerer<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tentativa de chamada de método de instância: obj.method(...)
|
||||||
|
// 1) Descobrir tipo do objeto e slot
|
||||||
|
let mut obj_info: Option<(u32, Type)> = None;
|
||||||
|
match self.arena.kind(ma.object) {
|
||||||
|
NodeKind::Ident(id) => {
|
||||||
|
let obj_name = self.interner.resolve(id.name);
|
||||||
|
if let Some(info) = self.find_local(obj_name) {
|
||||||
|
obj_info = Some((info.slot, info.ty.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NodeKind::MemberAccess(_) => {
|
||||||
|
if let Some(info) = self.resolve_member_access(ma.object) {
|
||||||
|
obj_info = Some(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((base_slot, ty)) = obj_info.clone() {
|
||||||
|
if let Type::Struct(ref sname) = ty {
|
||||||
|
let member_name = self.interner.resolve(ma.member);
|
||||||
|
let full_name = format!("{}.{}", sname, member_name);
|
||||||
|
let func_id_opt = self.function_ids.get(&full_name).cloned();
|
||||||
|
if let Some(func_id) = func_id_opt {
|
||||||
|
// Empilha self (todas as slots da instância)
|
||||||
|
let self_slots = self.struct_slots.get(sname).cloned().unwrap_or(1);
|
||||||
|
for i in 0..self_slots {
|
||||||
|
self.emit(InstrKind::GetLocal(base_slot + i));
|
||||||
|
}
|
||||||
|
// Empilha argumentos
|
||||||
|
for arg in &n.args {
|
||||||
|
self.lower_node(*arg)?;
|
||||||
|
}
|
||||||
|
let arg_slots = n.args.len() as u32;
|
||||||
|
self.emit(InstrKind::Call(func_id, self_slots + arg_slots));
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback original
|
||||||
for arg in &n.args {
|
for arg in &n.args {
|
||||||
self.lower_node(*arg)?;
|
self.lower_node(*arg)?;
|
||||||
}
|
}
|
||||||
@ -978,6 +1148,102 @@ impl<'a> Lowerer<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn lower_method_function(&mut self, type_name: &str, node: NodeId) -> Result<Function, ()> {
|
||||||
|
let n = match self.arena.kind(node) {
|
||||||
|
NodeKind::FnDecl(n) => n,
|
||||||
|
_ => return Err(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let full_name = format!("{}.{}", type_name, self.interner.resolve(n.name));
|
||||||
|
let func_id = match self.function_ids.get(&full_name) {
|
||||||
|
Some(id) => *id,
|
||||||
|
None => {
|
||||||
|
self.error(
|
||||||
|
"E_LOWER_UNSUPPORTED",
|
||||||
|
format!("Missing function id for method '{}'", full_name),
|
||||||
|
self.arena.span(node),
|
||||||
|
);
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.next_block_id = 0;
|
||||||
|
self.local_vars = vec![HashMap::new()];
|
||||||
|
self.max_slots_used = 0;
|
||||||
|
|
||||||
|
let mut params = Vec::new();
|
||||||
|
let mut local_types = HashMap::new();
|
||||||
|
let mut param_slots = 0u32;
|
||||||
|
// Guardar contexto anterior
|
||||||
|
let prev_ctx = self.current_type_context.clone();
|
||||||
|
let prev_self_slot = self.method_self_slot.take();
|
||||||
|
self.current_type_context = Some(type_name.to_string());
|
||||||
|
|
||||||
|
for param in &n.params {
|
||||||
|
let ty = self.lower_type_node(param.ty);
|
||||||
|
let slots = self.get_type_slots(&ty);
|
||||||
|
let param_name = self.interner.resolve(param.name).to_string();
|
||||||
|
params.push(Param {
|
||||||
|
name: param_name.clone(),
|
||||||
|
ty: ty.clone(),
|
||||||
|
});
|
||||||
|
// Slot inicial deste parâmetro
|
||||||
|
let this_param_start = param_slots;
|
||||||
|
self.local_vars[0].insert(
|
||||||
|
self.interner.resolve(param.name).to_string(),
|
||||||
|
LocalInfo {
|
||||||
|
slot: this_param_start,
|
||||||
|
ty: ty.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
for i in 0..slots {
|
||||||
|
local_types.insert(this_param_start + i, ty.clone());
|
||||||
|
}
|
||||||
|
if self.interner.resolve(param.name) == "self" {
|
||||||
|
self.method_self_slot = Some(this_param_start);
|
||||||
|
}
|
||||||
|
param_slots += slots;
|
||||||
|
}
|
||||||
|
self.max_slots_used = param_slots;
|
||||||
|
|
||||||
|
let ret_ty = if let Some(ret) = n.ret { self.lower_type_node(ret) } else { Type::Void };
|
||||||
|
let return_slots = self.get_type_slots(&ret_ty);
|
||||||
|
|
||||||
|
let func = Function {
|
||||||
|
id: func_id,
|
||||||
|
name: full_name,
|
||||||
|
params,
|
||||||
|
return_type: ret_ty,
|
||||||
|
blocks: Vec::new(),
|
||||||
|
local_types,
|
||||||
|
param_slots: param_slots as u16,
|
||||||
|
local_slots: 0,
|
||||||
|
return_slots: return_slots as u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.current_function = Some(func);
|
||||||
|
self.start_block();
|
||||||
|
self.lower_node(n.body)?;
|
||||||
|
|
||||||
|
if let Some(mut block) = self.current_block.take() {
|
||||||
|
if !matches!(block.terminator, Terminator::Return | Terminator::Jump(_) | Terminator::JumpIfFalse { .. }) {
|
||||||
|
block.terminator = Terminator::Return;
|
||||||
|
}
|
||||||
|
if let Some(func) = &mut self.current_function {
|
||||||
|
func.blocks.push(block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut final_func = self.current_function.take().unwrap();
|
||||||
|
final_func.local_slots = (self.max_slots_used - param_slots) as u16;
|
||||||
|
|
||||||
|
// Restaurar contexto
|
||||||
|
self.current_type_context = prev_ctx;
|
||||||
|
self.method_self_slot = prev_self_slot;
|
||||||
|
|
||||||
|
Ok(final_func)
|
||||||
|
}
|
||||||
|
|
||||||
fn lower_constructor_call(&mut self, ctor: NodeId, args: &[NodeId]) -> Result<(), ()> {
|
fn lower_constructor_call(&mut self, ctor: NodeId, args: &[NodeId]) -> Result<(), ()> {
|
||||||
let ctor_id = ctor;
|
let ctor_id = ctor;
|
||||||
let ctor = match self.arena.kind(ctor) {
|
let ctor = match self.arena.kind(ctor) {
|
||||||
@ -1167,6 +1433,13 @@ impl<'a> Lowerer<'a> {
|
|||||||
"bool" => Type::Bool,
|
"bool" => Type::Bool,
|
||||||
"string" => Type::String,
|
"string" => Type::String,
|
||||||
"void" => Type::Void,
|
"void" => Type::Void,
|
||||||
|
"this" => {
|
||||||
|
if let Some(ctx) = &self.current_type_context {
|
||||||
|
Type::Struct(ctx.clone())
|
||||||
|
} else {
|
||||||
|
Type::Void
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(sym) = self
|
if let Some(sym) = self
|
||||||
.module_symbols
|
.module_symbols
|
||||||
@ -1528,7 +1801,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
declare contract MyContract host {}
|
declare contract MyContract host {}
|
||||||
declare error MyError {}
|
declare error MyError {}
|
||||||
declare struct Point { x: int }
|
declare struct Point(x: int)
|
||||||
|
|
||||||
fn main(
|
fn main(
|
||||||
s: MyService,
|
s: MyService,
|
||||||
|
|||||||
@ -296,24 +296,33 @@ impl<'a> Parser<'a> {
|
|||||||
};
|
};
|
||||||
let name = self.expect_identifier()?;
|
let name = self.expect_identifier()?;
|
||||||
|
|
||||||
let mut params = Vec::new();
|
// Permitir blocos opcionais em QUALQUER ordem após o nome do tipo.
|
||||||
if self.peek().kind == TokenKind::OpenParen {
|
// Apenas o bloco de parâmetros/dados `(...)` é obrigatório para `struct`.
|
||||||
params = self.parse_param_list()?;
|
let mut params: Vec<ParamNodeArena> = Vec::new();
|
||||||
}
|
let mut constructors: Vec<NodeId> = Vec::new();
|
||||||
|
let mut constants: Vec<NodeId> = Vec::new();
|
||||||
let mut constructors = Vec::new();
|
let mut body: Option<NodeId> = None;
|
||||||
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 {
|
|
||||||
self.advance();
|
|
||||||
is_host = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut constants = Vec::new();
|
let mut seen_params = false;
|
||||||
if self.peek().kind == TokenKind::OpenDoubleBracket {
|
let mut seen_ctors = false;
|
||||||
|
let mut seen_consts = false;
|
||||||
|
let mut seen_body = false;
|
||||||
|
let mut seen_host = false;
|
||||||
|
|
||||||
|
// Loop consumindo blocos enquanto houver tokens reconhecíveis
|
||||||
|
loop {
|
||||||
|
match self.peek().kind {
|
||||||
|
TokenKind::OpenParen if !seen_params => {
|
||||||
|
params = self.parse_param_list()?;
|
||||||
|
seen_params = true;
|
||||||
|
}
|
||||||
|
TokenKind::OpenBracket if !seen_ctors => {
|
||||||
|
constructors = self.parse_constructor_list()?;
|
||||||
|
seen_ctors = true;
|
||||||
|
}
|
||||||
|
TokenKind::OpenDoubleBracket if !seen_consts => {
|
||||||
|
// Constantes associadas: [[ NAME: expr, ... ]]
|
||||||
self.advance();
|
self.advance();
|
||||||
while self.peek().kind != TokenKind::CloseDoubleBracket && self.peek().kind != TokenKind::Eof {
|
while self.peek().kind != TokenKind::CloseDoubleBracket && self.peek().kind != TokenKind::Eof {
|
||||||
let c_start = self.peek().span.start;
|
let c_start = self.peek().span.start;
|
||||||
@ -322,10 +331,7 @@ impl<'a> Parser<'a> {
|
|||||||
let c_value = self.parse_expr(0)?;
|
let c_value = self.parse_expr(0)?;
|
||||||
let c_span = Span::new(self.file_id, c_start, self.arena.span(c_value).end);
|
let c_span = Span::new(self.file_id, c_start, self.arena.span(c_value).end);
|
||||||
constants.push(self.arena.push(
|
constants.push(self.arena.push(
|
||||||
NodeKind::ConstantDecl(ConstantDeclNodeArena {
|
NodeKind::ConstantDecl(ConstantDeclNodeArena { name: c_name, value: c_value }),
|
||||||
name: c_name,
|
|
||||||
value: c_value,
|
|
||||||
}),
|
|
||||||
c_span,
|
c_span,
|
||||||
));
|
));
|
||||||
if self.peek().kind == TokenKind::Comma {
|
if self.peek().kind == TokenKind::Comma {
|
||||||
@ -333,11 +339,24 @@ impl<'a> Parser<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.consume(TokenKind::CloseDoubleBracket)?;
|
self.consume(TokenKind::CloseDoubleBracket)?;
|
||||||
|
seen_consts = true;
|
||||||
|
}
|
||||||
|
TokenKind::OpenBrace if !seen_body => {
|
||||||
|
body = Some(self.parse_type_body()?);
|
||||||
|
seen_body = true;
|
||||||
|
}
|
||||||
|
TokenKind::Host if !seen_host => {
|
||||||
|
self.advance();
|
||||||
|
is_host = true;
|
||||||
|
seen_host = true;
|
||||||
|
}
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut body = None;
|
// Para 'struct', exigir que o bloco de parâmetros tenha sido fornecido
|
||||||
if self.peek().kind == TokenKind::OpenBrace {
|
if type_kind == "struct" && !seen_params {
|
||||||
body = Some(self.parse_type_body()?);
|
return Err(self.error_with_code("Expected parameter block '(...)' in struct declaration", Some("E_PARSE_EXPECTED_TOKEN")));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut end_pos = start_span.end;
|
let mut end_pos = start_span.end;
|
||||||
@ -373,9 +392,18 @@ impl<'a> Parser<'a> {
|
|||||||
let mut members = Vec::new();
|
let mut members = Vec::new();
|
||||||
let mut methods = 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 {
|
||||||
if self.peek().kind == TokenKind::Fn {
|
if self.peek().kind == TokenKind::Pub {
|
||||||
let sig_node = self.parse_service_member()?;
|
// pub fn ... { ... }
|
||||||
methods.push(sig_node);
|
let _ = self.advance(); // consume 'pub'
|
||||||
|
let method_node = self.parse_type_method(Some("pub".to_string()))?;
|
||||||
|
methods.push(method_node);
|
||||||
|
if self.peek().kind == TokenKind::Semicolon {
|
||||||
|
self.advance();
|
||||||
|
}
|
||||||
|
} else if self.peek().kind == TokenKind::Fn {
|
||||||
|
// Method inside type body (signature-only or with body)
|
||||||
|
let method_node = self.parse_type_method(None)?;
|
||||||
|
methods.push(method_node);
|
||||||
if self.peek().kind == TokenKind::Semicolon {
|
if self.peek().kind == TokenKind::Semicolon {
|
||||||
self.advance();
|
self.advance();
|
||||||
}
|
}
|
||||||
@ -405,6 +433,58 @@ impl<'a> Parser<'a> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_type_method(&mut self, vis: Option<String>) -> Result<NodeId, DiagnosticBundle> {
|
||||||
|
let start_span = self.consume(TokenKind::Fn)?.span;
|
||||||
|
let name = self.expect_identifier()?;
|
||||||
|
let params = self.parse_param_list()?;
|
||||||
|
let ret = if self.peek().kind == TokenKind::Colon {
|
||||||
|
self.advance();
|
||||||
|
Some(self.parse_type_ref()?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.peek().kind == TokenKind::Semicolon {
|
||||||
|
let ret_node = if let Some(ret) = ret {
|
||||||
|
ret
|
||||||
|
} else {
|
||||||
|
self.arena.push(
|
||||||
|
NodeKind::TypeName(TypeNameNodeArena {
|
||||||
|
name: self.builtin_void,
|
||||||
|
}),
|
||||||
|
Span::new(self.file_id, 0, 0),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let span = Span::new(self.file_id, start_span.start, self.arena.span(ret_node).end);
|
||||||
|
return Ok(self.arena.push(
|
||||||
|
NodeKind::ServiceFnSig(ServiceFnSigNodeArena { name, params, ret: ret_node }),
|
||||||
|
span,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut else_fallback = None;
|
||||||
|
if self.peek().kind == TokenKind::Else {
|
||||||
|
self.advance();
|
||||||
|
else_fallback = Some(self.parse_block()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = self.parse_block()?;
|
||||||
|
let body_span = self.arena.span(body);
|
||||||
|
|
||||||
|
let span = Span::new(self.file_id, start_span.start, body_span.end);
|
||||||
|
Ok(self.arena.push(
|
||||||
|
NodeKind::FnDecl(FnDeclNodeArena {
|
||||||
|
vis,
|
||||||
|
name,
|
||||||
|
params,
|
||||||
|
ret,
|
||||||
|
else_fallback,
|
||||||
|
body,
|
||||||
|
}),
|
||||||
|
span,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_fn_decl(&mut self, vis: Option<String>) -> Result<NodeId, DiagnosticBundle> {
|
fn parse_fn_decl(&mut self, vis: Option<String>) -> Result<NodeId, DiagnosticBundle> {
|
||||||
let start_span = self.consume(TokenKind::Fn)?.span;
|
let start_span = self.consume(TokenKind::Fn)?.span;
|
||||||
let name = self.expect_identifier()?;
|
let name = self.expect_identifier()?;
|
||||||
@ -1074,8 +1154,14 @@ impl<'a> Parser<'a> {
|
|||||||
|
|
||||||
self.consume(TokenKind::As)?;
|
self.consume(TokenKind::As)?;
|
||||||
let name = self.expect_identifier()?;
|
let name = self.expect_identifier()?;
|
||||||
|
// Corpo opcional do construtor
|
||||||
let body = self.parse_block()?;
|
let body = if self.peek().kind == TokenKind::OpenBrace {
|
||||||
|
self.parse_block()?
|
||||||
|
} else {
|
||||||
|
// bloco vazio
|
||||||
|
let empty_span = Span::new(self.file_id, start_span.end, start_span.end);
|
||||||
|
self.arena.push(NodeKind::Block(BlockNodeArena { stmts: vec![], tail: None }), empty_span)
|
||||||
|
};
|
||||||
|
|
||||||
let span = Span::new(self.file_id, start_span.start, self.arena.span(body).end);
|
let span = Span::new(self.file_id, start_span.start, self.arena.span(body).end);
|
||||||
constructors.push(self.arena.push(
|
constructors.push(self.arena.push(
|
||||||
@ -1169,10 +1255,7 @@ fn add(a: int, b: int): int {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_parse_type_decl() {
|
fn test_parse_type_decl() {
|
||||||
let source = r#"
|
let source = r#"
|
||||||
pub declare struct Point {
|
pub declare struct Point(x: int, y: int){}
|
||||||
x: int,
|
|
||||||
y: int
|
|
||||||
}
|
|
||||||
"#;
|
"#;
|
||||||
let mut interner = NameInterner::new();
|
let mut interner = NameInterner::new();
|
||||||
let mut parser = Parser::new(source, FileId(0), &mut interner);
|
let mut parser = Parser::new(source, FileId(0), &mut interner);
|
||||||
@ -1192,6 +1275,38 @@ pub declare struct Point {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_type_body_method_with_body() {
|
||||||
|
let source = r#"
|
||||||
|
declare struct Vec2(x: int, y: int) {
|
||||||
|
fn getX(self: this): int {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let mut interner = NameInterner::new();
|
||||||
|
let mut parser = Parser::new(source, FileId(0), &mut interner);
|
||||||
|
let parsed = parser.parse_file().unwrap();
|
||||||
|
let file = match parsed.arena.kind(parsed.root) {
|
||||||
|
NodeKind::File(file) => file,
|
||||||
|
_ => panic!("Expected File"),
|
||||||
|
};
|
||||||
|
assert_eq!(file.decls.len(), 1);
|
||||||
|
|
||||||
|
let type_decl = match parsed.arena.kind(file.decls[0]) {
|
||||||
|
NodeKind::TypeDecl(t) => t,
|
||||||
|
_ => panic!("Expected TypeDecl"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let body_id = type_decl.body.expect("Expected type body");
|
||||||
|
let body = match parsed.arena.kind(body_id) {
|
||||||
|
NodeKind::TypeBody(tb) => tb,
|
||||||
|
_ => panic!("Expected TypeBody"),
|
||||||
|
};
|
||||||
|
assert_eq!(body.methods.len(), 1);
|
||||||
|
assert!(matches!(parsed.arena.kind(body.methods[0]), NodeKind::FnDecl(_)));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_service_decl() {
|
fn test_parse_service_decl() {
|
||||||
let source = r#"
|
let source = r#"
|
||||||
|
|||||||
@ -908,8 +908,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_duplicate_symbols() {
|
fn test_duplicate_symbols() {
|
||||||
let source = "
|
let source = "
|
||||||
declare struct Foo {}
|
declare struct Foo()
|
||||||
declare struct Foo {}
|
declare struct Foo()
|
||||||
";
|
";
|
||||||
let (parsed, _, interner) = setup_test(source);
|
let (parsed, _, interner) = setup_test(source);
|
||||||
let mut collector = SymbolCollector::new(&interner);
|
let mut collector = SymbolCollector::new(&interner);
|
||||||
@ -923,7 +923,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_namespace_collision() {
|
fn test_namespace_collision() {
|
||||||
let source = "
|
let source = "
|
||||||
declare struct Foo {}
|
declare struct Foo()
|
||||||
fn Foo() {}
|
fn Foo() {}
|
||||||
";
|
";
|
||||||
let (parsed, _, interner) = setup_test(source);
|
let (parsed, _, interner) = setup_test(source);
|
||||||
@ -1268,7 +1268,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_lower_type_node_complex() {
|
fn test_lower_type_node_complex() {
|
||||||
let source = "
|
let source = "
|
||||||
declare struct MyType {}
|
declare struct MyType()
|
||||||
fn main() {
|
fn main() {
|
||||||
let x: optional<int> = 1;
|
let x: optional<int> = 1;
|
||||||
let y: result<int, string> = 2;
|
let y: result<int, string> = 2;
|
||||||
|
|||||||
@ -21,6 +21,9 @@ pub struct TypeChecker<'a> {
|
|||||||
struct_methods: HashMap<String, HashMap<String, PbsType>>,
|
struct_methods: HashMap<String, HashMap<String, PbsType>>,
|
||||||
diagnostics: Vec<Diagnostic>,
|
diagnostics: Vec<Diagnostic>,
|
||||||
contract_registry: ContractRegistry,
|
contract_registry: ContractRegistry,
|
||||||
|
// Contexto atual de tipo (para resolver `this` e métodos/fields)
|
||||||
|
current_type_context: Option<String>,
|
||||||
|
current_struct_fields: Option<HashMap<String, PbsType>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TypeChecker<'a> {
|
impl<'a> TypeChecker<'a> {
|
||||||
@ -43,6 +46,8 @@ impl<'a> TypeChecker<'a> {
|
|||||||
struct_methods: HashMap::new(),
|
struct_methods: HashMap::new(),
|
||||||
diagnostics: Vec::new(),
|
diagnostics: Vec::new(),
|
||||||
contract_registry: ContractRegistry::new(),
|
contract_registry: ContractRegistry::new(),
|
||||||
|
current_type_context: None,
|
||||||
|
current_struct_fields: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,8 +155,12 @@ impl<'a> TypeChecker<'a> {
|
|||||||
let mut methods = HashMap::new();
|
let mut methods = HashMap::new();
|
||||||
if let Some(body_node) = n.body {
|
if let Some(body_node) = n.body {
|
||||||
if let NodeKind::TypeBody(body) = arena.kind(body_node) {
|
if let NodeKind::TypeBody(body) = arena.kind(body_node) {
|
||||||
|
// Dentro do contexto do tipo, habilitar `this`
|
||||||
|
let prev_ctx = self.current_type_context.clone();
|
||||||
|
self.current_type_context = Some(type_name.clone());
|
||||||
for m in &body.methods {
|
for m in &body.methods {
|
||||||
if let NodeKind::ServiceFnSig(sig) = arena.kind(*m) {
|
match arena.kind(*m) {
|
||||||
|
NodeKind::ServiceFnSig(sig) => {
|
||||||
let mut params = Vec::new();
|
let mut params = Vec::new();
|
||||||
for p in &sig.params {
|
for p in &sig.params {
|
||||||
params.push(self.resolve_type_node(arena, p.ty));
|
params.push(self.resolve_type_node(arena, p.ty));
|
||||||
@ -162,7 +171,23 @@ impl<'a> TypeChecker<'a> {
|
|||||||
};
|
};
|
||||||
methods.insert(self.interner.resolve(sig.name).to_string(), m_ty);
|
methods.insert(self.interner.resolve(sig.name).to_string(), m_ty);
|
||||||
}
|
}
|
||||||
|
NodeKind::FnDecl(fn_decl) => {
|
||||||
|
let mut params = Vec::new();
|
||||||
|
for p in &fn_decl.params {
|
||||||
|
params.push(self.resolve_type_node(arena, p.ty));
|
||||||
}
|
}
|
||||||
|
let ret_ty = if let Some(ret) = fn_decl.ret {
|
||||||
|
self.resolve_type_node(arena, ret)
|
||||||
|
} else {
|
||||||
|
PbsType::Void
|
||||||
|
};
|
||||||
|
let m_ty = PbsType::Function { params, return_type: Box::new(ret_ty) };
|
||||||
|
methods.insert(self.interner.resolve(fn_decl.name).to_string(), m_ty);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.current_type_context = prev_ctx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.struct_methods.insert(type_name, methods);
|
self.struct_methods.insert(type_name, methods);
|
||||||
@ -508,6 +533,11 @@ impl<'a> TypeChecker<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check implicit struct fields in method context
|
||||||
|
if let Some(ty) = self.resolve_ident_override(name_str) {
|
||||||
|
return ty;
|
||||||
|
}
|
||||||
|
|
||||||
// Check module symbols
|
// Check module symbols
|
||||||
if let Some(sym) = self.module_symbols.value_symbols.get(n.name) {
|
if let Some(sym) = self.module_symbols.value_symbols.get(n.name) {
|
||||||
if let Some(ty) = &sym.ty {
|
if let Some(ty) = &sym.ty {
|
||||||
@ -749,7 +779,29 @@ impl<'a> TypeChecker<'a> {
|
|||||||
self.struct_constants.insert(type_name, constants_map);
|
self.struct_constants.insert(type_name, constants_map);
|
||||||
|
|
||||||
if let Some(body) = n.body {
|
if let Some(body) = n.body {
|
||||||
|
// Preparar contexto de tipo e fields para métodos
|
||||||
|
let type_name = self.interner.resolve(n.name).to_string();
|
||||||
|
let prev_ctx = self.current_type_context.clone();
|
||||||
|
self.current_type_context = Some(type_name.clone());
|
||||||
|
|
||||||
|
// Coletar fields do cabeçalho e membros do corpo
|
||||||
|
let mut fields: HashMap<String, PbsType> = HashMap::new();
|
||||||
|
for p in &n.params {
|
||||||
|
let ty = self.resolve_type_node(arena, p.ty);
|
||||||
|
fields.insert(self.interner.resolve(p.name).to_string(), ty);
|
||||||
|
}
|
||||||
|
if let NodeKind::TypeBody(tb) = arena.kind(body) {
|
||||||
|
for m in &tb.members {
|
||||||
|
let ty = self.resolve_type_node(arena, m.ty);
|
||||||
|
fields.insert(self.interner.resolve(m.name).to_string(), ty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let prev_fields = self.current_struct_fields.replace(fields);
|
||||||
|
|
||||||
self.check_node(arena, body);
|
self.check_node(arena, body);
|
||||||
|
|
||||||
|
self.current_struct_fields = prev_fields;
|
||||||
|
self.current_type_context = prev_ctx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -782,6 +834,20 @@ impl<'a> TypeChecker<'a> {
|
|||||||
"string" => PbsType::String,
|
"string" => PbsType::String,
|
||||||
"void" => PbsType::Void,
|
"void" => PbsType::Void,
|
||||||
"bounded" => PbsType::Bounded,
|
"bounded" => PbsType::Bounded,
|
||||||
|
"this" => {
|
||||||
|
if let Some(ctx) = &self.current_type_context {
|
||||||
|
PbsType::Struct(ctx.clone())
|
||||||
|
} else {
|
||||||
|
self.diagnostics.push(Diagnostic {
|
||||||
|
severity: Severity::Error,
|
||||||
|
code: "E_TYPE_THIS_OUT_OF_CONTEXT".to_string(),
|
||||||
|
message: "`this` used outside of type context".to_string(),
|
||||||
|
span: arena.span(node),
|
||||||
|
related: Vec::new(),
|
||||||
|
});
|
||||||
|
PbsType::Void
|
||||||
|
}
|
||||||
|
}
|
||||||
"Color" | "ButtonState" | "Pad" | "Touch" => PbsType::Struct(name_str.to_string()),
|
"Color" | "ButtonState" | "Pad" | "Touch" => PbsType::Struct(name_str.to_string()),
|
||||||
_ => {
|
_ => {
|
||||||
// Look up in symbol table
|
// Look up in symbol table
|
||||||
@ -830,6 +896,16 @@ impl<'a> TypeChecker<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override de resolução de identificadores: permitir acesso direto a fields no contexto de método
|
||||||
|
fn resolve_ident_override(&self, name: &str) -> Option<PbsType> {
|
||||||
|
if let Some(fields) = &self.current_struct_fields {
|
||||||
|
if let Some(ty) = fields.get(name) {
|
||||||
|
return Some(ty.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn lookup_type(&self, name: NameId) -> Option<&Symbol> {
|
fn lookup_type(&self, name: NameId) -> Option<&Symbol> {
|
||||||
if let Some(sym) = self.module_symbols.type_symbols.get(name) {
|
if let Some(sym) = self.module_symbols.type_symbols.get(name) {
|
||||||
return Some(sym);
|
return Some(sym);
|
||||||
@ -960,12 +1036,20 @@ mod tests {
|
|||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(bundle) => {
|
Err(bundle) => {
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
|
let full_src = fs::read_to_string(&file_path).unwrap();
|
||||||
|
let mut arounds = Vec::new();
|
||||||
for diag in bundle.diagnostics {
|
for diag in bundle.diagnostics {
|
||||||
let code = diag.code;
|
let code = diag.code;
|
||||||
errors.push(format!("{}: {}", code, diag.message));
|
let span = diag.span;
|
||||||
|
errors.push(format!("{}: {} @ {}..{}", code, diag.message, span.start, span.end));
|
||||||
|
let s = span.start as usize;
|
||||||
|
let e = span.end as usize;
|
||||||
|
let a = s.saturating_sub(30);
|
||||||
|
let b = (e + 30).min(full_src.len());
|
||||||
|
arounds.push(format!("[{}..{}]: >>>{}<<<", s, e, &full_src[a..b].replace('\n', "\\n")));
|
||||||
}
|
}
|
||||||
let err_msg = errors.join(", ");
|
let err_msg = errors.join(", ");
|
||||||
println!("Compilation failed: {}", err_msg);
|
println!("Compilation failed: {}\n{}", err_msg, arounds.join("\n"));
|
||||||
Err(err_msg)
|
Err(err_msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1118,7 +1202,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_struct_type_usage() {
|
fn test_struct_type_usage() {
|
||||||
let code = "
|
let code = "
|
||||||
declare struct Point { x: int, y: int }
|
declare struct Point(x: int, y: int)
|
||||||
fn foo(p: Point) {}
|
fn foo(p: Point) {}
|
||||||
fn main() {
|
fn main() {
|
||||||
// Struct literals not in v0, but we can have variables of struct type
|
// Struct literals not in v0, but we can have variables of struct type
|
||||||
@ -1214,4 +1298,32 @@ mod tests {
|
|||||||
if let Err(e) = &res { println!("Error: {}", e); }
|
if let Err(e) = &res { println!("Error: {}", e); }
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_struct_method_with_body_and_call() {
|
||||||
|
let code = r#"
|
||||||
|
// struct Color
|
||||||
|
declare struct Vec2(x: int, y: int)
|
||||||
|
[
|
||||||
|
(x: int, y: int): (x, y) as default
|
||||||
|
(s: int): (s, s) as square
|
||||||
|
]
|
||||||
|
[[
|
||||||
|
ZERO: square(0)
|
||||||
|
]]
|
||||||
|
{
|
||||||
|
pub fn len(self: this): int {
|
||||||
|
return x * x + y * y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frame(): void {
|
||||||
|
let zero = Vec2.ZERO;
|
||||||
|
let zz = zero.len();
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let res = check_code(code);
|
||||||
|
if let Err(e) = &res { println!("Error: {}", e); }
|
||||||
|
assert!(res.is_ok());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,7 +41,7 @@ pub fn lower_module(
|
|||||||
pub fn lower_function(
|
pub fn lower_function(
|
||||||
core_func: &ir_core::Function,
|
core_func: &ir_core::Function,
|
||||||
program: &ir_core::Program,
|
program: &ir_core::Program,
|
||||||
function_returns: &HashMap<ir_core::ids::FunctionId, ir_core::Type>
|
function_returns: &HashMap<ir_core::ids::FunctionId, ir_core::Type>,
|
||||||
) -> Result<ir_vm::Function> {
|
) -> Result<ir_vm::Function> {
|
||||||
let mut vm_func = ir_vm::Function {
|
let mut vm_func = ir_vm::Function {
|
||||||
id: core_func.id,
|
id: core_func.id,
|
||||||
@ -240,7 +240,7 @@ pub fn lower_function(
|
|||||||
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Not, span.clone()));
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Not, span.clone()));
|
||||||
}
|
}
|
||||||
ir_core::InstrKind::Alloc { ty, slots } => {
|
ir_core::InstrKind::Alloc { ty, slots } => {
|
||||||
stack_types.push(ir_core::Type::Struct("".to_string())); // It's a gate
|
stack_types.push(ir_core::Type::Contract(format!("Gate<{}>", ty.0)));
|
||||||
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Alloc {
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Alloc {
|
||||||
type_id: ir_vm::TypeId(ty.0),
|
type_id: ir_vm::TypeId(ty.0),
|
||||||
slots: *slots
|
slots: *slots
|
||||||
@ -368,14 +368,7 @@ pub fn lower_function(
|
|||||||
|
|
||||||
fn is_gate_type(ty: &ir_core::Type) -> bool {
|
fn is_gate_type(ty: &ir_core::Type) -> bool {
|
||||||
match ty {
|
match ty {
|
||||||
ir_core::Type::Struct(_) |
|
ir_core::Type::Contract(name) => name.starts_with("Gate<"),
|
||||||
ir_core::Type::Array(_, _) |
|
|
||||||
ir_core::Type::Optional(_) |
|
|
||||||
ir_core::Type::Result(_, _) |
|
|
||||||
ir_core::Type::Service(_) |
|
|
||||||
ir_core::Type::Contract(_) |
|
|
||||||
ir_core::Type::ErrorType(_) |
|
|
||||||
ir_core::Type::Function { .. } => true,
|
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -251,8 +251,8 @@ mod tests {
|
|||||||
let module_dir = dir.path().join("math");
|
let module_dir = dir.path().join("math");
|
||||||
fs::create_dir_all(&module_dir).unwrap();
|
fs::create_dir_all(&module_dir).unwrap();
|
||||||
|
|
||||||
fs::write(module_dir.join("Vector.pbs"), "pub declare struct Vector {}").unwrap();
|
fs::write(module_dir.join("Vector.pbs"), "pub declare struct Vector()").unwrap();
|
||||||
fs::write(module_dir.join("Internal.pbs"), "declare struct Hidden {}").unwrap();
|
fs::write(module_dir.join("Internal.pbs"), "declare struct Hidden()").unwrap();
|
||||||
|
|
||||||
let mut fm = FileManager::new();
|
let mut fm = FileManager::new();
|
||||||
let exports = build_exports(&module_dir, &mut fm).unwrap();
|
let exports = build_exports(&module_dir, &mut fm).unwrap();
|
||||||
|
|||||||
@ -21,8 +21,7 @@ path = "src/bin/prometeuc.rs"
|
|||||||
dist = true
|
dist = true
|
||||||
include = [
|
include = [
|
||||||
"../../VERSION.txt",
|
"../../VERSION.txt",
|
||||||
"../../dist-staging/devtools/debugger-protocol",
|
"../../dist-staging/devtools/debugger-protocol"
|
||||||
"../../dist-staging/devtools/typescript-sdk"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
564
docs/roadmaps/packer/Prometeu Packer.md
Normal file
564
docs/roadmaps/packer/Prometeu Packer.md
Normal file
@ -0,0 +1,564 @@
|
|||||||
|
# Prometeu Packer (prometeu-packer) — Specification (Draft)
|
||||||
|
|
||||||
|
> **Status:** Draft / Community-facing
|
||||||
|
>
|
||||||
|
> This document specifies the **Prometeu Packer**, the tooling responsible for **asset management** and producing two build artifacts:
|
||||||
|
>
|
||||||
|
> * `build/assets.pa` — the ROM payload containing packed asset bytes
|
||||||
|
> * `build/asset_table.json` — a machine-readable descriptor of the packed assets
|
||||||
|
>
|
||||||
|
> The Packer is deliberately **agnostic of cartridge building**. A separate **Cartridge Builder** (outside the Packer) consumes `build/asset_table.json` to generate the final `cartridge/manifest.json`, copy `assets.pa` to the cartridge directory, and zip the cartridge into a distributable format.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Goals and Non-Goals
|
||||||
|
|
||||||
|
### 1.1 Goals
|
||||||
|
|
||||||
|
1. **Be the guardian of sanity** in a constantly mutating `assets/` workspace.
|
||||||
|
|
||||||
|
* Users may be disorganized.
|
||||||
|
* The directory may contain WIP, junk, unused files, duplicates, outdated exports.
|
||||||
|
* The Packer must help users identify and fix mistakes.
|
||||||
|
|
||||||
|
2. Provide a robust, deterministic, **tooling-grade** asset pipeline.
|
||||||
|
|
||||||
|
* Stable asset identity.
|
||||||
|
* Deterministic packing order.
|
||||||
|
* Reproducible output bytes.
|
||||||
|
|
||||||
|
3. Support both **raw (direct) assets** and **virtual assets**.
|
||||||
|
|
||||||
|
* Raw assets: the payload in ROM is exactly the source bytes.
|
||||||
|
* Virtual assets: the payload is derived from multiple inputs via a build pipeline (e.g., PNG + palettes → `TILES` payload).
|
||||||
|
|
||||||
|
4. Produce an output descriptor (`build/asset_table.json`) suitable for:
|
||||||
|
|
||||||
|
* a Cartridge Builder to generate the runtime manifest
|
||||||
|
* CI checks
|
||||||
|
* a future IDE / GUI tooling
|
||||||
|
|
||||||
|
5. Provide an extensive **diagnostics chain** (doctor) with structured error codes and suggested fixes.
|
||||||
|
|
||||||
|
### 1.2 Non-Goals
|
||||||
|
|
||||||
|
* The Packer **does not**:
|
||||||
|
|
||||||
|
* generate `cartridge/manifest.json`
|
||||||
|
* decide preload slots
|
||||||
|
* copy files into `cartridge/`
|
||||||
|
* compile PBS bytecode
|
||||||
|
* zip cartridges
|
||||||
|
|
||||||
|
These responsibilities belong to a separate **Cartridge Builder**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Repository / Project Layout (Golden Pipeline)
|
||||||
|
|
||||||
|
The Prometeu project uses the following canonical structure:
|
||||||
|
|
||||||
|
* `src/` — PBS scripts
|
||||||
|
* `assets/` — mutable asset workspace (WIP allowed)
|
||||||
|
* `build/` — generated artifacts and caches
|
||||||
|
* `cartridge/` — final cartridge directory (produced by Cartridge Builder)
|
||||||
|
* `prometeu.json` — project description (dependencies, version, etc.)
|
||||||
|
* `sdk/` — SDK/tooling and libraries
|
||||||
|
|
||||||
|
The Packer owns the asset workspace metadata under:
|
||||||
|
|
||||||
|
* `assets/.prometeu/` — Packer control directory (registry, cache, quarantine)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Crate Topology
|
||||||
|
|
||||||
|
### 3.1 Crates
|
||||||
|
|
||||||
|
* **`prometeu-packer`**
|
||||||
|
|
||||||
|
* **lib**: `prometeu_packer_core`
|
||||||
|
* **bin**: `prometeu-packer`
|
||||||
|
|
||||||
|
* a thin CLI wrapper delegating to `prometeu_packer_core::run()`
|
||||||
|
|
||||||
|
* **`prometeu` dispatcher**
|
||||||
|
|
||||||
|
* provides a wrapper command **`prometeup`** (or integrated subcommand)
|
||||||
|
* delegates to `prometeu-packer` for packer operations
|
||||||
|
|
||||||
|
### 3.2 Design Principle
|
||||||
|
|
||||||
|
Treat the Packer like the compiler: core library + CLI wrapper.
|
||||||
|
|
||||||
|
* The core library enables future integrations (IDE, GUI, watch mode) without shelling out.
|
||||||
|
* CLI is a stable interface for users and CI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Mental Model: A “Git-like” Asset Workspace
|
||||||
|
|
||||||
|
The Packer treats `assets/` like a **dirty working tree**.
|
||||||
|
|
||||||
|
* `assets/` can contain *anything*.
|
||||||
|
* Only the assets registered in the Packer registry are considered part of the build.
|
||||||
|
|
||||||
|
This is analogous to Git:
|
||||||
|
|
||||||
|
* working tree (chaos) vs index (truth)
|
||||||
|
|
||||||
|
The **source of truth** for “what counts” is the registry:
|
||||||
|
|
||||||
|
* `assets/.prometeu/index.json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Core Concepts
|
||||||
|
|
||||||
|
### 5.1 Managed Asset
|
||||||
|
|
||||||
|
A **managed asset** is an entry in `assets/.prometeu/index.json` pointing to an **asset root directory** that contains an anchor file:
|
||||||
|
|
||||||
|
* `asset.json` (the asset specification)
|
||||||
|
|
||||||
|
Everything else is free-form.
|
||||||
|
|
||||||
|
### 5.2 Asset Identity
|
||||||
|
|
||||||
|
Each asset has stable identity:
|
||||||
|
|
||||||
|
* `asset_id: u32` — stable within the project (used by runtime/tooling)
|
||||||
|
* `asset_uuid: string` — globally unique stable identifier (useful for IDE and future migrations)
|
||||||
|
|
||||||
|
Names and paths may change, but identity remains.
|
||||||
|
|
||||||
|
### 5.3 Asset Types (Bank Targets)
|
||||||
|
|
||||||
|
Assets ultimately target a **bank type** in the runtime:
|
||||||
|
|
||||||
|
* `TILES`
|
||||||
|
* `SOUNDS`
|
||||||
|
* (future) `SPRITESHEET`, `MAP`, `FONT`, `RAW_BLOB`, etc.
|
||||||
|
|
||||||
|
The Packer does **not** define bank memory semantics. It defines the *ROM payload* and its metadata.
|
||||||
|
|
||||||
|
### 5.4 Raw vs Virtual Assets
|
||||||
|
|
||||||
|
* **Raw assets**: ROM payload equals the source bytes.
|
||||||
|
* **Virtual assets**: ROM payload is derived from input(s) via deterministic build steps.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
* PNG + palette files → `TILES` payload (indexed pixels + packed palettes)
|
||||||
|
* WAV → PCM16LE payload
|
||||||
|
* Multiple PNGs → atlas spritesheet
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Directory Structure and Control Files
|
||||||
|
|
||||||
|
### 6.1 Workspace
|
||||||
|
|
||||||
|
`assets/` is a mutable workspace:
|
||||||
|
|
||||||
|
* users may create nested organization trees
|
||||||
|
* junk files are allowed
|
||||||
|
|
||||||
|
### 6.2 Control Directory
|
||||||
|
|
||||||
|
The Packer stores its truth + tools state in:
|
||||||
|
|
||||||
|
```
|
||||||
|
assets/
|
||||||
|
.prometeu/
|
||||||
|
index.json
|
||||||
|
cache/
|
||||||
|
fingerprints.json
|
||||||
|
build-cache.json
|
||||||
|
quarantine/
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
* `index.json` — registry of managed assets
|
||||||
|
* `cache/` — fingerprints and incremental build cache
|
||||||
|
* `quarantine/` — optional area to move detected junk (only by explicit user action)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Asset Specification: `asset.json`
|
||||||
|
|
||||||
|
`asset.json` describes:
|
||||||
|
|
||||||
|
1. **the output ROM payload** expected by runtime
|
||||||
|
2. **the build pipeline** (for virtual assets)
|
||||||
|
3. **metadata** needed by runtime/builder
|
||||||
|
|
||||||
|
This spec is modular: **each asset format** (e.g. `TILES/indexed_v1`) has its own dedicated specification document.
|
||||||
|
|
||||||
|
### 7.1 Common Fields (All Assets)
|
||||||
|
|
||||||
|
* `schema_version`
|
||||||
|
* `name`
|
||||||
|
* `type` (bank type)
|
||||||
|
* `codec` (e.g. `RAW`; future: compression)
|
||||||
|
* `inputs` (for virtual assets)
|
||||||
|
* `output` (format + required metadata)
|
||||||
|
* `build` (optional pipeline configuration)
|
||||||
|
|
||||||
|
### 7.2 Virtual Asset Pipeline Declaration
|
||||||
|
|
||||||
|
Virtual assets must be declared in a way that is:
|
||||||
|
|
||||||
|
* deterministic
|
||||||
|
* fully materialized (no silent inference)
|
||||||
|
* explicit about defaults (defaults may exist, but must be written into `asset.json` or build outputs)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Build Artifacts Produced by the Packer
|
||||||
|
|
||||||
|
### 8.1 `build/assets.pa`
|
||||||
|
|
||||||
|
**`assets.pa`** is the ROM asset payload used by the runtime.
|
||||||
|
|
||||||
|
**Definition:** a contiguous binary blob where each managed asset contributes a payload region.
|
||||||
|
|
||||||
|
#### Key Properties
|
||||||
|
|
||||||
|
* Deterministic asset order (by `asset_id`)
|
||||||
|
* Offsets are recorded in `build/asset_table.json`
|
||||||
|
* Alignment rules (configurable by packer, default: no alignment unless required by a format)
|
||||||
|
|
||||||
|
**Note:** `assets.pa` is intentionally simple.
|
||||||
|
|
||||||
|
* No internal header is required.
|
||||||
|
* The authoritative structure comes from the descriptor (`asset_table.json`).
|
||||||
|
|
||||||
|
Future versions may introduce chunk tables, but v1 keeps ROM simple.
|
||||||
|
|
||||||
|
### 8.2 `build/asset_table.json`
|
||||||
|
|
||||||
|
**`asset_table.json`** is the canonical descriptor output of the Packer.
|
||||||
|
|
||||||
|
It contains:
|
||||||
|
|
||||||
|
* `assets_pa` file info (size, hash)
|
||||||
|
* `asset_table[]` entries describing each payload slice
|
||||||
|
* optional diagnostics/warnings
|
||||||
|
|
||||||
|
#### Asset Table Entry
|
||||||
|
|
||||||
|
An entry describes a ROM slice and its runtime meaning:
|
||||||
|
|
||||||
|
* `asset_id` — stable u32
|
||||||
|
* `asset_uuid` — stable UUID string
|
||||||
|
* `asset_name` — stable user-facing name
|
||||||
|
* `bank_type` — e.g. `TILES`, `SOUNDS`
|
||||||
|
* `offset` — byte offset in `assets.pa`
|
||||||
|
* `size` — bytes stored in ROM
|
||||||
|
* `decoded_size` — bytes after decode (equal to `size` when `codec=RAW`)
|
||||||
|
* `codec` — `RAW` (future: compression)
|
||||||
|
* `metadata` — format-specific metadata needed by runtime/builder
|
||||||
|
|
||||||
|
Additional tooling fields:
|
||||||
|
|
||||||
|
* `source_root` — path to asset dir
|
||||||
|
* `inputs` — resolved input paths
|
||||||
|
* `source_hashes` — stable fingerprints of inputs
|
||||||
|
|
||||||
|
`asset_table.json` is machine-readable and designed for:
|
||||||
|
|
||||||
|
* cartridge builder consumption
|
||||||
|
* IDE visualization
|
||||||
|
* debugging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Determinism Rules
|
||||||
|
|
||||||
|
1. Asset packing order MUST be deterministic.
|
||||||
|
|
||||||
|
* Default: increasing `asset_id`
|
||||||
|
|
||||||
|
2. All derived outputs MUST be deterministic.
|
||||||
|
|
||||||
|
* No random seeds unless explicitly declared
|
||||||
|
* Any seed must be written to output metadata
|
||||||
|
|
||||||
|
3. Default values MUST be materialized.
|
||||||
|
|
||||||
|
* If the packer infers something, it must be written into `asset.json` (via `--fix`) or recorded in build outputs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Diagnostics and the “Sanity Guardian” Chain
|
||||||
|
|
||||||
|
The Packer provides structured diagnostics:
|
||||||
|
|
||||||
|
* `code` — stable diagnostic code
|
||||||
|
* `severity` — `error | warning | info`
|
||||||
|
* `path` — affected file
|
||||||
|
* `message` — human friendly
|
||||||
|
* `help` — extended context
|
||||||
|
* `fixes[]` — suggested automated or manual fixes
|
||||||
|
|
||||||
|
### 10.1 Diagnostic Classes
|
||||||
|
|
||||||
|
1. **Registered Errors** (break build)
|
||||||
|
|
||||||
|
* registry entry missing anchor file
|
||||||
|
* `asset.json` invalid
|
||||||
|
* missing inputs
|
||||||
|
* format/metadata mismatch
|
||||||
|
|
||||||
|
2. **Workspace Warnings** (does not break build)
|
||||||
|
|
||||||
|
* orphaned `asset.json` (not registered)
|
||||||
|
* unused large files
|
||||||
|
* duplicate inputs by hash
|
||||||
|
|
||||||
|
3. **Policy Hints** (optional)
|
||||||
|
|
||||||
|
* naming conventions
|
||||||
|
* missing preview
|
||||||
|
|
||||||
|
### 10.2 `doctor` Modes
|
||||||
|
|
||||||
|
* `doctor` (default) — validate registry only (fast)
|
||||||
|
* `doctor --workspace` — deep scan workspace (slow)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Incremental Build, Cache, and Fingerprints
|
||||||
|
|
||||||
|
The Packer maintains fingerprints of inputs:
|
||||||
|
|
||||||
|
* size
|
||||||
|
* mtime
|
||||||
|
* strong hash (sha256)
|
||||||
|
|
||||||
|
Stored in:
|
||||||
|
|
||||||
|
* `assets/.prometeu/cache/fingerprints.json`
|
||||||
|
|
||||||
|
This enables:
|
||||||
|
|
||||||
|
* detecting changes
|
||||||
|
* rebuild only what changed
|
||||||
|
* producing stable reports
|
||||||
|
|
||||||
|
The cache must never compromise determinism.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Quarantine and Garbage Collection
|
||||||
|
|
||||||
|
### 12.1 Quarantine
|
||||||
|
|
||||||
|
The Packer can optionally move suspected junk to:
|
||||||
|
|
||||||
|
* `assets/.prometeu/quarantine/`
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
* Quarantine is **never automatic** without user consent.
|
||||||
|
* Packer must explain exactly what will be moved.
|
||||||
|
|
||||||
|
### 12.2 Garbage Collection (`gc`)
|
||||||
|
|
||||||
|
The Packer can report unused files:
|
||||||
|
|
||||||
|
* files not referenced by any registered asset
|
||||||
|
* orphaned asset dirs
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
|
||||||
|
* list candidates
|
||||||
|
* optionally move to quarantine
|
||||||
|
* never delete without explicit user request
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. CLI Commands (Comprehensive)
|
||||||
|
|
||||||
|
> The CLI is a stable interface; all commands are implemented by calling `prometeu_packer_core`.
|
||||||
|
|
||||||
|
### 13.1 `prometeu packer init`
|
||||||
|
|
||||||
|
Creates the control directory and initial registry:
|
||||||
|
|
||||||
|
* creates `assets/.prometeu/index.json`
|
||||||
|
* creates caches directory
|
||||||
|
|
||||||
|
### 13.2 `prometeu packer add <path> [--name <name>] [--type <TILES|SOUNDS|...>]`
|
||||||
|
|
||||||
|
Registers a new managed asset.
|
||||||
|
|
||||||
|
* does not require moving files
|
||||||
|
* can create an asset root directory if desired
|
||||||
|
* generates `asset.json` with explicit defaults
|
||||||
|
* allocates `asset_id` and `asset_uuid`
|
||||||
|
|
||||||
|
Variants:
|
||||||
|
|
||||||
|
* `add --dir` creates a dedicated asset root dir
|
||||||
|
* `add --in-place` anchors next to the file
|
||||||
|
|
||||||
|
### 13.3 `prometeu packer adopt`
|
||||||
|
|
||||||
|
Scans workspace for unregistered `asset.json` anchors and offers to register them.
|
||||||
|
|
||||||
|
* default: dry-run list
|
||||||
|
* `--apply` registers them
|
||||||
|
|
||||||
|
### 13.4 `prometeu packer forget <name|id|uuid>`
|
||||||
|
|
||||||
|
Removes an asset from the registry without deleting files.
|
||||||
|
|
||||||
|
Useful for WIP and cleanup.
|
||||||
|
|
||||||
|
### 13.5 `prometeu packer rm <name|id|uuid> [--delete]`
|
||||||
|
|
||||||
|
Removes the asset from the registry.
|
||||||
|
|
||||||
|
* default: no deletion
|
||||||
|
* `--delete` can remove the asset root dir (dangerous; must confirm in UI tooling, or require a force flag in CLI)
|
||||||
|
|
||||||
|
### 13.6 `prometeu packer list`
|
||||||
|
|
||||||
|
Lists managed assets:
|
||||||
|
|
||||||
|
* id, uuid, name
|
||||||
|
* type
|
||||||
|
* status (ok/error)
|
||||||
|
|
||||||
|
### 13.7 `prometeu packer show <name|id|uuid>`
|
||||||
|
|
||||||
|
Shows detailed information:
|
||||||
|
|
||||||
|
* resolved inputs
|
||||||
|
* metadata
|
||||||
|
* fingerprints
|
||||||
|
* last build summary
|
||||||
|
|
||||||
|
### 13.8 `prometeu packer doctor [--workspace] [--strict] [--fix]`
|
||||||
|
|
||||||
|
Runs diagnostics:
|
||||||
|
|
||||||
|
* `--workspace` deep scan
|
||||||
|
* `--strict` warnings become errors
|
||||||
|
* `--fix` applies safe automatic fixes (materialize defaults, normalize paths)
|
||||||
|
|
||||||
|
### 13.9 `prometeu packer build [--out build/assets.pa] [--table build/asset_table.json]`
|
||||||
|
|
||||||
|
Builds:
|
||||||
|
|
||||||
|
* `build/assets.pa`
|
||||||
|
* `build/asset_table.json`
|
||||||
|
|
||||||
|
Key behaviors:
|
||||||
|
|
||||||
|
* validates registry before packing
|
||||||
|
* packs assets deterministically
|
||||||
|
* for virtual assets, runs build pipelines
|
||||||
|
* records all offsets and metadata
|
||||||
|
|
||||||
|
### 13.10 `prometeu packer watch`
|
||||||
|
|
||||||
|
Watches registered inputs and registry changes.
|
||||||
|
|
||||||
|
* emits events (future)
|
||||||
|
* rebuilds incrementally
|
||||||
|
|
||||||
|
`watch` is optional in v0 but recommended.
|
||||||
|
|
||||||
|
### 13.11 `prometeu packer gc [--workspace] [--quarantine]`
|
||||||
|
|
||||||
|
Reports unused files.
|
||||||
|
|
||||||
|
* default: report only
|
||||||
|
* `--quarantine` moves candidates to quarantine
|
||||||
|
|
||||||
|
### 13.12 `prometeu packer quarantine <path> [--restore]`
|
||||||
|
|
||||||
|
Moves or restores files into/from quarantine.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Virtual Assets (Deep Explanation)
|
||||||
|
|
||||||
|
Virtual assets are a major capability.
|
||||||
|
|
||||||
|
### 14.1 Why Virtual Assets
|
||||||
|
|
||||||
|
* Most runtime formats should be derived from human-friendly authoring formats.
|
||||||
|
* Example:
|
||||||
|
|
||||||
|
* author uses `source.png` and palette files
|
||||||
|
* runtime expects indexed pixels + packed RGB565 palettes
|
||||||
|
|
||||||
|
### 14.2 Virtual Asset Contract
|
||||||
|
|
||||||
|
* Inputs are explicit.
|
||||||
|
* Build steps are deterministic.
|
||||||
|
* Outputs match a well-defined runtime payload format.
|
||||||
|
|
||||||
|
### 14.3 Examples of Future Virtual Assets
|
||||||
|
|
||||||
|
* `TILES/indexed_v1`: PNG + palette files → indexed pixels + packed palettes
|
||||||
|
* `SOUNDS/pcm16le_v1`: WAV → PCM16LE
|
||||||
|
* `SPRITESHEET/atlas_v1`: multiple PNG frames → atlas + metadata
|
||||||
|
|
||||||
|
Each `output.format` must have its own dedicated spec.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Integration with Cartridge Builder
|
||||||
|
|
||||||
|
The Cartridge Builder should:
|
||||||
|
|
||||||
|
1. Compile PBS into bytecode (e.g. `program.pbc` / `program.pbx`)
|
||||||
|
2. Call `prometeu packer build`
|
||||||
|
3. Consume `build/asset_table.json` and produce `cartridge/manifest.json`
|
||||||
|
4. Copy artifacts into `cartridge/`
|
||||||
|
5. Zip the cartridge into a distributable format (`.crt` / `.rom` / `.pro`)
|
||||||
|
|
||||||
|
The packer never touches `cartridge/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 16. Compatibility and Versioning
|
||||||
|
|
||||||
|
* `assets/.prometeu/index.json` has `schema_version`
|
||||||
|
* `asset.json` has `schema_version`
|
||||||
|
* `build/asset_table.json` has `schema_version`
|
||||||
|
|
||||||
|
The Packer must be able to migrate older schema versions or emit actionable diagnostics.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 17. Security and Trust Model
|
||||||
|
|
||||||
|
* The Packer is offline tooling.
|
||||||
|
* It must never execute untrusted scripts.
|
||||||
|
* It should treat external inputs as untrusted data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 18. Implementation Notes (Non-Normative)
|
||||||
|
|
||||||
|
* Rust implementation with a core crate + CLI wrapper.
|
||||||
|
* Prefer structured JSON serde models.
|
||||||
|
* Use stable diagnostic codes.
|
||||||
|
* Keep the build deterministic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix A — Glossary
|
||||||
|
|
||||||
|
* **ROM (`assets.pa`)**: packed asset payload used by runtime
|
||||||
|
* **Descriptor (`asset_table.json`)**: mapping from logical assets to ROM slices
|
||||||
|
* **Managed asset**: registered asset with stable identity and anchor file
|
||||||
|
* **Virtual asset**: derived asset built from multiple inputs
|
||||||
|
* **Quarantine**: safe area for suspected junk
|
||||||
|
* **Doctor**: diagnostic command to keep sanity
|
||||||
@ -2,12 +2,7 @@
|
|||||||
"kind": "File",
|
"kind": "File",
|
||||||
"imports": [],
|
"imports": [],
|
||||||
"decls": [
|
"decls": [
|
||||||
21,
|
30,
|
||||||
26,
|
40
|
||||||
39,
|
|
||||||
44,
|
|
||||||
48,
|
|
||||||
57,
|
|
||||||
103
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -1,176 +1,19 @@
|
|||||||
0000 GetLocal U32(0)
|
0000 PushConst U32(1)
|
||||||
0006 GetLocal U32(1)
|
0006 SetLocal U32(0)
|
||||||
000C Add
|
000C GetLocal U32(0)
|
||||||
000E Ret
|
0012 GetLocal U32(0)
|
||||||
0010 PushConst U32(1)
|
0018 SetLocal U32(1)
|
||||||
0016 SetLocal U32(0)
|
001E SetLocal U32(0)
|
||||||
001C PushConst U32(2)
|
0024 GetLocal U32(0)
|
||||||
0022 SetLocal U32(1)
|
002A GetLocal U32(1)
|
||||||
0028 GetLocal U32(0)
|
0030 Call U32(1)
|
||||||
002E GetLocal U32(1)
|
0036 SetLocal U32(2)
|
||||||
0034 Call U32(0)
|
003C Ret
|
||||||
003A SetLocal U32(2)
|
003E GetLocal U32(0)
|
||||||
0040 GetLocal U32(2)
|
0044 GetLocal U32(0)
|
||||||
0046 PushConst U32(3)
|
004A GetLocal U32(1)
|
||||||
004C Eq
|
0050 Add
|
||||||
004E JmpIfFalse U32(92)
|
0052 Mul
|
||||||
0054 Jmp U32(74)
|
0054 GetLocal U32(1)
|
||||||
005A PushBounded U32(2016)
|
005A Mul
|
||||||
0060 Syscall U32(4112)
|
005C Ret
|
||||||
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.
@ -1,66 +1,18 @@
|
|||||||
// CartridgeCanonical.pbs
|
declare struct Vec2(x: int, y: int)
|
||||||
// Purpose: VM Heartbeat Test (Industrial Baseline)
|
[
|
||||||
|
(x: int, y: int): (x, y) as default
|
||||||
declare struct Color(raw: bounded)
|
(s: int): (s, s) as square
|
||||||
|
]
|
||||||
[[
|
[[
|
||||||
BLACK: Color(0b),
|
ZERO: square(0)
|
||||||
WHITE: Color(65535b),
|
|
||||||
RED: Color(63488b),
|
|
||||||
GREEN: Color(2016b),
|
|
||||||
BLUE: Color(31b)
|
|
||||||
]]
|
]]
|
||||||
|
{
|
||||||
declare struct ButtonState(
|
pub fn len(self: this): int {
|
||||||
pressed: bool,
|
return x * x + y * y;
|
||||||
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 {
|
fn frame(): void {
|
||||||
// 1. Locals & Arithmetic
|
let zero = Vec2.ZERO;
|
||||||
let x = 10;
|
let zz = zero.len();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user