dev/pbs #8

Merged
bquarkz merged 74 commits from dev/pbs into master 2026-02-03 15:28:31 +00:00
12 changed files with 191 additions and 202 deletions
Showing only changes of commit adcb34826c - Show all commits

View File

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

View File

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

View File

@ -18,7 +18,7 @@ impl ContractRegistry {
// GFX mappings // GFX mappings
let mut gfx = HashMap::new(); let mut gfx = HashMap::new();
gfx.insert("clear".to_string(), ContractMethod { gfx.insert("clear".to_string(), ContractMethod {
id: 0x1001, id: 0x1010,
params: vec![PbsType::Struct("Color".to_string())], params: vec![PbsType::Struct("Color".to_string())],
return_type: PbsType::Void, return_type: PbsType::Void,
}); });
@ -62,12 +62,12 @@ impl ContractRegistry {
// Input mappings // Input mappings
let mut input = HashMap::new(); let mut input = HashMap::new();
input.insert("pad".to_string(), ContractMethod { input.insert("pad".to_string(), ContractMethod {
id: 0x2001, id: 0x2010,
params: vec![], params: vec![],
return_type: PbsType::Struct("Pad".to_string()), return_type: PbsType::Struct("Pad".to_string()),
}); });
input.insert("touch".to_string(), ContractMethod { input.insert("touch".to_string(), ContractMethod {
id: 0x2002, id: 0x2011,
params: vec![], params: vec![],
return_type: PbsType::Struct("Touch".to_string()), 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::ast::*;
use crate::frontends::pbs::symbols::*; use crate::frontends::pbs::symbols::*;
use crate::frontends::pbs::contracts::ContractRegistry; use crate::frontends::pbs::contracts::ContractRegistry;
use crate::frontends::pbs::types::PbsType;
use crate::ir_core; use crate::ir_core;
use crate::ir_core::ids::{FieldId, FunctionId, TypeId, ValueId}; use crate::ir_core::ids::{FieldId, FunctionId, TypeId, ValueId};
use crate::ir_core::{Block, Function, Instr, Module, Param, Program, Terminator, Type}; use crate::ir_core::{Block, Function, Instr, Module, Param, Program, Terminator, Type};
@ -126,7 +127,7 @@ impl<'a> Lowerer<'a> {
name: param.name.clone(), name: param.name.clone(),
ty: ty.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); local_types.insert(i as u32, ty);
} }
@ -184,8 +185,7 @@ impl<'a> Lowerer<'a> {
Ok(()) Ok(())
} }
Node::BoundedLit(n) => { Node::BoundedLit(n) => {
let id = self.program.const_pool.add_int(n.value as i64); self.emit(Instr::PushBounded(n.value));
self.emit(Instr::PushConst(id));
Ok(()) Ok(())
} }
Node::Ident(n) => self.lower_ident(n), Node::Ident(n) => self.lower_ident(n),
@ -259,7 +259,7 @@ impl<'a> Lowerer<'a> {
// 2. Preserve gate identity // 2. Preserve gate identity
let gate_slot = self.get_next_local_slot(); 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)); self.emit(Instr::SetLocal(gate_slot));
// 3. Begin Operation // 3. Begin Operation
@ -269,7 +269,7 @@ impl<'a> Lowerer<'a> {
// 4. Bind view to local // 4. Bind view to local
self.local_vars.push(HashMap::new()); self.local_vars.push(HashMap::new());
let view_slot = self.get_next_local_slot(); 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)); self.emit(Instr::SetLocal(view_slot));
// 5. Body // 5. Body
@ -288,7 +288,7 @@ impl<'a> Lowerer<'a> {
// 2. Preserve gate identity // 2. Preserve gate identity
let gate_slot = self.get_next_local_slot(); 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)); self.emit(Instr::SetLocal(gate_slot));
// 3. Begin Operation // 3. Begin Operation
@ -298,7 +298,7 @@ impl<'a> Lowerer<'a> {
// 4. Bind view to local // 4. Bind view to local
self.local_vars.push(HashMap::new()); self.local_vars.push(HashMap::new());
let view_slot = self.get_next_local_slot(); 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)); self.emit(Instr::SetLocal(view_slot));
// 5. Body // 5. Body
@ -317,7 +317,7 @@ impl<'a> Lowerer<'a> {
// 2. Preserve gate identity // 2. Preserve gate identity
let gate_slot = self.get_next_local_slot(); 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)); self.emit(Instr::SetLocal(gate_slot));
// 3. Begin Operation // 3. Begin Operation
@ -327,7 +327,7 @@ impl<'a> Lowerer<'a> {
// 4. Bind view to local // 4. Bind view to local
self.local_vars.push(HashMap::new()); self.local_vars.push(HashMap::new());
let view_slot = self.get_next_local_slot(); 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)); self.emit(Instr::SetLocal(view_slot));
// 5. Body // 5. Body
@ -449,8 +449,7 @@ impl<'a> Lowerer<'a> {
return Ok(()); return Ok(());
} }
}; };
let id = self.program.const_pool.add_int(val); self.emit(Instr::PushBounded(val));
self.emit(Instr::PushConst(id));
return Ok(()); 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 // Check for Color.rgb
if ma.member == "rgb" { if ma.member == "rgb" {
if let Node::Ident(obj_id) = &*ma.object { 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 // Try to get literal values for r, g, b
let mut literals = Vec::new(); let mut literals = Vec::new();
for arg in &n.args { for arg in &n.args {
if let Node::IntLiteral(lit) = arg { if let Node::IntLit(lit) = arg {
literals.push(Some(lit.value)); literals.push(Some(lit.value));
} else { } else {
literals.push(None); literals.push(None);
@ -594,8 +599,7 @@ impl<'a> Lowerer<'a> {
let g6 = (g & 0xFF) >> 2; let g6 = (g & 0xFF) >> 2;
let b5 = (b & 0xFF) >> 3; let b5 = (b & 0xFF) >> 3;
let rgb565 = (r5 << 11) | (g6 << 5) | b5; let rgb565 = (r5 << 11) | (g6 << 5) | b5;
let id = self.program.const_pool.add_int(rgb565); self.emit(Instr::PushBounded(rgb565 as u32));
self.emit(Instr::PushConst(id));
return Ok(()); return Ok(());
} }
} }
@ -615,8 +619,10 @@ impl<'a> Lowerer<'a> {
let is_shadowed = self.find_local(&obj_id.name).is_some(); let is_shadowed = self.find_local(&obj_id.name).is_some();
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(method) = self.contract_registry.get_method(&obj_id.name, &ma.member) {
self.emit(Instr::HostCall(syscall_id)); 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(()); 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);
@ -810,11 +816,37 @@ impl<'a> Lowerer<'a> {
fn get_type_slots(&self, ty: &Type) -> u32 { fn get_type_slots(&self, ty: &Type) -> u32 {
match ty { match ty {
Type::Void => 0,
Type::Struct(name) => self.struct_slots.get(name).cloned().unwrap_or(1), Type::Struct(name) => self.struct_slots.get(name).cloned().unwrap_or(1),
Type::Array(_, size) => *size, Type::Array(_, size) => *size,
_ => 1, _ => 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)] #[cfg(test)]
@ -1013,10 +1045,10 @@ mod tests {
let func = &program.modules[0].functions[0]; let func = &program.modules[0].functions[0];
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 -> 0x1010
assert!(instrs.iter().any(|i| matches!(i, ir_core::Instr::HostCall(0x1001)))); assert!(instrs.iter().any(|i| matches!(i, ir_core::Instr::HostCall(0x1010, 0))));
// Log.write -> 0x5001 // 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] #[test]

View File

@ -614,6 +614,20 @@ impl<'a> TypeChecker<'a> {
if expected == found { if expected == found {
return true; 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) { match (expected, found) {
(PbsType::Optional(_), PbsType::None) => true, (PbsType::Optional(_), PbsType::None) => true,
(PbsType::Optional(inner), found) => self.is_assignable(inner, found), (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 { pub enum Instr {
/// Placeholder for constant loading. /// Placeholder for constant loading.
PushConst(ConstId), PushConst(ConstId),
/// Push a bounded value (0..0xFFFF).
PushBounded(u32),
/// Placeholder for function calls. /// Placeholder for function calls.
Call(FunctionId, u32), Call(FunctionId, u32),
/// Host calls (syscalls). /// Host calls (syscalls). (id, return_slots)
HostCall(u32), HostCall(u32, u32),
/// Variable access. /// Variable access.
GetLocal(u32), GetLocal(u32),
SetLocal(u32), SetLocal(u32),

View File

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

View File

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

View File

@ -710,6 +710,55 @@ mod tests {
_ => panic!("Expected Trap"), _ => 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 { impl NativeInterface for PrometeuOS {
@ -861,6 +910,17 @@ impl NativeInterface for PrometeuOS {
ret.push_null(); ret.push_null();
Ok(()) 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 --- // --- Input Syscalls ---
@ -914,6 +974,30 @@ impl NativeInterface for PrometeuOS {
ret.push_int(hw.touch().f.hold_frames as i64); ret.push_int(hw.touch().f.hold_frames as i64);
Ok(()) 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 --- // --- 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);
}
}