This commit is contained in:
Nilton Constantino 2026-01-29 17:56:12 +00:00
parent d216918c79
commit f56353ce9b
No known key found for this signature in database
7 changed files with 239 additions and 71 deletions

View File

@ -1,7 +1,14 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::frontends::pbs::types::PbsType;
pub struct ContractMethod {
pub id: u32,
pub params: Vec<PbsType>,
pub return_type: PbsType,
}
pub struct ContractRegistry { pub struct ContractRegistry {
mappings: HashMap<String, HashMap<String, u32>>, mappings: HashMap<String, HashMap<String, ContractMethod>>,
} }
impl ContractRegistry { impl ContractRegistry {
@ -10,81 +17,233 @@ impl ContractRegistry {
// GFX mappings // GFX mappings
let mut gfx = HashMap::new(); let mut gfx = HashMap::new();
gfx.insert("clear".to_string(), 0x1001); gfx.insert("clear".to_string(), ContractMethod {
gfx.insert("fillRect".to_string(), 0x1002); id: 0x1001,
gfx.insert("drawLine".to_string(), 0x1003); params: vec![PbsType::Int],
gfx.insert("drawCircle".to_string(), 0x1004); return_type: PbsType::Void,
gfx.insert("drawDisc".to_string(), 0x1005); });
gfx.insert("drawSquare".to_string(), 0x1006); gfx.insert("fillRect".to_string(), ContractMethod {
gfx.insert("setSprite".to_string(), 0x1007); id: 0x1002,
gfx.insert("drawText".to_string(), 0x1008); params: vec![PbsType::Int, PbsType::Int, PbsType::Int, PbsType::Int, PbsType::Int],
return_type: PbsType::Void,
});
gfx.insert("drawLine".to_string(), ContractMethod {
id: 0x1003,
params: vec![PbsType::Int, PbsType::Int, PbsType::Int, PbsType::Int, PbsType::Int],
return_type: PbsType::Void,
});
gfx.insert("drawCircle".to_string(), ContractMethod {
id: 0x1004,
params: vec![PbsType::Int, PbsType::Int, PbsType::Int, PbsType::Int],
return_type: PbsType::Void,
});
gfx.insert("drawDisc".to_string(), ContractMethod {
id: 0x1005,
params: vec![PbsType::Int, PbsType::Int, PbsType::Int, PbsType::Int, PbsType::Int],
return_type: PbsType::Void,
});
gfx.insert("drawSquare".to_string(), ContractMethod {
id: 0x1006,
params: vec![PbsType::Int, PbsType::Int, PbsType::Int, PbsType::Int],
return_type: PbsType::Void,
});
gfx.insert("setSprite".to_string(), ContractMethod {
id: 0x1007,
params: vec![PbsType::Int, PbsType::Int, PbsType::Int],
return_type: PbsType::Void,
});
gfx.insert("drawText".to_string(), ContractMethod {
id: 0x1008,
params: vec![PbsType::Int, PbsType::Int, PbsType::String, PbsType::Int],
return_type: PbsType::Void,
});
mappings.insert("Gfx".to_string(), gfx); mappings.insert("Gfx".to_string(), gfx);
// Input mappings // Input mappings
let mut input = HashMap::new(); let mut input = HashMap::new();
input.insert("getPad".to_string(), 0x2001); input.insert("getPad".to_string(), ContractMethod {
input.insert("getPadPressed".to_string(), 0x2002); id: 0x2001,
input.insert("getPadReleased".to_string(), 0x2003); params: vec![PbsType::Int],
input.insert("getPadHold".to_string(), 0x2004); return_type: PbsType::Int,
});
input.insert("getPadPressed".to_string(), ContractMethod {
id: 0x2002,
params: vec![PbsType::Int],
return_type: PbsType::Int,
});
input.insert("getPadReleased".to_string(), ContractMethod {
id: 0x2003,
params: vec![PbsType::Int],
return_type: PbsType::Int,
});
input.insert("getPadHold".to_string(), ContractMethod {
id: 0x2004,
params: vec![PbsType::Int],
return_type: PbsType::Int,
});
mappings.insert("Input".to_string(), input); mappings.insert("Input".to_string(), input);
// Touch mappings // Touch mappings
let mut touch = HashMap::new(); let mut touch = HashMap::new();
touch.insert("getX".to_string(), 0x2101); touch.insert("getX".to_string(), ContractMethod {
touch.insert("getY".to_string(), 0x2102); id: 0x2101,
touch.insert("isDown".to_string(), 0x2103); params: vec![],
touch.insert("isPressed".to_string(), 0x2104); return_type: PbsType::Int,
touch.insert("isReleased".to_string(), 0x2105); });
touch.insert("getHold".to_string(), 0x2106); touch.insert("getY".to_string(), ContractMethod {
id: 0x2102,
params: vec![],
return_type: PbsType::Int,
});
touch.insert("isDown".to_string(), ContractMethod {
id: 0x2103,
params: vec![],
return_type: PbsType::Bool,
});
touch.insert("isPressed".to_string(), ContractMethod {
id: 0x2104,
params: vec![],
return_type: PbsType::Bool,
});
touch.insert("isReleased".to_string(), ContractMethod {
id: 0x2105,
params: vec![],
return_type: PbsType::Bool,
});
touch.insert("getHold".to_string(), ContractMethod {
id: 0x2106,
params: vec![],
return_type: PbsType::Int,
});
mappings.insert("Touch".to_string(), touch); mappings.insert("Touch".to_string(), touch);
// Audio mappings // Audio mappings
let mut audio = HashMap::new(); let mut audio = HashMap::new();
audio.insert("playSample".to_string(), 0x3001); audio.insert("playSample".to_string(), ContractMethod {
audio.insert("play".to_string(), 0x3002); id: 0x3001,
params: vec![PbsType::Int],
return_type: PbsType::Void,
});
audio.insert("play".to_string(), ContractMethod {
id: 0x3002,
params: vec![PbsType::Int],
return_type: PbsType::Void,
});
mappings.insert("Audio".to_string(), audio); mappings.insert("Audio".to_string(), audio);
// FS mappings // FS mappings
let mut fs = HashMap::new(); let mut fs = HashMap::new();
fs.insert("open".to_string(), 0x4001); fs.insert("open".to_string(), ContractMethod {
fs.insert("read".to_string(), 0x4002); id: 0x4001,
fs.insert("write".to_string(), 0x4003); params: vec![PbsType::String, PbsType::String],
fs.insert("close".to_string(), 0x4004); return_type: PbsType::Int,
fs.insert("listDir".to_string(), 0x4005); });
fs.insert("exists".to_string(), 0x4006); fs.insert("read".to_string(), ContractMethod {
fs.insert("delete".to_string(), 0x4007); id: 0x4002,
params: vec![PbsType::Int],
return_type: PbsType::String,
});
fs.insert("write".to_string(), ContractMethod {
id: 0x4003,
params: vec![PbsType::Int, PbsType::String],
return_type: PbsType::Int,
});
fs.insert("close".to_string(), ContractMethod {
id: 0x4004,
params: vec![PbsType::Int],
return_type: PbsType::Void,
});
fs.insert("listDir".to_string(), ContractMethod {
id: 0x4005,
params: vec![PbsType::String],
return_type: PbsType::String,
});
fs.insert("exists".to_string(), ContractMethod {
id: 0x4006,
params: vec![PbsType::String],
return_type: PbsType::Bool,
});
fs.insert("delete".to_string(), ContractMethod {
id: 0x4007,
params: vec![PbsType::String],
return_type: PbsType::Bool,
});
mappings.insert("Fs".to_string(), fs); mappings.insert("Fs".to_string(), fs);
// Log mappings // Log mappings
let mut log = HashMap::new(); let mut log = HashMap::new();
log.insert("write".to_string(), 0x5001); log.insert("write".to_string(), ContractMethod {
log.insert("writeTag".to_string(), 0x5002); id: 0x5001,
params: vec![PbsType::Int, PbsType::String],
return_type: PbsType::Void,
});
log.insert("writeTag".to_string(), ContractMethod {
id: 0x5002,
params: vec![PbsType::Int, PbsType::Int, PbsType::String],
return_type: PbsType::Void,
});
mappings.insert("Log".to_string(), log); mappings.insert("Log".to_string(), log);
// System mappings // System mappings
let mut system = HashMap::new(); let mut system = HashMap::new();
system.insert("hasCart".to_string(), 0x0001); system.insert("hasCart".to_string(), ContractMethod {
system.insert("runCart".to_string(), 0x0002); id: 0x0001,
params: vec![],
return_type: PbsType::Bool,
});
system.insert("runCart".to_string(), ContractMethod {
id: 0x0002,
params: vec![],
return_type: PbsType::Void,
});
mappings.insert("System".to_string(), system); mappings.insert("System".to_string(), system);
// Asset mappings // Asset mappings
let mut asset = HashMap::new(); let mut asset = HashMap::new();
asset.insert("load".to_string(), 0x6001); asset.insert("load".to_string(), ContractMethod {
asset.insert("status".to_string(), 0x6002); id: 0x6001,
asset.insert("commit".to_string(), 0x6003); params: vec![PbsType::String],
asset.insert("cancel".to_string(), 0x6004); return_type: PbsType::Int,
});
asset.insert("status".to_string(), ContractMethod {
id: 0x6002,
params: vec![PbsType::Int],
return_type: PbsType::Int,
});
asset.insert("commit".to_string(), ContractMethod {
id: 0x6003,
params: vec![PbsType::Int],
return_type: PbsType::Void,
});
asset.insert("cancel".to_string(), ContractMethod {
id: 0x6004,
params: vec![PbsType::Int],
return_type: PbsType::Void,
});
mappings.insert("Asset".to_string(), asset); mappings.insert("Asset".to_string(), asset);
// Bank mappings // Bank mappings
let mut bank = HashMap::new(); let mut bank = HashMap::new();
bank.insert("info".to_string(), 0x6101); bank.insert("info".to_string(), ContractMethod {
bank.insert("slotInfo".to_string(), 0x6102); id: 0x6101,
params: vec![PbsType::Int],
return_type: PbsType::String,
});
bank.insert("slotInfo".to_string(), ContractMethod {
id: 0x6102,
params: vec![PbsType::Int, PbsType::Int],
return_type: PbsType::String,
});
mappings.insert("Bank".to_string(), bank); mappings.insert("Bank".to_string(), bank);
Self { mappings } Self { mappings }
} }
pub fn resolve(&self, contract: &str, method: &str) -> Option<u32> { pub fn resolve(&self, contract: &str, method: &str) -> Option<u32> {
self.mappings.get(contract).and_then(|m| m.get(method)).copied() self.mappings.get(contract).and_then(|m| m.get(method)).map(|m| m.id)
}
pub fn get_method(&self, contract: &str, method: &str) -> Option<&ContractMethod> {
self.mappings.get(contract).and_then(|m| m.get(method))
} }
} }

