This commit is contained in:
Nilton Constantino 2026-01-31 01:33:19 +00:00
parent 180c7e19e0
commit adcb34826c
No known key found for this signature in database
12 changed files with 191 additions and 202 deletions

View File

@ -103,6 +103,9 @@ impl<'a> BytecodeEmitter<'a> {
let mapped_id = mapped_const_ids[id.0 as usize];
asm_instrs.push(Asm::Op(OpCode::PushConst, vec![Operand::U32(mapped_id)]));
}
InstrKind::PushBounded(val) => {
asm_instrs.push(Asm::Op(OpCode::PushBounded, vec![Operand::U32(*val)]));
}
InstrKind::PushBool(v) => {
asm_instrs.push(Asm::Op(OpCode::PushBool, vec![Operand::Bool(*v)]));
}

View File

@ -224,7 +224,7 @@ mod tests {
000C Mul
000E Ret
0010 PushConst U32(2)
0016 Syscall U32(4097)
0016 Syscall U32(4112)
001C PushConst U32(3)
0022 SetLocal U32(0)
0028 GetLocal U32(0)

View File

@ -18,7 +18,7 @@ impl ContractRegistry {
// GFX mappings
let mut gfx = HashMap::new();
gfx.insert("clear".to_string(), ContractMethod {
id: 0x1001,
id: 0x1010,
params: vec![PbsType::Struct("Color".to_string())],
return_type: PbsType::Void,
});
@ -62,12 +62,12 @@ impl ContractRegistry {
// Input mappings
let mut input = HashMap::new();
input.insert("pad".to_string(), ContractMethod {
id: 0x2001,
id: 0x2010,
params: vec![],
return_type: PbsType::Struct("Pad".to_string()),
});
input.insert("touch".to_string(), ContractMethod {
id: 0x2002,
id: 0x2011,
params: vec![],
return_type: PbsType::Struct("Touch".to_string()),
});

View File

@ -2,6 +2,7 @@ use crate::common::diagnostics::{Diagnostic, DiagnosticBundle, DiagnosticLevel};
use crate::frontends::pbs::ast::*;
use crate::frontends::pbs::symbols::*;
use crate::frontends::pbs::contracts::ContractRegistry;
use crate::frontends::pbs::types::PbsType;
use crate::ir_core;
use crate::ir_core::ids::{FieldId, FunctionId, TypeId, ValueId};
use crate::ir_core::{Block, Function, Instr, Module, Param, Program, Terminator, Type};
@ -126,7 +127,7 @@ impl<'a> Lowerer<'a> {
name: param.name.clone(),
ty: ty.clone(),
});
self.local_vars[0].insert(param.name.clone(), i as u32);
self.local_vars[0].insert(param.name.clone(), LocalInfo { slot: i as u32, ty: ty.clone() });
local_types.insert(i as u32, ty);
}
@ -184,8 +185,7 @@ impl<'a> Lowerer<'a> {
Ok(())
}
Node::BoundedLit(n) => {
let id = self.program.const_pool.add_int(n.value as i64);
self.emit(Instr::PushConst(id));
self.emit(Instr::PushBounded(n.value));
Ok(())
}
Node::Ident(n) => self.lower_ident(n),
@ -259,7 +259,7 @@ impl<'a> Lowerer<'a> {
// 2. Preserve gate identity
let gate_slot = self.get_next_local_slot();
self.local_vars.last_mut().unwrap().insert(format!("$gate_{}", gate_slot), gate_slot);
self.local_vars.last_mut().unwrap().insert(format!("$gate_{}", gate_slot), LocalInfo { slot: gate_slot, ty: Type::Int });
self.emit(Instr::SetLocal(gate_slot));
// 3. Begin Operation
@ -269,7 +269,7 @@ impl<'a> Lowerer<'a> {
// 4. Bind view to local
self.local_vars.push(HashMap::new());
let view_slot = self.get_next_local_slot();
self.local_vars.last_mut().unwrap().insert(n.binding.to_string(), view_slot);
self.local_vars.last_mut().unwrap().insert(n.binding.to_string(), LocalInfo { slot: view_slot, ty: Type::Int });
self.emit(Instr::SetLocal(view_slot));
// 5. Body
@ -288,7 +288,7 @@ impl<'a> Lowerer<'a> {
// 2. Preserve gate identity
let gate_slot = self.get_next_local_slot();
self.local_vars.last_mut().unwrap().insert(format!("$gate_{}", gate_slot), gate_slot);
self.local_vars.last_mut().unwrap().insert(format!("$gate_{}", gate_slot), LocalInfo { slot: gate_slot, ty: Type::Int });
self.emit(Instr::SetLocal(gate_slot));
// 3. Begin Operation
@ -298,7 +298,7 @@ impl<'a> Lowerer<'a> {
// 4. Bind view to local
self.local_vars.push(HashMap::new());
let view_slot = self.get_next_local_slot();
self.local_vars.last_mut().unwrap().insert(n.binding.to_string(), view_slot);
self.local_vars.last_mut().unwrap().insert(n.binding.to_string(), LocalInfo { slot: view_slot, ty: Type::Int });
self.emit(Instr::SetLocal(view_slot));
// 5. Body
@ -317,7 +317,7 @@ impl<'a> Lowerer<'a> {
// 2. Preserve gate identity
let gate_slot = self.get_next_local_slot();
self.local_vars.last_mut().unwrap().insert(format!("$gate_{}", gate_slot), gate_slot);
self.local_vars.last_mut().unwrap().insert(format!("$gate_{}", gate_slot), LocalInfo { slot: gate_slot, ty: Type::Int });
self.emit(Instr::SetLocal(gate_slot));
// 3. Begin Operation
@ -327,7 +327,7 @@ impl<'a> Lowerer<'a> {
// 4. Bind view to local
self.local_vars.push(HashMap::new());
let view_slot = self.get_next_local_slot();
self.local_vars.last_mut().unwrap().insert(n.binding.to_string(), view_slot);
self.local_vars.last_mut().unwrap().insert(n.binding.to_string(), LocalInfo { slot: view_slot, ty: Type::Int });
self.emit(Instr::SetLocal(view_slot));
// 5. Body
@ -449,8 +449,7 @@ impl<'a> Lowerer<'a> {
return Ok(());
}
};
let id = self.program.const_pool.add_int(val);
self.emit(Instr::PushConst(id));
self.emit(Instr::PushBounded(val));
return Ok(());
}
}
@ -574,6 +573,12 @@ impl<'a> Lowerer<'a> {
}
}
// Check for .raw()
if ma.member == "raw" {
self.lower_node(&ma.object)?;
return Ok(());
}
// Check for Color.rgb
if ma.member == "rgb" {
if let Node::Ident(obj_id) = &*ma.object {
@ -582,7 +587,7 @@ impl<'a> Lowerer<'a> {
// Try to get literal values for r, g, b
let mut literals = Vec::new();
for arg in &n.args {
if let Node::IntLiteral(lit) = arg {
if let Node::IntLit(lit) = arg {
literals.push(Some(lit.value));
} else {
literals.push(None);
@ -594,8 +599,7 @@ impl<'a> Lowerer<'a> {
let g6 = (g & 0xFF) >> 2;
let b5 = (b & 0xFF) >> 3;
let rgb565 = (r5 << 11) | (g6 << 5) | b5;
let id = self.program.const_pool.add_int(rgb565);
self.emit(Instr::PushConst(id));
self.emit(Instr::PushBounded(rgb565 as u32));
return Ok(());
}
}
@ -615,8 +619,10 @@ impl<'a> Lowerer<'a> {
let is_shadowed = self.find_local(&obj_id.name).is_some();
if is_host_contract && !is_shadowed {
if let Some(syscall_id) = self.contract_registry.resolve(&obj_id.name, &ma.member) {
self.emit(Instr::HostCall(syscall_id));
if let Some(method) = self.contract_registry.get_method(&obj_id.name, &ma.member) {
let ir_ty = self.convert_pbs_type(&method.return_type);
let return_slots = self.get_type_slots(&ir_ty);
self.emit(Instr::HostCall(method.id, return_slots));
return Ok(());
} else {
self.error("E_RESOLVE_UNDEFINED", format!("Undefined contract member '{}.{}'", obj_id.name, ma.member), ma.span);
@ -810,11 +816,37 @@ impl<'a> Lowerer<'a> {
fn get_type_slots(&self, ty: &Type) -> u32 {
match ty {
Type::Void => 0,
Type::Struct(name) => self.struct_slots.get(name).cloned().unwrap_or(1),
Type::Array(_, size) => *size,
_ => 1,
}
}
fn convert_pbs_type(&self, ty: &PbsType) -> Type {
match ty {
PbsType::Int => Type::Int,
PbsType::Float => Type::Float,
PbsType::Bool => Type::Bool,
PbsType::String => Type::String,
PbsType::Void => Type::Void,
PbsType::None => Type::Void,
PbsType::Bounded => Type::Bounded,
PbsType::Optional(inner) => Type::Optional(Box::new(self.convert_pbs_type(inner))),
PbsType::Result(ok, err) => Type::Result(
Box::new(self.convert_pbs_type(ok)),
Box::new(self.convert_pbs_type(err)),
),
PbsType::Struct(name) => Type::Struct(name.clone()),
PbsType::Service(name) => Type::Service(name.clone()),
PbsType::Contract(name) => Type::Contract(name.clone()),
PbsType::ErrorType(name) => Type::ErrorType(name.clone()),
PbsType::Function { params, return_type } => Type::Function {
params: params.iter().map(|p| self.convert_pbs_type(p)).collect(),
return_type: Box::new(self.convert_pbs_type(return_type)),
},
}
}
}
#[cfg(test)]
@ -1013,10 +1045,10 @@ mod tests {
let func = &program.modules[0].functions[0];
let instrs: Vec<_> = func.blocks.iter().flat_map(|b| b.instrs.iter()).collect();
// Gfx.clear -> 0x1001
assert!(instrs.iter().any(|i| matches!(i, ir_core::Instr::HostCall(0x1001))));
// Gfx.clear -> 0x1010
assert!(instrs.iter().any(|i| matches!(i, ir_core::Instr::HostCall(0x1010, 0))));
// Log.write -> 0x5001
assert!(instrs.iter().any(|i| matches!(i, ir_core::Instr::HostCall(0x5001))));
assert!(instrs.iter().any(|i| matches!(i, ir_core::Instr::HostCall(0x5001, 0))));
}
#[test]

View File

@ -614,6 +614,20 @@ impl<'a> TypeChecker<'a> {
if expected == found {
return true;
}
// Color is basically a bounded (u16)
if matches!(expected, PbsType::Struct(s) if s == "Color") && *found == PbsType::Bounded {
return true;
}
if *expected == PbsType::Bounded && matches!(found, PbsType::Struct(s) if s == "Color") {
return true;
}
// Allow int as Color/bounded (for compatibility)
if (matches!(expected, PbsType::Struct(s) if s == "Color") || *expected == PbsType::Bounded) && *found == PbsType::Int {
return true;
}
match (expected, found) {
(PbsType::Optional(_), PbsType::None) => true,
(PbsType::Optional(inner), found) => self.is_assignable(inner, found),

View File

@ -6,10 +6,12 @@ use super::ids::{ConstId, FieldId, FunctionId, TypeId, ValueId};
pub enum Instr {
/// Placeholder for constant loading.
PushConst(ConstId),
/// Push a bounded value (0..0xFFFF).
PushBounded(u32),
/// Placeholder for function calls.
Call(FunctionId, u32),
/// Host calls (syscalls).
HostCall(u32),
/// Host calls (syscalls). (id, return_slots)
HostCall(u32, u32),
/// Variable access.
GetLocal(u32),
SetLocal(u32),

View File

@ -44,6 +44,8 @@ pub enum InstrKind {
/// Pushes a constant from the pool onto the stack.
PushConst(ConstId),
/// Pushes a bounded value (0..0xFFFF) onto the stack.
PushBounded(u32),
/// Pushes a boolean onto the stack.
PushBool(bool),
/// Pushes a `null` value onto the stack.
@ -203,6 +205,7 @@ mod tests {
InstrKind::Nop,
InstrKind::Halt,
InstrKind::PushConst(ConstId(0)),
InstrKind::PushBounded(0),
InstrKind::PushBool(true),
InstrKind::PushNull,
InstrKind::Pop,
@ -261,6 +264,9 @@ mod tests {
{
"PushConst": 0
},
{
"PushBounded": 0
},
{
"PushBool": true
},

View File

@ -92,6 +92,10 @@ pub fn lower_function(
stack_types.push(ty);
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::PushConst(ir_vm::ConstId(id.0)), None));
}
ir_core::Instr::PushBounded(val) => {
stack_types.push(ir_core::Type::Bounded);
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::PushBounded(*val), None));
}
ir_core::Instr::Call(func_id, arg_count) => {
// Pop arguments from type stack
for _ in 0..*arg_count {
@ -106,10 +110,12 @@ pub fn lower_function(
arg_count: *arg_count
}, None));
}
ir_core::Instr::HostCall(id) => {
ir_core::Instr::HostCall(id, slots) => {
// HostCall return types are not easily known without a registry,
// but usually they return Int or Void in v0.
stack_types.push(ir_core::Type::Int);
// but we now pass the number of slots.
for _ in 0..*slots {
stack_types.push(ir_core::Type::Int);
}
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Syscall(*id), None));
}
ir_core::Instr::GetLocal(slot) => {
@ -366,6 +372,7 @@ fn lower_type(ty: &ir_core::Type) -> ir_vm::Type {
ir_core::Type::Float => ir_vm::Type::Float,
ir_core::Type::Bool => ir_vm::Type::Bool,
ir_core::Type::String => ir_vm::Type::String,
ir_core::Type::Bounded => ir_vm::Type::Bounded,
ir_core::Type::Optional(inner) => ir_vm::Type::Array(Box::new(lower_type(inner))),
ir_core::Type::Result(ok, _) => lower_type(ok),
ir_core::Type::Struct(_)
@ -411,7 +418,7 @@ mod tests {
Block {
id: 1,
instrs: vec![
Instr::HostCall(42),
Instr::HostCall(42, 1),
],
terminator: Terminator::Return,
},

View File

@ -710,6 +710,55 @@ mod tests {
_ => panic!("Expected Trap"),
}
}
#[test]
fn test_gfx_clear565_syscall() {
let mut hw = crate::Hardware::new();
let mut os = PrometeuOS::new(None);
let mut stack = Vec::new();
// Success case
let args = vec![Value::Bounded(0xF800)]; // Red
{
let mut ret = HostReturn::new(&mut stack);
os.syscall(Syscall::GfxClear565 as u32, &args, &mut ret, &mut hw).unwrap();
}
assert_eq!(stack.len(), 0); // void return
// OOB case
let args = vec![Value::Bounded(0x10000)];
{
let mut ret = HostReturn::new(&mut stack);
let res = os.syscall(Syscall::GfxClear565 as u32, &args, &mut ret, &mut hw);
assert!(res.is_err());
match res.err().unwrap() {
VmFault::Trap(trap, _) => assert_eq!(trap, prometeu_bytecode::abi::TRAP_OOB),
_ => panic!("Expected Trap OOB"),
}
}
}
#[test]
fn test_input_snapshots_syscalls() {
let mut hw = crate::Hardware::new();
let mut os = PrometeuOS::new(None);
// Pad snapshot
let mut stack = Vec::new();
{
let mut ret = HostReturn::new(&mut stack);
os.syscall(Syscall::InputPadSnapshot as u32, &[], &mut ret, &mut hw).unwrap();
}
assert_eq!(stack.len(), 48);
// Touch snapshot
let mut stack = Vec::new();
{
let mut ret = HostReturn::new(&mut stack);
os.syscall(Syscall::InputTouchSnapshot as u32, &[], &mut ret, &mut hw).unwrap();
}
assert_eq!(stack.len(), 6);
}
}
impl NativeInterface for PrometeuOS {
@ -861,6 +910,17 @@ impl NativeInterface for PrometeuOS {
ret.push_null();
Ok(())
}
// gfx.clear565(color_u16) -> void
Syscall::GfxClear565 => {
let color_val = expect_int(args, 0)? as u32;
if color_val > 0xFFFF {
return Err(VmFault::Trap(prometeu_bytecode::abi::TRAP_OOB, "Color value out of bounds (bounded)".into()));
}
let color = Color::from_raw(color_val as u16);
hw.gfx_mut().clear(color);
// No return value for void
Ok(())
}
// --- Input Syscalls ---
@ -914,6 +974,30 @@ impl NativeInterface for PrometeuOS {
ret.push_int(hw.touch().f.hold_frames as i64);
Ok(())
}
Syscall::InputPadSnapshot => {
let pad = hw.pad();
for btn in [
&pad.up, &pad.down, &pad.left, &pad.right,
&pad.a, &pad.b, &pad.x, &pad.y,
&pad.l, &pad.r, &pad.start, &pad.select,
] {
ret.push_bool(btn.pressed);
ret.push_bool(btn.released);
ret.push_bool(btn.down);
ret.push_int(btn.hold_frames as i64);
}
Ok(())
}
Syscall::InputTouchSnapshot => {
let touch = hw.touch();
ret.push_bool(touch.f.pressed);
ret.push_bool(touch.f.released);
ret.push_bool(touch.f.down);
ret.push_int(touch.f.hold_frames as i64);
ret.push_int(touch.x as i64);
ret.push_int(touch.y as i64);
Ok(())
}
// --- Audio Syscalls ---

View File

@ -1,28 +0,0 @@
> **Status:** Ready to copy/paste to Junie
>
> **Goal:** expose hardware types **1:1** to PBS and VM as **SAFE builtins** (stack/value), *not* HIP.
>
> **Key constraint:** Prometeu does **not** have `u16` as a primitive. Use `bounded` for 16-bit-ish hardware scalars.
>
> **Deliverables (in order):**
>
> 1. VM hostcall ABI supports returning **flattened SAFE structs** (multi-slot).
> 2. PBS prelude defines `Color`, `ButtonState`, `Pad`, `Touch` using `bounded`.
> 3. Lowering emits deterministic syscalls for `Gfx.clear(Color)` and `Input.pad()/touch()`.
> 4. Runtime implements the syscalls and an integration cartridge validates behavior.
>
> **Hard rules (do not break):**
>
> * No heap, no gates, no HIP for these types.
> * No `u16` anywhere in PBS surface.
> * Returned structs are *values*, copied by stack.
> * Every PR must include tests.
> * No renumbering opcodes; append only.
## Notes / Forbidden
* DO NOT introduce `u16` into PBS.
* DO NOT allocate heap for these types.
* DO NOT encode `Pad`/`Touch` as gates.
* DO NOT change unrelated opcodes.
* DO NOT add “convenient” APIs not listed above.

View File

@ -1,145 +0,0 @@
## PR-03 — Lowering: Host Contracts for Gfx/Input using deterministic syscalls
### Goal
Map PBS host contracts to stable syscalls with a deterministic ABI.
### Required host contracts in PBS surface
```pbs
pub declare contract Gfx host
{
fn clear(color: Color): void;
}
pub declare contract Input host
{
fn pad(): Pad;
fn touch(): Touch;
}
```
### Required lowering rules
1. `Gfx.clear(color)`
* Emit `SYSCALL_GFX_CLEAR`
* ABI: args = [Color.raw] as `bounded`
* returns: void
2. `Input.pad()`
* Emit `SYSCALL_INPUT_PAD`
* args: none
* returns: flattened `Pad` in field order as declared
3. `Input.touch()`
* Emit `SYSCALL_INPUT_TOUCH`
* args: none
* returns: flattened `Touch` in field order as declared
### Flattening order (binding)
**ButtonState** returns 4 slots in order:
1. pressed (bool)
2. released (bool)
3. down (bool)
4. hold_frames (bounded)
**Pad** returns 12 ButtonState blocks in this exact order:
`up, down, left, right, a, b, x, y, l, r, start, select`
**Touch** returns:
1. f (ButtonState block)
2. x (int)
3. y (int)
### Tests (mandatory)
* Lowering golden test: `Gfx.clear(Color.WHITE)` emits `SYSCALL_GFX_CLEAR` with 1 arg.
* Lowering golden test: `Input.pad()` emits `SYSCALL_INPUT_PAD` and assigns to local.
* Lowering golden test: `Input.touch()` emits `SYSCALL_INPUT_TOUCH`.
### Non-goals
* No runtime changes
* No VM heap
---
## PR-04 — Runtime: Implement syscalls for Color/Gfx and Input pad/touch + integration cartridge
### Goal
Make the new syscalls actually work and prove them with an integration test cartridge.
### Required syscall implementations
#### 1) `SYSCALL_GFX_CLEAR`
* Read 1 arg: `bounded` raw color
* Convert to `u16` internally (runtime-only)
* If raw > 0xFFFF, trap `TRAP_OOB` or `TRAP_TYPE` (choose one and document)
* Fill framebuffer with that RGB565 value
#### 2) `SYSCALL_INPUT_PAD`
* No args
* Snapshot the current runtime `Pad` and push a flattened `Pad` return:
* For each button: pressed, released, down, hold_frames
* hold_frames pushed as `bounded`
#### 3) `SYSCALL_INPUT_TOUCH`
* No args
* Snapshot `Touch` and push flattened `Touch` return:
* f ButtonState
* x int
* y int
### Integration cartridge (mandatory)
Add `test-cartridges/hw_hello` (or similar) with:
```pbs
fn frame(): void
{
// 1) clear screen white
Gfx.clear(Color.WHITE);
// 2) read pad and branch
let p: Pad = Input.pad();
if p.any() {
Gfx.clear(Color.MAGENTA);
}
// 3) read touch and branch on f.down
let t: Touch = Input.touch();
if t.f.down {
// choose a third color to prove the struct returned correctly
Gfx.clear(Color.BLUE);
}
}
```
### Acceptance criteria
* Cartridge runs without VM faults.
* With no input: screen is WHITE.
* With any pad button held: screen becomes MAGENTA.
* With touch f.down: screen becomes BLUE.
### Tests (mandatory)
* Runtime unit test: `SYSCALL_GFX_CLEAR` rejects raw > 0xFFFF deterministically.
* Runtime unit test: `SYSCALL_INPUT_PAD` returns correct number of stack slots (48).
* Runtime unit test: `SYSCALL_INPUT_TOUCH` returns correct number of stack slots (4 + 2 = 6).
---

View File

@ -0,0 +1,14 @@
fn frame(): void
{
Gfx.clear(Color.WHITE);
let p: Pad = Input.pad();
if p.any() {
Gfx.clear(Color.MAGENTA);
}
let t: Touch = Input.touch();
if t.f.down {
Gfx.clear(Color.BLUE);
}
}