diff --git a/crates/prometeu-compiler/src/frontends/pbs/ast.rs b/crates/prometeu-compiler/src/frontends/pbs/ast.rs index 515aa2a9..598b28d1 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/ast.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/ast.rs @@ -42,6 +42,7 @@ pub enum NodeKind { ImportSpec(ImportSpecNodeArena), ServiceDecl(ServiceDeclNodeArena), ServiceFnSig(ServiceFnSigNodeArena), + ServiceFnDecl(ServiceFnDeclNodeArena), FnDecl(FnDeclNodeArena), TypeDecl(TypeDeclNodeArena), TypeBody(TypeBodyNodeArena), @@ -104,6 +105,14 @@ pub struct ServiceFnSigNodeArena { pub ret: NodeId, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ServiceFnDeclNodeArena { + pub name: NameId, + pub params: Vec, + pub ret: NodeId, + pub body: NodeId, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ParamNodeArena { pub span: Span, diff --git a/crates/prometeu-compiler/src/frontends/pbs/contracts.rs b/crates/prometeu-compiler/src/frontends/pbs/contracts.rs index 992fc37f..7d10d98d 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/contracts.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/contracts.rs @@ -213,19 +213,21 @@ impl ContractRegistry { }); mappings.insert("Fs".to_string(), fs); - // Log mappings + // Log mappings (host) let mut log = HashMap::new(); log.insert("write".to_string(), ContractMethod { id: 0x5001, params: vec![PbsType::Int, PbsType::String], - return_type: PbsType::Void, + // Alguns targets retornam status; para segurança, trate como Int para permitir POP automático + return_type: PbsType::Int, }); log.insert("writeTag".to_string(), ContractMethod { id: 0x5002, params: vec![PbsType::Int, PbsType::Int, PbsType::String], - return_type: PbsType::Void, + return_type: PbsType::Int, }); - mappings.insert("Log".to_string(), log); + // O contrato host exposto no SDK é LogHost (não-público), então o registro deve atender por esse nome + mappings.insert("LogHost".to_string(), log); // System mappings let mut system = HashMap::new(); diff --git a/crates/prometeu-compiler/src/frontends/pbs/lowering.rs b/crates/prometeu-compiler/src/frontends/pbs/lowering.rs index 0c34f7e4..1a5cc1a2 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/lowering.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/lowering.rs @@ -45,6 +45,17 @@ pub struct Lowerer<'a> { } impl<'a> Lowerer<'a> { + #[inline] + fn hash_tag_u16(s: &str) -> u16 { + // FNV-1a 16-bit (simple, deterministic, allows small collisions) + let mut hash: u16 = 0x811C; // offset basis (truncated) + let prime: u16 = 0x0101; // 257 + for &b in s.as_bytes() { + hash ^= b as u16; + hash = hash.wrapping_mul(prime); + } + hash + } pub fn new( arena: &'a AstArena, module_symbols: &'a ModuleSymbols, @@ -1123,15 +1134,71 @@ impl<'a> Lowerer<'a> { .get(obj_id.name) .or_else(|| self.imported_symbols.type_symbols.get(obj_id.name)); if let Some(sym) = sym_opt { - if sym.kind == SymbolKind::Contract && sym.is_host { - // Lower arguments first to avoid borrowing conflicts - for arg in &n.args { - self.lower_node(*arg)?; + // 0) Açúcar sintático para Log: permitir quando Log for service/contract (não-host) também + if obj_name == "Log" { + let level_opt: Option = match member_name { + "trace" => Some(0), + "debug" => Some(1), + "info" => Some(2), + "warn" => Some(3), + "error" => Some(4), + _ => None, + }; + + if let Some(level) = level_opt { + if n.args.len() == 1 { + // Log.(msg) + self.emit(InstrKind::PushBounded(level)); + self.lower_node(n.args[0])?; + // Syscall 0x5001 retorna status (1 slot): descartar em ExprStmt + self.emit(InstrKind::HostCall(0x5001, 1)); + self.emit(InstrKind::Pop); + return Ok(()); + } + if n.args.len() == 2 { + if let NodeKind::StringLit(tag_node) = self.arena.kind(n.args[0]) { + let tag_hash = Self::hash_tag_u16(&tag_node.value) as u32; + self.emit(InstrKind::PushBounded(level)); + self.emit(InstrKind::PushBounded(tag_hash)); + self.lower_node(n.args[1])?; + // Syscall 0x5002 retorna status (1 slot): descartar em ExprStmt + self.emit(InstrKind::HostCall(0x5002, 1)); + self.emit(InstrKind::Pop); + return Ok(()); + } else { + self.error( + "E_LOWER_UNSUPPORTED", + format!("Log.{} com tag dinâmica ainda não é suportado; use tag string literal", member_name), + self.arena.span(node), + ); + return Err(()); + } + } + self.error( + "E_LOWER_UNSUPPORTED", + format!("Assinatura inválida para Log.{}", member_name), + self.arena.span(node), + ); + return Err(()); } - if let Some(method) = self.contract_registry.get_method(obj_name, member_name) { - let id = method.id; - // Compute return slots from declared return type. Structs may span >1 slot (e.g., Button=4) - let return_slots = match &method.return_type { + } + + if sym.kind == SymbolKind::Contract && sym.is_host { + // 1) Caminho padrão via ContractRegistry + if self.contract_registry.get_method(obj_name, member_name).is_some() { + // Extrai valores necessários sem manter o empréstimo do registry vivo + let (id, return_ty) = { + let method = self.contract_registry.get_method(obj_name, member_name).unwrap(); + (method.id, method.return_type.clone()) + }; + + // Lower arguments primeiro + for arg in &n.args { + self.lower_node(*arg)?; + } + + // Compute return slots a partir do tipo retornado + let return_slots = match &return_ty { PbsType::Void | PbsType::None => 0, PbsType::Struct(name) => { // Prefer builtin struct slots, then fallback to struct_slots map, default 1 @@ -1142,9 +1209,12 @@ impl<'a> Lowerer<'a> { self.get_type_slots(&ty) } }; + self.emit(InstrKind::HostCall(id, return_slots)); return Ok(()); } + + // (Açúcar de Log movido para antes do branch host) } } } @@ -2246,6 +2316,92 @@ mod tests { assert!(instrs.iter().any(|i| matches!(i.kind, InstrKind::HostCall(0x5001, 0)))); } + #[test] + fn test_log_sugar_lowering_basic() { + let code = " + declare contract Log host {} + fn main() { + Log.info(\"Hello\"); + } + "; + let mut interner = NameInterner::new(); + let mut parser = Parser::new(code, FileId(0), &mut interner); + let parsed = parser.parse_file().expect("Failed to parse"); + + let mut collector = SymbolCollector::new(&interner); + let (type_symbols, value_symbols) = collector + .collect(&parsed.arena, parsed.root) + .expect("Failed to collect symbols"); + let module_symbols = ModuleSymbols { type_symbols, value_symbols }; + + let imported = ModuleSymbols::new(); + let lowerer = Lowerer::new(&parsed.arena, &module_symbols, &imported, &interner); + let program = lowerer.lower_file(parsed.root, "test").expect("Lowering failed"); + + let func = &program.modules[0].functions[0]; + let instrs: Vec<_> = func.blocks.iter().flat_map(|b| b.instrs.iter()).collect(); + + // Deve gerar HostCall para Log.write (0x5001) + assert!(instrs.iter().any(|i| matches!(i.kind, InstrKind::HostCall(0x5001, 0)))); + } + + #[test] + fn test_log_sugar_lowering_with_tag_literal() { + let code = " + declare contract Log host {} + fn main() { + Log.warn(\"net\", \"Down\"); + } + "; + let mut interner = NameInterner::new(); + let mut parser = Parser::new(code, FileId(0), &mut interner); + let parsed = parser.parse_file().expect("Failed to parse"); + + let mut collector = SymbolCollector::new(&interner); + let (type_symbols, value_symbols) = collector + .collect(&parsed.arena, parsed.root) + .expect("Failed to collect symbols"); + let module_symbols = ModuleSymbols { type_symbols, value_symbols }; + + let imported = ModuleSymbols::new(); + let lowerer = Lowerer::new(&parsed.arena, &module_symbols, &imported, &interner); + let program = lowerer.lower_file(parsed.root, "test").expect("Lowering failed"); + + let func = &program.modules[0].functions[0]; + let instrs: Vec<_> = func.blocks.iter().flat_map(|b| b.instrs.iter()).collect(); + + // Deve gerar HostCall para Log.writeTag (0x5002) + assert!(instrs.iter().any(|i| matches!(i.kind, InstrKind::HostCall(0x5002, 0)))); + } + + #[test] + fn test_log_sugar_lowering_with_dynamic_tag_error() { + let code = " + declare contract Log host {} + fn main() { + let t = \"net\"; + Log.debug(t, \"X\"); + } + "; + let mut interner = NameInterner::new(); + let mut parser = Parser::new(code, FileId(0), &mut interner); + let parsed = parser.parse_file().expect("Failed to parse"); + + let mut collector = SymbolCollector::new(&interner); + let (type_symbols, value_symbols) = collector + .collect(&parsed.arena, parsed.root) + .expect("Failed to collect symbols"); + let module_symbols = ModuleSymbols { type_symbols, value_symbols }; + + let imported = ModuleSymbols::new(); + let lowerer = Lowerer::new(&parsed.arena, &module_symbols, &imported, &interner); + let result = lowerer.lower_file(parsed.root, "test"); + + assert!(result.is_err()); + let bundle = result.err().unwrap(); + assert!(bundle.diagnostics.iter().any(|d| d.code == "E_LOWER_UNSUPPORTED")); + } + #[test] fn test_contract_call_without_host_lowering() { let code = " diff --git a/crates/prometeu-compiler/src/frontends/pbs/parser.rs b/crates/prometeu-compiler/src/frontends/pbs/parser.rs index 2932e15c..acd42319 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/parser.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/parser.rs @@ -243,8 +243,14 @@ impl<'a> Parser<'a> { self.consume(TokenKind::OpenBrace)?; let mut members = Vec::new(); while self.peek().kind != TokenKind::CloseBrace && self.peek().kind != TokenKind::Eof { + // Ignora pontos-e-vírgula extras + if self.peek().kind == TokenKind::Semicolon { + self.advance(); + continue; + } + members.push(self.parse_service_member()?); - // Optional semicolon after signature + // Opcional ';' após assinatura if self.peek().kind == TokenKind::Semicolon { self.advance(); } @@ -279,11 +285,21 @@ impl<'a> Parser<'a> { ) }; - let span = Span::new(self.file_id, start_span.start, self.arena.span(ret).end); - Ok(self.arena.push( - NodeKind::ServiceFnSig(ServiceFnSigNodeArena { name, params, ret }), - span, - )) + // Aceita tanto assinatura quanto corpo + if self.peek().kind == TokenKind::OpenBrace { + let body = self.parse_block()?; + let span = Span::new(self.file_id, start_span.start, self.arena.span(body).end); + Ok(self.arena.push( + NodeKind::ServiceFnDecl(ServiceFnDeclNodeArena { name, params, ret, body }), + span, + )) + } else { + let span = Span::new(self.file_id, start_span.start, self.arena.span(ret).end); + Ok(self.arena.push( + NodeKind::ServiceFnSig(ServiceFnSigNodeArena { name, params, ret }), + span, + )) + } } fn parse_type_decl(&mut self, vis: Option) -> Result { diff --git a/crates/prometeu-compiler/src/frontends/pbs/resolve.rs b/crates/prometeu-compiler/src/frontends/pbs/resolve.rs index 176048a9..43e3e3f8 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/resolve.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/resolve.rs @@ -98,6 +98,9 @@ fn walk_node( NodeKind::FnDecl(fn_decl) => { walk_node(fn_decl.body, arena, module, index, imports, ref_index, node_to_symbol, diagnostics); } + NodeKind::ServiceFnDecl(_decl) => { + // Não resolve corpo/locais nesta fase (handled em resolver.rs) + } NodeKind::Block(block) => { for &stmt in &block.stmts { walk_node(stmt, arena, module, index, imports, ref_index, node_to_symbol, diagnostics); diff --git a/crates/prometeu-compiler/src/frontends/pbs/resolver.rs b/crates/prometeu-compiler/src/frontends/pbs/resolver.rs index 000e0d9d..61c4d6ab 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/resolver.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/resolver.rs @@ -458,11 +458,33 @@ impl<'a> Resolver<'a> { self.resolve_identifier(ext, arena.span(id), Namespace::Type); } for member in &n.members { - if let NodeKind::ServiceFnSig(sig) = arena.kind(*member) { - for param in &sig.params { - self.resolve_type_ref(arena, param.ty); + match arena.kind(*member) { + NodeKind::ServiceFnSig(sig) => { + for param in &sig.params { + self.resolve_type_ref(arena, param.ty); + } + self.resolve_type_ref(arena, sig.ret); } - self.resolve_type_ref(arena, sig.ret); + NodeKind::ServiceFnDecl(decl) => { + // service com corpo: resolve tipos, define parâmetros como locais e resolve corpo + for param in &decl.params { + self.resolve_type_ref(arena, param.ty); + } + self.resolve_type_ref(arena, decl.ret); + + self.enter_scope(); + for param in &decl.params { + let sym_id = self.define_local(param.name, param.span.clone(), SymbolKind::Local); + if let Some(sym_id) = sym_id { + if let Some(ty_id) = self.resolve_type_ref(arena, param.ty) { + self.type_facts.set_symbol_type(sym_id, ty_id); + } + } + } + self.resolve_node(arena, decl.body); + self.exit_scope(); + } + _ => {} } } } diff --git a/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs b/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs index a840a163..dc46da62 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs @@ -203,6 +203,46 @@ impl<'a> TypeChecker<'a> { self.check_fn_decl(arena, node, n); PbsType::Void } + NodeKind::ServiceDecl(n) => { + // Verifica membros do service, incluindo funções com corpo + for m in &n.members { + match arena.kind(*m) { + NodeKind::ServiceFnSig(sig) => { + // Registrar tipo da função no contexto do service + let mut params = Vec::new(); + for p in &sig.params { + params.push(self.resolve_type_node(arena, p.ty)); + } + let ret_ty = self.resolve_type_node(arena, sig.ret); + // Não armazenamos tabela de métodos de service ainda; apenas validação superficial + let _ = (params, ret_ty); + } + NodeKind::ServiceFnDecl(decl) => { + // Checagem semelhante a FnDecl: escopo próprio, parâmetros locais e tipo de retorno + let mut params_tys = Vec::new(); + for p in &decl.params { + params_tys.push(self.resolve_type_node(arena, p.ty)); + } + let ret_ty = self.resolve_type_node(arena, decl.ret); + + // Entra em escopo, define parâmetros e checa o corpo + self.enter_scope(); + for (i, p) in decl.params.iter().enumerate() { + let ty = params_tys.get(i).cloned().unwrap_or(PbsType::Void); + self.define_local(self.interner.resolve(p.name), ty, false); + } + let prev_ret = self.current_return_type.clone(); + self.current_return_type = Some(ret_ty.clone()); + // Corpo do método do service é um Block; percorremos seus statements + self.check_node(arena, decl.body); + self.current_return_type = prev_ret; + self.exit_scope(); + } + _ => {} + } + } + PbsType::Void + } NodeKind::TypeDecl(n) => { self.check_type_decl(arena, node, n); PbsType::Void @@ -278,7 +318,8 @@ impl<'a> TypeChecker<'a> { .or_else(|| self.imported_symbols.type_symbols.get(id.name)); if let Some(sym) = sym_opt { - if sym.kind == SymbolKind::Contract && sym.is_host { + // Permitir consulta ao registry para contratos host (e também aceitar LogHost por nome) + if sym.kind == SymbolKind::Contract && (sym.is_host || name_str == "LogHost") { // Check if the method exists in registry if let Some(method) = self.contract_registry.get_method(name_str, member_str) { return PbsType::Function { diff --git a/test-cartridges/canonical/golden/program.pbc b/test-cartridges/canonical/golden/program.pbc index 9e80eb11..a079f217 100644 Binary files a/test-cartridges/canonical/golden/program.pbc and b/test-cartridges/canonical/golden/program.pbc differ diff --git a/test-cartridges/sdk/src/main/modules/log/log.pbs b/test-cartridges/sdk/src/main/modules/log/log.pbs new file mode 100644 index 00000000..e53f1a9d --- /dev/null +++ b/test-cartridges/sdk/src/main/modules/log/log.pbs @@ -0,0 +1,48 @@ +// Injeção pelo hardware +declare contract LogHost host { + fn write(level: int, msg: string): void; + fn writeTag(level: int, tag: int, msg: string): void; +} + +// exposicao do modulo, com sugar sintatico, nada injetado aqui +pub service Log { + fn trace(msg: string): void { + LogHost.write(0, msg); + } + + fn trace(tag: int, msg: string): void { + LogHost.writeTag(0, tag, msg); + } + + fn debug(msg: string): void { + LogHost.write(1, msg); + } + + fn debug(tag: int, msg: string): void { + LogHost.writeTag(1, tag, msg); + } + + fn info(msg: string): void { + LogHost.write(2, msg); + } + + fn info(tag: int, msg: string): void { + LogHost.writeTag(2, tag, msg); + } + + fn warn(msg: string): void { + LogHost.write(3, msg); + } + + fn warn(tag: int, msg: string): void { + LogHost.writeTag(3, tag, msg); + } + + fn err(msg: string): void { + LogHost.write(4, msg); + } + + fn err(tag: int, msg: string): void { + LogHost.writeTag(4, tag, msg); + } +} \ No newline at end of file diff --git a/test-cartridges/test01/cartridge/program.pbc b/test-cartridges/test01/cartridge/program.pbc index fe796d27..7b0cf3d6 100644 Binary files a/test-cartridges/test01/cartridge/program.pbc and b/test-cartridges/test01/cartridge/program.pbc differ diff --git a/test-cartridges/test01/src/main/modules/main.pbs b/test-cartridges/test01/src/main/modules/main.pbs index 25df192d..65f157e2 100644 --- a/test-cartridges/test01/src/main/modules/main.pbs +++ b/test-cartridges/test01/src/main/modules/main.pbs @@ -1,5 +1,6 @@ import { Color, Gfx } from "@sdk:gfx"; import { Pad, Touch } from "@sdk:input"; +import { Log } from "@sdk:log"; declare struct Vec2(x: int, y: int) [ @@ -51,10 +52,14 @@ fn frame(): void { } // 4. Input Snapshot & Nested Member Access - let pad = Pad.a(); - let touch = Touch.finger(); - let is_pressed = pad.down || touch.down; + let pad_a = Pad.a(); + let touch_f = Touch.finger(); + let is_pressed = pad_a.down || touch_f.down; if is_pressed { Gfx.clear(Color.BLUE); } + + if Pad.b().pressed { + Log.debug("B Pressed"); + } }