fix structs
This commit is contained in:
parent
5cf77359fb
commit
d994005f8b
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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#"
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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]
|
||||
|
||||
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",
|
||||
"imports": [],
|
||||
"decls": [
|
||||
21,
|
||||
26,
|
||||
39,
|
||||
44,
|
||||
48,
|
||||
57,
|
||||
103
|
||||
30,
|
||||
40
|
||||
]
|
||||
}
|
||||
@ -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
|
||||
|
||||
Binary file not shown.
@ -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();
|
||||
}
|
||||
|
||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user