pr 40
This commit is contained in:
parent
180c7e19e0
commit
adcb34826c
@ -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)]));
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()),
|
||||
});
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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
|
||||
},
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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 ---
|
||||
|
||||
|
||||
@ -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.
|
||||
@ -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).
|
||||
|
||||
---
|
||||
|
||||
14
test-cartridges/hw_hello/src/main.pbs
Normal file
14
test-cartridges/hw_hello/src/main.pbs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user