diff --git a/crates/prometeu-compiler/src/backend/emit_bytecode.rs b/crates/prometeu-compiler/src/backend/emit_bytecode.rs index ee0590f9..1fdc92c2 100644 --- a/crates/prometeu-compiler/src/backend/emit_bytecode.rs +++ b/crates/prometeu-compiler/src/backend/emit_bytecode.rs @@ -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)])); } diff --git a/crates/prometeu-compiler/src/compiler.rs b/crates/prometeu-compiler/src/compiler.rs index b4e20107..a69cc219 100644 --- a/crates/prometeu-compiler/src/compiler.rs +++ b/crates/prometeu-compiler/src/compiler.rs @@ -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) diff --git a/crates/prometeu-compiler/src/frontends/pbs/contracts.rs b/crates/prometeu-compiler/src/frontends/pbs/contracts.rs index 1d543739..8bddd003 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/contracts.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/contracts.rs @@ -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()), }); diff --git a/crates/prometeu-compiler/src/frontends/pbs/lowering.rs b/crates/prometeu-compiler/src/frontends/pbs/lowering.rs index 434f91d3..36540b40 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/lowering.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/lowering.rs @@ -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] diff --git a/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs b/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs index d4598d17..c4c7b6a5 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs @@ -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), diff --git a/crates/prometeu-compiler/src/ir_core/instr.rs b/crates/prometeu-compiler/src/ir_core/instr.rs index 1b6f3596..6cd3e859 100644 --- a/crates/prometeu-compiler/src/ir_core/instr.rs +++ b/crates/prometeu-compiler/src/ir_core/instr.rs @@ -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), diff --git a/crates/prometeu-compiler/src/ir_vm/instr.rs b/crates/prometeu-compiler/src/ir_vm/instr.rs index 9205ea66..b91b5abc 100644 --- a/crates/prometeu-compiler/src/ir_vm/instr.rs +++ b/crates/prometeu-compiler/src/ir_vm/instr.rs @@ -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 }, diff --git a/crates/prometeu-compiler/src/lowering/core_to_vm.rs b/crates/prometeu-compiler/src/lowering/core_to_vm.rs index 3f9ce990..36f01665 100644 --- a/crates/prometeu-compiler/src/lowering/core_to_vm.rs +++ b/crates/prometeu-compiler/src/lowering/core_to_vm.rs @@ -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, }, diff --git a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs index 31482f41..936ac19f 100644 --- a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs +++ b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs @@ -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 --- diff --git a/docs/specs/pbs/files/PRs para Junie Global.md b/docs/specs/pbs/files/PRs para Junie Global.md index 77596171..e69de29b 100644 --- a/docs/specs/pbs/files/PRs para Junie Global.md +++ b/docs/specs/pbs/files/PRs para Junie Global.md @@ -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. diff --git a/docs/specs/pbs/files/PRs para Junie.md b/docs/specs/pbs/files/PRs para Junie.md index 8f70b531..e69de29b 100644 --- a/docs/specs/pbs/files/PRs para Junie.md +++ b/docs/specs/pbs/files/PRs para Junie.md @@ -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). - ---- - diff --git a/test-cartridges/hw_hello/src/main.pbs b/test-cartridges/hw_hello/src/main.pbs new file mode 100644 index 00000000..c422cd81 --- /dev/null +++ b/test-cartridges/hw_hello/src/main.pbs @@ -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); + } +}