fix structs

This commit is contained in:
bQUARKz 2026-02-06 21:11:04 +00:00
parent 5cf77359fb
commit d994005f8b
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
14 changed files with 1366 additions and 347 deletions

View File

@ -57,15 +57,10 @@ pub fn emit_module(module: &ir_vm::Module) -> Result<EmitResult> {
pub fn emit_fragments(module: &ir_vm::Module) -> Result<EmitFragments> {
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 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 assemble_res = prometeu_bytecode::asm::assemble_with_unresolved(&asm_instrs).map_err(|e| anyhow!(e))?;
@ -155,11 +150,15 @@ impl BytecodeEmitter {
&mut self,
module: &'b ir_vm::Module,
asm_instrs: &mut Vec<Asm>,
ir_instr_map: &mut Vec<Option<&'b ir_vm::Instruction>>,
mapped_const_ids: &[u32]
ir_instr_map: &mut Vec<Option<&'b ir_vm::Instruction>>
) -> 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();
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());
}
@ -170,6 +169,8 @@ impl BytecodeEmitter {
// Each function starts with a label for its entry point.
asm_instrs.push(Asm::Label(function.name.clone()));
ir_instr_map.push(None);
// Track an approximate stack height for this function
let mut stack_height: i32 = 0;
for instr in &function.body {
let op_start_idx = asm_instrs.len();
@ -179,32 +180,55 @@ impl BytecodeEmitter {
InstrKind::Nop => asm_instrs.push(Asm::Op(OpCode::Nop, vec![])),
InstrKind::Halt => asm_instrs.push(Asm::Op(OpCode::Halt, vec![])),
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)]));
stack_height += 1;
}
InstrKind::PushBounded(val) => {
asm_instrs.push(Asm::Op(OpCode::PushBounded, vec![Operand::U32(*val)]));
stack_height += 1;
}
InstrKind::PushBool(v) => {
asm_instrs.push(Asm::Op(OpCode::PushBool, vec![Operand::Bool(*v)]));
stack_height += 1;
}
InstrKind::PushNull => {
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::Add => asm_instrs.push(Asm::Op(OpCode::Add, vec![])),
InstrKind::Sub => asm_instrs.push(Asm::Op(OpCode::Sub, vec![])),
InstrKind::Mul => asm_instrs.push(Asm::Op(OpCode::Mul, vec![])),
InstrKind::Div => asm_instrs.push(Asm::Op(OpCode::Div, vec![])),
InstrKind::Neg => asm_instrs.push(Asm::Op(OpCode::Neg, vec![])),
InstrKind::Eq => asm_instrs.push(Asm::Op(OpCode::Eq, vec![])),
InstrKind::Neq => asm_instrs.push(Asm::Op(OpCode::Neq, vec![])),
InstrKind::Lt => asm_instrs.push(Asm::Op(OpCode::Lt, vec![])),
InstrKind::Gt => asm_instrs.push(Asm::Op(OpCode::Gt, vec![])),
InstrKind::Lte => asm_instrs.push(Asm::Op(OpCode::Lte, vec![])),
InstrKind::Gte => asm_instrs.push(Asm::Op(OpCode::Gte, vec![])),
InstrKind::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![])); stack_height = (stack_height - 1).max(0); }
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![])); stack_height = (stack_height - 1).max(0); }
InstrKind::Neg => { asm_instrs.push(Asm::Op(OpCode::Neg, vec![])); /* unary */ }
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![])); stack_height = (stack_height - 1).max(0); }
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![])); stack_height = (stack_height - 1).max(0); }
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![])); stack_height = (stack_height - 1).max(0); }
InstrKind::And => asm_instrs.push(Asm::Op(OpCode::And, vec![])),
InstrKind::Or => asm_instrs.push(Asm::Op(OpCode::Or, vec![])),
InstrKind::Not => asm_instrs.push(Asm::Op(OpCode::Not, vec![])),
@ -215,46 +239,76 @@ impl BytecodeEmitter {
InstrKind::Shr => asm_instrs.push(Asm::Op(OpCode::Shr, vec![])),
InstrKind::LocalLoad { slot } => {
asm_instrs.push(Asm::Op(OpCode::GetLocal, vec![Operand::U32(*slot)]));
stack_height += 1;
}
InstrKind::LocalStore { slot } => {
asm_instrs.push(Asm::Op(OpCode::SetLocal, vec![Operand::U32(*slot)]));
stack_height = (stack_height - 1).max(0);
}
InstrKind::GetGlobal(slot) => {
asm_instrs.push(Asm::Op(OpCode::GetGlobal, vec![Operand::U32(*slot)]));
stack_height += 1;
}
InstrKind::SetGlobal(slot) => {
asm_instrs.push(Asm::Op(OpCode::SetGlobal, vec![Operand::U32(*slot)]));
stack_height = (stack_height - 1).max(0);
}
InstrKind::Jmp(label) => {
asm_instrs.push(Asm::Op(OpCode::Jmp, vec![Operand::RelLabel(label.0.clone(), function.name.clone())]));
}
InstrKind::JmpIfFalse(label) => {
asm_instrs.push(Asm::Op(OpCode::JmpIfFalse, vec![Operand::RelLabel(label.0.clone(), function.name.clone())]));
// VM consumes the condition for JmpIfFalse
stack_height = (stack_height - 1).max(0);
}
InstrKind::Label(label) => {
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, .. } => {
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())]));
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))?;
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);
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) => {
asm_instrs.push(Asm::Op(OpCode::Syscall, vec![Operand::U32(*id)]));
}
InstrKind::FrameSync => asm_instrs.push(Asm::Op(OpCode::FrameSync, vec![])),
InstrKind::Alloc { type_id, 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 } => {
asm_instrs.push(Asm::Op(OpCode::GateLoad, vec![Operand::U32(*offset)]));
stack_height += 1;
}
InstrKind::GateStore { 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::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::types::Type;
use prometeu_bytecode::{BytecodeLoader, ConstantPoolEntry};
use prometeu_bytecode::disasm::disasm;
#[test]
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[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");
}
}