View File

@ -414,7 +414,7 @@ impl<'a> Lowerer<'a> {
if is_host_contract && !is_shadowed { if is_host_contract && !is_shadowed {
if let Some(syscall_id) = self.contract_registry.resolve(&obj_id.name, &ma.member) { if let Some(syscall_id) = self.contract_registry.resolve(&obj_id.name, &ma.member) {
self.emit(Instr::Syscall(syscall_id)); self.emit(Instr::HostCall(syscall_id));
return Ok(()); return Ok(());
} else { } else {
self.error("E_RESOLVE_UNDEFINED", format!("Undefined contract member '{}.{}'", obj_id.name, ma.member), ma.span); self.error("E_RESOLVE_UNDEFINED", format!("Undefined contract member '{}.{}'", obj_id.name, ma.member), ma.span);
@ -771,7 +771,7 @@ mod tests {
declare contract Log host {} declare contract Log host {}
fn main() { fn main() {
Gfx.clear(0); Gfx.clear(0);
Log.write(\"Hello\"); Log.write(2, \"Hello\");
} }
"; ";
let mut parser = Parser::new(code, 0); let mut parser = Parser::new(code, 0);
@ -788,9 +788,9 @@ mod tests {
let instrs: Vec<_> = func.blocks.iter().flat_map(|b| b.instrs.iter()).collect(); let instrs: Vec<_> = func.blocks.iter().flat_map(|b| b.instrs.iter()).collect();
// Gfx.clear -> 0x1001 // Gfx.clear -> 0x1001
assert!(instrs.iter().any(|i| matches!(i, ir_core::Instr::Syscall(0x1001)))); assert!(instrs.iter().any(|i| matches!(i, ir_core::Instr::HostCall(0x1001))));
// Log.write -> 0x5001 // Log.write -> 0x5001
assert!(instrs.iter().any(|i| matches!(i, ir_core::Instr::Syscall(0x5001)))); assert!(instrs.iter().any(|i| matches!(i, ir_core::Instr::HostCall(0x5001))));
} }
#[test] #[test]

View File

@ -148,7 +148,12 @@ impl<'a> TypeChecker<'a> {
if let Some(sym) = self.module_symbols.type_symbols.get(&id.name) { if let Some(sym) = self.module_symbols.type_symbols.get(&id.name) {
if sym.kind == SymbolKind::Contract && sym.is_host { if sym.kind == SymbolKind::Contract && sym.is_host {
// Check if the method exists in registry // Check if the method exists in registry
if self.contract_registry.resolve(&id.name, &n.member).is_none() { if let Some(method) = self.contract_registry.get_method(&id.name, &n.member) {
return PbsType::Function {
params: method.params.clone(),
return_type: Box::new(method.return_type.clone()),
};
} else {
self.diagnostics.push(Diagnostic { self.diagnostics.push(Diagnostic {
level: DiagnosticLevel::Error, level: DiagnosticLevel::Error,
code: Some("E_RESOLVE_UNDEFINED".to_string()), code: Some("E_RESOLVE_UNDEFINED".to_string()),
@ -733,6 +738,32 @@ mod tests {
assert!(res.is_ok()); assert!(res.is_ok());
} }
#[test]
fn test_host_method_arity_mismatch() {
let code = "
declare contract Gfx host {}
fn main() {
Gfx.clear(0, 1);
}
";
let res = check_code(code);
assert!(res.is_err());
assert!(res.unwrap_err().contains("E_TYPE_MISMATCH"));
}
#[test]
fn test_host_method_type_mismatch() {
let code = "
declare contract Gfx host {}
fn main() {
Gfx.clear(\"red\");
}
";
let res = check_code(code);
assert!(res.is_err());
assert!(res.unwrap_err().contains("E_TYPE_MISMATCH"));
}
#[test] #[test]
fn test_void_return_ok() { fn test_void_return_ok() {
let code = "fn main() { return; }"; let code = "fn main() { return; }";

View File

@ -9,7 +9,7 @@ pub enum Instr {
/// Placeholder for function calls. /// Placeholder for function calls.
Call(FunctionId, u32), Call(FunctionId, u32),
/// Host calls (syscalls). /// Host calls (syscalls).
Syscall(u32), HostCall(u32),
/// Variable access. /// Variable access.
GetLocal(u32), GetLocal(u32),
SetLocal(u32), SetLocal(u32),

View File

@ -4,7 +4,7 @@ use crate::ir_vm::module::Module;
pub fn validate_module(_module: &Module) -> Result<(), DiagnosticBundle> { pub fn validate_module(_module: &Module) -> Result<(), DiagnosticBundle> {
// TODO: Implement common IR validations: // TODO: Implement common IR validations:
// - Type checking rules // - Type checking rules
// - Syscall signatures // - HostCall signatures
// - VM invariants // - VM invariants
Ok(()) Ok(())
} }

View File

@ -52,7 +52,7 @@ pub fn lower_function(core_func: &ir_core::Function) -> Result<ir_vm::Function>
func_id: *func_id, func_id: *func_id,
arg_count: *arg_count arg_count: *arg_count
}, },
ir_core::Instr::Syscall(id) => ir_vm::InstrKind::Syscall(*id), ir_core::Instr::HostCall(id) => ir_vm::InstrKind::Syscall(*id),
ir_core::Instr::GetLocal(slot) => ir_vm::InstrKind::GetLocal(*slot), ir_core::Instr::GetLocal(slot) => ir_vm::InstrKind::GetLocal(*slot),
ir_core::Instr::SetLocal(slot) => ir_vm::InstrKind::SetLocal(*slot), ir_core::Instr::SetLocal(slot) => ir_vm::InstrKind::SetLocal(*slot),
ir_core::Instr::Pop => ir_vm::InstrKind::Pop, ir_core::Instr::Pop => ir_vm::InstrKind::Pop,
@ -160,7 +160,7 @@ mod tests {
Block { Block {
id: 1, id: 1,
instrs: vec![ instrs: vec![
Instr::Syscall(42), Instr::HostCall(42),
], ],
terminator: Terminator::Return, terminator: Terminator::Return,
}, },
@ -202,7 +202,7 @@ mod tests {
} }
match &func.body[5].kind { match &func.body[5].kind {
InstrKind::Syscall(id) => assert_eq!(*id, 42), InstrKind::Syscall(id) => assert_eq!(*id, 42),
_ => panic!("Expected Syscall 42"), _ => panic!("Expected HostCall 42"),
} }
match &func.body[6].kind { match &func.body[6].kind {
InstrKind::Ret => (), InstrKind::Ret => (),

View File

@ -1,25 +1,3 @@
# PR-24 — Validate Contract Calls in Frontend (Arity + Types)
### Goal
Move contract validation to compile time.
### Required Changes
* During PBS type checking:
* Validate argument count against contract signature
* Validate argument types
* Lower only validated calls to `HostCall`
### Tests
* Wrong arity → `E_TYPE_MISMATCH`
* Correct call lowers to Core IR `HostCall`
---
# PR-25 — Core IR Invariants Test Suite # PR-25 — Core IR Invariants Test Suite
### Goal ### Goal