View File

@ -33,6 +33,11 @@ pub struct Lowerer<'a> {
struct_constructors: HashMap<String, HashMap<String, NodeId>>,
type_constants: HashMap<String, HashMap<String, NodeId>>,
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,
diagnostics: Vec<Diagnostic>,
max_slots_used: u32,
@ -78,6 +83,9 @@ impl<'a> Lowerer<'a> {
struct_constructors: HashMap::new(),
type_constants: HashMap::new(),
current_type_context: None,
user_struct_field_offsets: HashMap::new(),
user_struct_field_types: HashMap::new(),
method_self_slot: None,
contract_registry: ContractRegistry::new(),
diagnostics: Vec::new(),
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> {
// 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) {
NodeKind::File(file) => file,
_ => {
@ -120,6 +131,21 @@ impl<'a> Lowerer<'a> {
self.next_type_id += 1;
self.type_ids
.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 {
name: module_name.to_string(),
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);
Ok(self.program)
}
@ -515,6 +596,23 @@ impl<'a> Lowerer<'a> {
} else {
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 {
Type::Int
}
@ -546,6 +644,27 @@ impl<'a> Lowerer<'a> {
}
Ok(())
} 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
match name_str {
"true" => {
@ -664,6 +783,11 @@ impl<'a> Lowerer<'a> {
}
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 {
"ButtonState" => match field_name {
"pressed" => 0,
@ -698,6 +822,11 @@ impl<'a> Lowerer<'a> {
}
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 {
"Pad" => Type::Struct("ButtonState".to_string()),
"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 {
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<(), ()> {
let ctor_id = ctor;
let ctor = match self.arena.kind(ctor) {
@ -1167,6 +1433,13 @@ impl<'a> Lowerer<'a> {
"bool" => Type::Bool,
"string" => Type::String,
"void" => Type::Void,
"this" => {
if let Some(ctx) = &self.current_type_context {
Type::Struct(ctx.clone())
} else {
Type::Void
}
}
_ => {
if let Some(sym) = self
.module_symbols
@ -1528,7 +1801,7 @@ mod tests {
}
declare contract MyContract host {}
declare error MyError {}
declare struct Point { x: int }
declare struct Point(x: int)
fn main(
s: MyService,

View File

@ -296,48 +296,67 @@ impl<'a> Parser<'a> {
};
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()?;
}
// Permitir blocos opcionais em QUALQUER ordem após o nome do tipo.
// Apenas o bloco de parâmetros/dados `(...)` é obrigatório para `struct`.
let mut params: Vec<ParamNodeArena> = Vec::new();
let mut constructors: Vec<NodeId> = Vec::new();
let mut constants: Vec<NodeId> = Vec::new();
let mut body: Option<NodeId> = None;
let mut is_host = false;
if self.peek().kind == TokenKind::Host {
self.advance();
is_host = true;
}
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)?;
let c_span = Span::new(self.file_id, c_start, self.arena.span(c_value).end);
constants.push(self.arena.push(
NodeKind::ConstantDecl(ConstantDeclNodeArena {
name: c_name,
value: c_value,
}),
c_span,
));
if self.peek().kind == TokenKind::Comma {
self.advance();
let mut seen_params = false;
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();
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)?;
let c_span = Span::new(self.file_id, c_start, self.arena.span(c_value).end);
constants.push(self.arena.push(
NodeKind::ConstantDecl(ConstantDeclNodeArena { name: c_name, value: c_value }),
c_span,
));
if self.peek().kind == TokenKind::Comma {
self.advance();
}
}
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,
}
self.consume(TokenKind::CloseDoubleBracket)?;
}
let mut body = None;
if self.peek().kind == TokenKind::OpenBrace {
body = Some(self.parse_type_body()?);
// Para 'struct', exigir que o bloco de parâmetros tenha sido fornecido
if type_kind == "struct" && !seen_params {
return Err(self.error_with_code("Expected parameter block '(...)' in struct declaration", Some("E_PARSE_EXPECTED_TOKEN")));
}
let mut end_pos = start_span.end;
@ -373,9 +392,18 @@ impl<'a> Parser<'a> {
let mut members = Vec::new();
let mut methods = Vec::new();
while self.peek().kind != TokenKind::CloseBrace && self.peek().kind != TokenKind::Eof {
if self.peek().kind == TokenKind::Fn {
let sig_node = self.parse_service_member()?;
methods.push(sig_node);
if self.peek().kind == TokenKind::Pub {
// pub fn ... { ... }
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 {
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> {
let start_span = self.consume(TokenKind::Fn)?.span;
let name = self.expect_identifier()?;
@ -1074,8 +1154,14 @@ impl<'a> Parser<'a> {
self.consume(TokenKind::As)?;
let name = self.expect_identifier()?;
let body = self.parse_block()?;
// Corpo opcional do construtor
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);
constructors.push(self.arena.push(
@ -1169,10 +1255,7 @@ fn add(a: int, b: int): int {
#[test]
fn test_parse_type_decl() {
let source = r#"
pub declare struct Point {
x: int,
y: int
}
pub declare struct Point(x: int, y: int){}
"#;
let mut interner = NameInterner::new();
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]
fn test_parse_service_decl() {
let source = r#"

View File

@ -908,8 +908,8 @@ mod tests {
#[test]
fn test_duplicate_symbols() {
let source = "
declare struct Foo {}
declare struct Foo {}
declare struct Foo()
declare struct Foo()
";
let (parsed, _, interner) = setup_test(source);
let mut collector = SymbolCollector::new(&interner);
@ -923,7 +923,7 @@ mod tests {
#[test]
fn test_namespace_collision() {
let source = "
declare struct Foo {}
declare struct Foo()
fn Foo() {}
";
let (parsed, _, interner) = setup_test(source);
@ -1268,7 +1268,7 @@ mod tests {
#[test]
fn test_lower_type_node_complex() {
let source = "
declare struct MyType {}
declare struct MyType()
fn main() {
let x: optional<int> = 1;
let y: result<int, string> = 2;

View File

@ -21,6 +21,9 @@ pub struct TypeChecker<'a> {
struct_methods: HashMap<String, HashMap<String, PbsType>>,
diagnostics: Vec<Diagnostic>,
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> {
@ -43,6 +46,8 @@ impl<'a> TypeChecker<'a> {
struct_methods: HashMap::new(),
diagnostics: Vec::new(),
contract_registry: ContractRegistry::new(),
current_type_context: None,
current_struct_fields: None,
}
}
@ -150,19 +155,39 @@ impl<'a> TypeChecker<'a> {
let mut methods = HashMap::new();
if let Some(body_node) = n.body {
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 {
if let NodeKind::ServiceFnSig(sig) = arena.kind(*m) {
let mut params = Vec::new();
for p in &sig.params {
params.push(self.resolve_type_node(arena, p.ty));
match arena.kind(*m) {
NodeKind::ServiceFnSig(sig) => {
let mut params = Vec::new();
for p in &sig.params {
params.push(self.resolve_type_node(arena, p.ty));
}
let m_ty = PbsType::Function {
params,
return_type: Box::new(self.resolve_type_node(arena, sig.ret)),
};
methods.insert(self.interner.resolve(sig.name).to_string(), m_ty);
}
let m_ty = PbsType::Function {
params,
return_type: Box::new(self.resolve_type_node(arena, sig.ret)),
};
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);
@ -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
if let Some(sym) = self.module_symbols.value_symbols.get(n.name) {
if let Some(ty) = &sym.ty {
@ -749,7 +779,29 @@ impl<'a> TypeChecker<'a> {
self.struct_constants.insert(type_name, constants_map);
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.current_struct_fields = prev_fields;
self.current_type_context = prev_ctx;
}
}
@ -782,6 +834,20 @@ impl<'a> TypeChecker<'a> {
"string" => PbsType::String,
"void" => PbsType::Void,
"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()),
_ => {
// 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> {
if let Some(sym) = self.module_symbols.type_symbols.get(name) {
return Some(sym);
@ -960,12 +1036,20 @@ mod tests {
Ok(_) => Ok(()),
Err(bundle) => {
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 {
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(", ");
println!("Compilation failed: {}", err_msg);
println!("Compilation failed: {}\n{}", err_msg, arounds.join("\n"));
Err(err_msg)
}
}
@ -1118,7 +1202,7 @@ mod tests {
#[test]
fn test_struct_type_usage() {
let code = "
declare struct Point { x: int, y: int }
declare struct Point(x: int, y: int)
fn foo(p: Point) {}
fn main() {
// 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); }
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());
}
}

View File

@ -41,7 +41,7 @@ pub fn lower_module(
pub fn lower_function(
core_func: &ir_core::Function,
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> {
let mut vm_func = ir_vm::Function {
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()));
}
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 {
type_id: ir_vm::TypeId(ty.0),
slots: *slots
@ -368,14 +368,7 @@ pub fn lower_function(
fn is_gate_type(ty: &ir_core::Type) -> bool {
match ty {
ir_core::Type::Struct(_) |
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,
ir_core::Type::Contract(name) => name.starts_with("Gate<"),
_ => false,
}
}

View File

@ -251,8 +251,8 @@ mod tests {
let module_dir = dir.path().join("math");
fs::create_dir_all(&module_dir).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("Vector.pbs"), "pub declare struct Vector()").unwrap();
fs::write(module_dir.join("Internal.pbs"), "declare struct Hidden()").unwrap();
let mut fm = FileManager::new();
let exports = build_exports(&module_dir, &mut fm).unwrap();

View File

@ -21,8 +21,7 @@ path = "src/bin/prometeuc.rs"
dist = true
include = [
"../../VERSION.txt",
"../../dist-staging/devtools/debugger-protocol",
"../../dist-staging/devtools/typescript-sdk"
"../../dist-staging/devtools/debugger-protocol"
]
[dependencies]

View 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

View File

@ -2,12 +2,7 @@
"kind": "File",
"imports": [],
"decls": [
21,
26,
39,
44,
48,
57,
103
30,
40
]
}

View File

@ -1,176 +1,19 @@
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
0000 PushConst U32(1)
0006 SetLocal U32(0)
000C GetLocal U32(0)
0012 GetLocal U32(0)
0018 SetLocal U32(1)
001E SetLocal U32(0)
0024 GetLocal U32(0)
002A GetLocal U32(1)
0030 Call U32(1)
0036 SetLocal U32(2)
003C Ret
003E GetLocal U32(0)
0044 GetLocal U32(0)
004A GetLocal U32(1)
0050 Add
0052 Mul
0054 GetLocal U32(1)
005A Mul
005C Ret

View File

@ -1,66 +1,18 @@
// CartridgeCanonical.pbs
// Purpose: VM Heartbeat Test (Industrial Baseline)
declare struct Color(raw: bounded)
declare struct Vec2(x: int, y: int)
[
(x: int, y: int): (x, y) as default
(s: int): (s, s) as square
]
[[
BLACK: Color(0b),
WHITE: Color(65535b),
RED: Color(63488b),
GREEN: Color(2016b),
BLUE: Color(31b)
ZERO: square(0)
]]
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;
{
pub fn len(self: this): int {
return x * x + y * y;
}
}
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);
}
let zero = Vec2.ZERO;
let zz = zero.len();
}