From 57fc54c6c30a980cf5c9742284755da6f2fb86c0 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Tue, 30 Dec 2025 20:04:33 -0800 Subject: [PATCH 01/30] Add break and continue statements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements break and continue for while loops: - Lexer: Break/Continue tokens - Parser: parse_break/parse_continue functions - AST/HIR: BreakStmt/ContinueStmt nodes - Type checker: validates break/continue are inside loops - Codegen: LoopTarget struct tracks header/exit blocks for jumps Includes positive and negative test cases. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- capc/src/ast.rs | 14 +++++++ capc/src/codegen/emit.rs | 33 ++++++++++++++- capc/src/codegen/mod.rs | 1 + capc/src/hir.rs | 14 +++++++ capc/src/lexer.rs | 4 ++ capc/src/parser.rs | 22 ++++++++++ capc/src/typeck/check.rs | 35 ++++++++++++++++ capc/src/typeck/lower.rs | 20 ++++++--- capc/src/typeck/monomorphize.rs | 6 +++ capc/tests/run.rs | 42 +++++++++++++++++++ capc/tests/typecheck.rs | 27 ++++++++++++ tests/programs/break_basic.cap | 16 +++++++ tests/programs/break_nested.cap | 28 +++++++++++++ tests/programs/continue_basic.cap | 23 ++++++++++ .../should_fail_break_in_function.cap | 18 ++++++++ .../should_fail_break_outside_loop.cap | 11 +++++ .../should_fail_continue_outside_loop.cap | 11 +++++ 17 files changed, 319 insertions(+), 6 deletions(-) create mode 100644 tests/programs/break_basic.cap create mode 100644 tests/programs/break_nested.cap create mode 100644 tests/programs/continue_basic.cap create mode 100644 tests/programs/should_fail_break_in_function.cap create mode 100644 tests/programs/should_fail_break_outside_loop.cap create mode 100644 tests/programs/should_fail_continue_outside_loop.cap diff --git a/capc/src/ast.rs b/capc/src/ast.rs index 05bd6f1..7a27f44 100644 --- a/capc/src/ast.rs +++ b/capc/src/ast.rs @@ -142,6 +142,8 @@ pub enum Stmt { Let(LetStmt), Assign(AssignStmt), Return(ReturnStmt), + Break(BreakStmt), + Continue(ContinueStmt), If(IfStmt), While(WhileStmt), Expr(ExprStmt), @@ -161,6 +163,16 @@ pub struct ReturnStmt { pub span: Span, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BreakStmt { + pub span: Span, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ContinueStmt { + pub span: Span, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct IfStmt { pub cond: Expr, @@ -195,6 +207,8 @@ impl Stmt { Stmt::Let(s) => s.span, Stmt::Assign(s) => s.span, Stmt::Return(s) => s.span, + Stmt::Break(s) => s.span, + Stmt::Continue(s) => s.span, Stmt::If(s) => s.span, Stmt::While(s) => s.span, Stmt::Expr(s) => s.span, diff --git a/capc/src/codegen/emit.rs b/capc/src/codegen/emit.rs index 8b41f9d..7f177ea 100644 --- a/capc/src/codegen/emit.rs +++ b/capc/src/codegen/emit.rs @@ -32,6 +32,12 @@ struct ResultStringSlots { err_align: u32, } +/// Target blocks for break/continue inside a loop. +#[derive(Copy, Clone, Debug)] +pub(super) struct LoopTarget { + pub header_block: ir::Block, + pub exit_block: ir::Block, +} /// Emit a single HIR statement. pub(super) fn emit_hir_stmt( @@ -43,6 +49,7 @@ pub(super) fn emit_hir_stmt( struct_layouts: &StructLayoutIndex, module: &mut ObjectModule, data_counter: &mut u32, + loop_target: Option, ) -> Result { emit_hir_stmt_inner( builder, @@ -53,6 +60,7 @@ pub(super) fn emit_hir_stmt( struct_layouts, module, data_counter, + loop_target, ) .map_err(|err| err.with_span(stmt.span())) } @@ -66,6 +74,7 @@ fn emit_hir_stmt_inner( struct_layouts: &StructLayoutIndex, module: &mut ObjectModule, data_counter: &mut u32, + loop_target: Option, ) -> Result { use crate::hir::HirStmt; @@ -250,6 +259,7 @@ fn emit_hir_stmt_inner( struct_layouts, module, data_counter, + loop_target, )?; if flow == Flow::Terminated { then_terminated = true; @@ -276,6 +286,7 @@ fn emit_hir_stmt_inner( struct_layouts, module, data_counter, + loop_target, )?; if flow == Flow::Terminated { else_terminated = true; @@ -322,6 +333,12 @@ fn emit_hir_stmt_inner( builder.switch_to_block(body_block); + // Create loop target for break/continue + let body_loop_target = Some(LoopTarget { + header_block, + exit_block, + }); + // Loop body gets its own locals let mut body_locals = saved_locals.clone(); let mut body_terminated = false; @@ -335,6 +352,7 @@ fn emit_hir_stmt_inner( struct_layouts, module, data_counter, + body_loop_target, )?; if flow == Flow::Terminated { body_terminated = true; @@ -355,6 +373,16 @@ fn emit_hir_stmt_inner( // After the loop, restore the pre-loop locals snapshot *locals = saved_locals; } + HirStmt::Break(_) => { + let target = loop_target.expect("break outside of loop (should be caught by typeck)"); + builder.ins().jump(target.exit_block, &[]); + return Ok(Flow::Terminated); + } + HirStmt::Continue(_) => { + let target = loop_target.expect("continue outside of loop (should be caught by typeck)"); + builder.ins().jump(target.header_block, &[]); + return Ok(Flow::Terminated); + } } Ok(Flow::Continues) } @@ -897,6 +925,7 @@ fn emit_hir_expr_inner( struct_layouts, module, data_counter, + None, // break/continue not supported in expression-context matches ) } else { emit_hir_match_expr( @@ -1621,7 +1650,6 @@ fn emit_hir_short_circuit_expr( Ok(ValueRepr::Single(param)) } -/// Emit HIR match as statement (arms can contain returns, don't produce values) /// Emit HIR match as statement (arms can contain returns, don't produce values). fn emit_hir_match_stmt( builder: &mut FunctionBuilder, @@ -1632,6 +1660,7 @@ fn emit_hir_match_stmt( struct_layouts: &StructLayoutIndex, module: &mut ObjectModule, data_counter: &mut u32, + loop_target: Option, ) -> Result { // Emit the scrutinee expression let value = emit_hir_expr( @@ -1714,6 +1743,7 @@ fn emit_hir_match_stmt( struct_layouts, module, data_counter, + loop_target, )?; if flow == Flow::Terminated { arm_terminated = true; @@ -1832,6 +1862,7 @@ fn emit_hir_match_expr( struct_layouts, module, data_counter, + None, // break/continue not allowed in value-producing match )?; if flow == Flow::Terminated { prefix_terminated = true; diff --git a/capc/src/codegen/mod.rs b/capc/src/codegen/mod.rs index 31c16ad..4b796b4 100644 --- a/capc/src/codegen/mod.rs +++ b/capc/src/codegen/mod.rs @@ -336,6 +336,7 @@ pub fn build_object( &struct_layouts, &mut module, &mut data_counter, + None, // no loop context at function top level )?; if flow == Flow::Terminated { terminated = true; diff --git a/capc/src/hir.rs b/capc/src/hir.rs index 98759e4..33fd4cb 100644 --- a/capc/src/hir.rs +++ b/capc/src/hir.rs @@ -107,6 +107,8 @@ pub enum HirStmt { Let(HirLetStmt), Assign(HirAssignStmt), Return(HirReturnStmt), + Break(HirBreakStmt), + Continue(HirContinueStmt), If(HirIfStmt), While(HirWhileStmt), Expr(HirExprStmt), @@ -118,6 +120,8 @@ impl HirStmt { HirStmt::Let(s) => s.span, HirStmt::Assign(s) => s.span, HirStmt::Return(s) => s.span, + HirStmt::Break(s) => s.span, + HirStmt::Continue(s) => s.span, HirStmt::If(s) => s.span, HirStmt::While(s) => s.span, HirStmt::Expr(s) => s.span, @@ -146,6 +150,16 @@ pub struct HirReturnStmt { pub span: Span, } +#[derive(Debug, Clone)] +pub struct HirBreakStmt { + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct HirContinueStmt { + pub span: Span, +} + #[derive(Debug, Clone)] pub struct HirIfStmt { pub cond: HirExpr, diff --git a/capc/src/lexer.rs b/capc/src/lexer.rs index 3121d8c..c663d60 100644 --- a/capc/src/lexer.rs +++ b/capc/src/lexer.rs @@ -39,6 +39,10 @@ pub enum TokenKind { Else, #[token("while")] While, + #[token("break")] + Break, + #[token("continue")] + Continue, #[token("return")] Return, #[token("struct")] diff --git a/capc/src/parser.rs b/capc/src/parser.rs index 76c9514..35090db 100644 --- a/capc/src/parser.rs +++ b/capc/src/parser.rs @@ -466,6 +466,8 @@ impl Parser { match self.peek_kind() { Some(TokenKind::Let) => Ok(Stmt::Let(self.parse_let()?)), Some(TokenKind::Return) => Ok(Stmt::Return(self.parse_return()?)), + Some(TokenKind::Break) => Ok(Stmt::Break(self.parse_break()?)), + Some(TokenKind::Continue) => Ok(Stmt::Continue(self.parse_continue()?)), Some(TokenKind::If) => Ok(Stmt::If(self.parse_if()?)), Some(TokenKind::While) => Ok(Stmt::While(self.parse_while()?)), Some(TokenKind::Ident) => { @@ -529,6 +531,26 @@ impl Parser { }) } + fn parse_break(&mut self) -> Result { + let token = self.expect(TokenKind::Break)?; + let end = self + .maybe_consume(TokenKind::Semi) + .map_or(token.span.end, |t| t.span.end); + Ok(BreakStmt { + span: Span::new(token.span.start, end), + }) + } + + fn parse_continue(&mut self) -> Result { + let token = self.expect(TokenKind::Continue)?; + let end = self + .maybe_consume(TokenKind::Semi) + .map_or(token.span.end, |t| t.span.end); + Ok(ContinueStmt { + span: Span::new(token.span.start, end), + }) + } + fn parse_if(&mut self) -> Result { let start = self.expect(TokenKind::If)?.span.start; let cond = self.parse_expr()?; diff --git a/capc/src/typeck/check.rs b/capc/src/typeck/check.rs index ccd62e9..443032a 100644 --- a/capc/src/typeck/check.rs +++ b/capc/src/typeck/check.rs @@ -148,6 +148,8 @@ fn block_contains_ptr(block: &Block) -> Option { } } Stmt::Assign(_) => {} + Stmt::Break(_) => {} + Stmt::Continue(_) => {} Stmt::If(if_stmt) => { if let Some(span) = block_contains_ptr(&if_stmt.then_block) { return Some(span); @@ -280,6 +282,7 @@ pub(super) fn check_function( stdlib, module_name, &type_params, + false, // not inside a loop at function top level )?; } @@ -317,6 +320,7 @@ fn check_stmt( stdlib: &StdlibIndex, module_name: &str, type_params: &HashSet, + in_loop: bool, ) -> Result<(), TypeError> { match stmt { Stmt::Let(let_stmt) => { @@ -481,6 +485,24 @@ fn check_stmt( } ensure_linear_all_consumed(scopes, struct_map, enum_map, ret_stmt.span)?; } + Stmt::Break(break_stmt) => { + if !in_loop { + return Err(TypeError::new( + "break statement outside of loop".to_string(), + break_stmt.span, + )); + } + ensure_linear_all_consumed(scopes, struct_map, enum_map, break_stmt.span)?; + } + Stmt::Continue(continue_stmt) => { + if !in_loop { + return Err(TypeError::new( + "continue statement outside of loop".to_string(), + continue_stmt.span, + )); + } + ensure_linear_all_consumed(scopes, struct_map, enum_map, continue_stmt.span)?; + } Stmt::If(if_stmt) => { let cond_ty = check_expr( &if_stmt.cond, @@ -515,6 +537,7 @@ fn check_stmt( stdlib, module_name, type_params, + in_loop, )?; let mut else_scopes = scopes.clone(); if let Some(block) = &if_stmt.else_block { @@ -530,6 +553,7 @@ fn check_stmt( stdlib, module_name, type_params, + in_loop, )?; } merge_branch_states( @@ -575,6 +599,7 @@ fn check_stmt( stdlib, module_name, type_params, + true, // inside loop, break/continue allowed )?; ensure_affine_states_match( scopes, @@ -599,6 +624,7 @@ fn check_stmt( ret_ty, module_name, type_params, + in_loop, )?; } else { check_expr( @@ -635,6 +661,7 @@ fn check_block( stdlib: &StdlibIndex, module_name: &str, type_params: &HashSet, + in_loop: bool, ) -> Result<(), TypeError> { scopes.push_scope(); for stmt in &block.stmts { @@ -650,6 +677,7 @@ fn check_block( stdlib, module_name, type_params, + in_loop, )?; } ensure_linear_scope_consumed(scopes, struct_map, enum_map, block.span)?; @@ -1598,6 +1626,7 @@ pub(super) fn check_expr( ret_ty, module_name, type_params, + false, // break/continue not allowed in value-producing match ), Expr::Try(try_expr) => { let inner_ty = check_expr( @@ -1781,6 +1810,7 @@ fn check_match_stmt( ret_ty: &Ty, module_name: &str, type_params: &HashSet, + in_loop: bool, ) -> Result { let match_ty = check_expr( &match_expr.expr, @@ -1813,6 +1843,7 @@ fn check_match_stmt( stdlib, module_name, type_params, + in_loop, )?; arm_scope.pop_scope(); arm_scopes.push(arm_scope); @@ -1843,6 +1874,7 @@ fn check_match_expr_value( ret_ty: &Ty, module_name: &str, type_params: &HashSet, + in_loop: bool, ) -> Result { let match_ty = check_expr( &match_expr.expr, @@ -1876,6 +1908,7 @@ fn check_match_expr_value( ret_ty, module_name, type_params, + in_loop, )?; arm_scope.pop_scope(); arm_scopes.push(arm_scope); @@ -1915,6 +1948,7 @@ fn check_match_arm_value( ret_ty: &Ty, module_name: &str, type_params: &HashSet, + in_loop: bool, ) -> Result { let Some((last, prefix)) = block.stmts.split_last() else { return Err(TypeError::new( @@ -1941,6 +1975,7 @@ fn check_match_arm_value( stdlib, module_name, type_params, + in_loop, )?; } match last { diff --git a/capc/src/typeck/lower.rs b/capc/src/typeck/lower.rs index 7b40847..9b6a78e 100644 --- a/capc/src/typeck/lower.rs +++ b/capc/src/typeck/lower.rs @@ -4,11 +4,11 @@ use crate::ast::*; use crate::error::TypeError; use crate::abi::AbiType; use crate::hir::{ - HirAssignStmt, HirBinary, HirBlock, HirCall, HirEnum, HirEnumVariant, HirEnumVariantExpr, - HirExpr, HirExprStmt, HirExternFunction, HirField, HirFieldAccess, HirFunction, HirIfStmt, - HirLetStmt, HirLiteral, HirLocal, HirMatch, HirMatchArm, HirParam, HirPattern, HirReturnStmt, - HirStmt, HirStruct, HirStructLiteral, HirStructLiteralField, HirType, HirUnary, HirWhileStmt, - IntrinsicId, LocalId, ResolvedCallee, + HirAssignStmt, HirBinary, HirBlock, HirBreakStmt, HirCall, HirContinueStmt, HirEnum, + HirEnumVariant, HirEnumVariantExpr, HirExpr, HirExprStmt, HirExternFunction, HirField, + HirFieldAccess, HirFunction, HirIfStmt, HirLetStmt, HirLiteral, HirLocal, HirMatch, + HirMatchArm, HirParam, HirPattern, HirReturnStmt, HirStmt, HirStruct, HirStructLiteral, + HirStructLiteralField, HirType, HirUnary, HirWhileStmt, IntrinsicId, LocalId, ResolvedCallee, }; use super::{ @@ -318,6 +318,16 @@ fn lower_stmt(stmt: &Stmt, ctx: &mut LoweringCtx, ret_ty: &Ty) -> Result { + Ok(HirStmt::Break(HirBreakStmt { + span: break_stmt.span, + })) + } + Stmt::Continue(continue_stmt) => { + Ok(HirStmt::Continue(HirContinueStmt { + span: continue_stmt.span, + })) + } Stmt::If(if_stmt) => { let cond = lower_expr(&if_stmt.cond, ctx, ret_ty)?; let then_block = lower_block(&if_stmt.then_block, ctx, ret_ty)?; diff --git a/capc/src/typeck/monomorphize.rs b/capc/src/typeck/monomorphize.rs index 763223d..f155264 100644 --- a/capc/src/typeck/monomorphize.rs +++ b/capc/src/typeck/monomorphize.rs @@ -420,6 +420,12 @@ impl MonoCtx { span: ret.span, })) } + HirStmt::Break(break_stmt) => Ok(HirStmt::Break(HirBreakStmt { + span: break_stmt.span, + })), + HirStmt::Continue(continue_stmt) => Ok(HirStmt::Continue(HirContinueStmt { + span: continue_stmt.span, + })), HirStmt::If(if_stmt) => { let cond = self.mono_expr(module, &if_stmt.cond, subs)?; let then_block = self.mono_block(module, &if_stmt.then_block, subs)?; diff --git a/capc/tests/run.rs b/capc/tests/run.rs index 94cb3be..99b4bab 100644 --- a/capc/tests/run.rs +++ b/capc/tests/run.rs @@ -642,3 +642,45 @@ fn run_scoping_assign() { assert_eq!(code, 0); assert!(stdout.contains("scoping assign test ok"), "stdout was: {stdout:?}"); } + +#[test] +fn run_break_basic() { + let out_dir = make_out_dir("break_basic"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/break_basic.cap", + ]); + assert_eq!(code, 0); + assert!(stdout.contains("break ok"), "stdout was: {stdout:?}"); +} + +#[test] +fn run_continue_basic() { + let out_dir = make_out_dir("continue_basic"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/continue_basic.cap", + ]); + assert_eq!(code, 0); + assert!(stdout.contains("continue ok"), "stdout was: {stdout:?}"); +} + +#[test] +fn run_break_nested() { + let out_dir = make_out_dir("break_nested"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/break_nested.cap", + ]); + assert_eq!(code, 0); + assert!(stdout.contains("break nested ok"), "stdout was: {stdout:?}"); +} diff --git a/capc/tests/typecheck.rs b/capc/tests/typecheck.rs index d7d5756..f86069e 100644 --- a/capc/tests/typecheck.rs +++ b/capc/tests/typecheck.rs @@ -881,3 +881,30 @@ fn typecheck_impl_wrong_module() { "expected error about impl module, got: {msg}" ); } + +#[test] +fn typecheck_break_outside_loop_fails() { + let source = load_program("should_fail_break_outside_loop.cap"); + let module = parse_module(&source).expect("parse module"); + let stdlib = load_stdlib().expect("load stdlib"); + let err = type_check_program(&module, &stdlib, &[]).expect_err("expected type error"); + assert!(err.to_string().contains("break statement outside of loop")); +} + +#[test] +fn typecheck_continue_outside_loop_fails() { + let source = load_program("should_fail_continue_outside_loop.cap"); + let module = parse_module(&source).expect("parse module"); + let stdlib = load_stdlib().expect("load stdlib"); + let err = type_check_program(&module, &stdlib, &[]).expect_err("expected type error"); + assert!(err.to_string().contains("continue statement outside of loop")); +} + +#[test] +fn typecheck_break_in_function_fails() { + let source = load_program("should_fail_break_in_function.cap"); + let module = parse_module(&source).expect("parse module"); + let stdlib = load_stdlib().expect("load stdlib"); + let err = type_check_program(&module, &stdlib, &[]).expect_err("expected type error"); + assert!(err.to_string().contains("break statement outside of loop")); +} diff --git a/tests/programs/break_basic.cap b/tests/programs/break_basic.cap new file mode 100644 index 0000000..dc2e116 --- /dev/null +++ b/tests/programs/break_basic.cap @@ -0,0 +1,16 @@ +module break_basic +use sys::system +use sys::console + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + let i = 0 + while (i < 10) { + if (i == 5) { + break + } + i = i + 1 + } + c.println("break ok") + return 0 +} diff --git a/tests/programs/break_nested.cap b/tests/programs/break_nested.cap new file mode 100644 index 0000000..ca69d36 --- /dev/null +++ b/tests/programs/break_nested.cap @@ -0,0 +1,28 @@ +module break_nested +use sys::system +use sys::console + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + let outer_count = 0 + let inner_sum = 0 + let i = 0 + while (i < 5) { + let j = 0 + while (j < 10) { + if (j == 3) { + break + } + inner_sum = inner_sum + 1 + j = j + 1 + } + outer_count = outer_count + 1 + i = i + 1 + } + if (outer_count == 5) { + if (inner_sum == 15) { + c.println("break nested ok") + } + } + return 0 +} diff --git a/tests/programs/continue_basic.cap b/tests/programs/continue_basic.cap new file mode 100644 index 0000000..92df805 --- /dev/null +++ b/tests/programs/continue_basic.cap @@ -0,0 +1,23 @@ +module continue_basic +use sys::system +use sys::console + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + let sum = 0 + let i = 0 + while (i < 10) { + i = i + 1 + if (i == 3) { + continue + } + if (i == 7) { + continue + } + sum = sum + i + } + if (sum == 45) { + c.println("continue ok") + } + return 0 +} diff --git a/tests/programs/should_fail_break_in_function.cap b/tests/programs/should_fail_break_in_function.cap new file mode 100644 index 0000000..842e3c9 --- /dev/null +++ b/tests/programs/should_fail_break_in_function.cap @@ -0,0 +1,18 @@ +module should_fail_break_in_function +use sys::system +use sys::console + +fn helper() -> i32 { + break + return 0 +} + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + let i = 0 + while (i < 10) { + let x = helper() + i = i + 1 + } + return 0 +} diff --git a/tests/programs/should_fail_break_outside_loop.cap b/tests/programs/should_fail_break_outside_loop.cap new file mode 100644 index 0000000..ccd78cb --- /dev/null +++ b/tests/programs/should_fail_break_outside_loop.cap @@ -0,0 +1,11 @@ +module should_fail_break_outside_loop +use sys::system +use sys::console + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + c.println("before") + break + c.println("after") + return 0 +} diff --git a/tests/programs/should_fail_continue_outside_loop.cap b/tests/programs/should_fail_continue_outside_loop.cap new file mode 100644 index 0000000..2ef463f --- /dev/null +++ b/tests/programs/should_fail_continue_outside_loop.cap @@ -0,0 +1,11 @@ +module should_fail_continue_outside_loop +use sys::system +use sys::console + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + c.println("before") + continue + c.println("after") + return 0 +} From a22c056fa069a7c65975c15f719dd0d0322afc0f Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Tue, 30 Dec 2025 20:23:47 -0800 Subject: [PATCH 02/30] Fix nested match codegen panic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, nested matches where all arms terminate would panic with "you cannot add an instruction to a block already filled". Example that crashed: ``` fn test(x: i32) -> i32 { match (x > 0) { true => { match (x > 5) { true => { return 2 } false => { return 1 } } } false => { return 0 } } } ``` The issue: after the inner match terminates all paths (adds trap to merge_block), the outer match still thinks control continues and tries to add a `jump` to the already-terminated block. The fix has two parts: 1. emit_hir_match_stmt now returns `Result` where the bool indicates whether all paths diverged (no arm continues to merge_block) 2. HirStmt::Expr special-cases unit-type match expressions - calls emit_hir_match_stmt directly and returns Flow::Terminated if diverged This approach is pragmatic: the clean solution would have all expressions return divergence info, but that requires updating ~50 call sites. Since match is currently the only expression type that can diverge, special-casing it in HirStmt::Expr is reasonable. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- capc/src/codegen/emit.rs | 37 +++++++++++++++++++++++++++++---- capc/tests/run.rs | 14 +++++++++++++ tests/programs/nested_match.cap | 30 ++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 tests/programs/nested_match.cap diff --git a/capc/src/codegen/emit.rs b/capc/src/codegen/emit.rs index 7f177ea..89b42ba 100644 --- a/capc/src/codegen/emit.rs +++ b/capc/src/codegen/emit.rs @@ -207,6 +207,30 @@ fn emit_hir_stmt_inner( return Ok(Flow::Terminated); } HirStmt::Expr(expr_stmt) => { + // Special handling for match expressions that might diverge + if let crate::hir::HirExpr::Match(match_expr) = &expr_stmt.expr { + if matches!( + match_expr.result_ty.ty, + crate::typeck::Ty::Builtin(crate::typeck::BuiltinType::Unit) + ) { + let diverged = emit_hir_match_stmt( + builder, + match_expr, + locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + loop_target, + )?; + if diverged { + return Ok(Flow::Terminated); + } + // Don't fall through to emit_hir_expr - we already emitted the match + return Ok(Flow::Continues); + } + } let _ = emit_hir_expr( builder, &expr_stmt.expr, @@ -916,7 +940,9 @@ fn emit_hir_expr_inner( match_expr.result_ty.ty, crate::typeck::Ty::Builtin(crate::typeck::BuiltinType::Unit) ) { - emit_hir_match_stmt( + // Note: divergence handling is done at HirStmt::Expr level. + // Here we just emit the match and return Unit. + let _diverged = emit_hir_match_stmt( builder, match_expr, locals, @@ -926,7 +952,8 @@ fn emit_hir_expr_inner( module, data_counter, None, // break/continue not supported in expression-context matches - ) + )?; + Ok(ValueRepr::Unit) } else { emit_hir_match_expr( builder, @@ -1651,6 +1678,7 @@ fn emit_hir_short_circuit_expr( } /// Emit HIR match as statement (arms can contain returns, don't produce values). +/// Returns true if all paths diverged (returned/broke/continued). fn emit_hir_match_stmt( builder: &mut FunctionBuilder, match_expr: &crate::hir::HirMatch, @@ -1661,7 +1689,7 @@ fn emit_hir_match_stmt( module: &mut ObjectModule, data_counter: &mut u32, loop_target: Option, -) -> Result { +) -> Result { // Emit the scrutinee expression let value = emit_hir_expr( builder, @@ -1769,7 +1797,8 @@ fn emit_hir_match_stmt( builder.ins().trap(ir::TrapCode::UnreachableCodeReached); } - Ok(ValueRepr::Unit) + // Return true if all paths diverged (no arm continues to merge_block) + Ok(!any_arm_continues) } /// Emit HIR match expression diff --git a/capc/tests/run.rs b/capc/tests/run.rs index 99b4bab..9043d09 100644 --- a/capc/tests/run.rs +++ b/capc/tests/run.rs @@ -684,3 +684,17 @@ fn run_break_nested() { assert_eq!(code, 0); assert!(stdout.contains("break nested ok"), "stdout was: {stdout:?}"); } + +#[test] +fn run_nested_match() { + let out_dir = make_out_dir("nested_match"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/nested_match.cap", + ]); + assert_eq!(code, 0); + assert!(stdout.contains("nested match ok"), "stdout was: {stdout:?}"); +} diff --git a/tests/programs/nested_match.cap b/tests/programs/nested_match.cap new file mode 100644 index 0000000..62281bf --- /dev/null +++ b/tests/programs/nested_match.cap @@ -0,0 +1,30 @@ +module nested_match +use sys::system +use sys::console + +fn classify(x: i32) -> i32 { + match (x > 0) { + true => { + match (x > 10) { + true => { return 2 } + false => { return 1 } + } + } + false => { return 0 } + } +} + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + let a = classify(-5) + let b = classify(5) + let d = classify(15) + if (a == 0) { + if (b == 1) { + if (d == 2) { + c.println("nested match ok") + } + } + } + return 0 +} From 978f14b444d6a88e1c39d31c5ce0dbb673f38a50 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Tue, 30 Dec 2025 20:29:49 -0800 Subject: [PATCH 03/30] Use break in sort example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that break is supported, replace the `j = 0 // break` workaround with an actual break statement. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- examples/sort/sort.cap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/sort/sort.cap b/examples/sort/sort.cap index c360f07..8f59920 100644 --- a/examples/sort/sort.cap +++ b/examples/sort/sort.cap @@ -72,7 +72,7 @@ fn sort_indices(lines: VecString, indices: VecI32) -> unit { } j = j - 1 } else { - j = 0 // break + break } } i = i + 1 From d9e0aa372cb455492637b4a073ef6fac52569b09 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Wed, 31 Dec 2025 11:23:57 -0800 Subject: [PATCH 04/30] Add for loop support with range syntax MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement for loops with the syntax `for i in start..end { body }`: - Lexer: Add For, In, DotDot tokens - AST: Add ForStmt struct - Parser: Add parse_for() function - HIR: Add HirForStmt struct - Type checker: Validate range bounds are i32, check affine moves - Lowering: Lower ForStmt to HirForStmt with fresh local for loop var - Monomorphization: Handle for loop monomorphization - Codegen: Generate loop with separate increment block for proper continue semantics (increment before jumping to header) Add comprehensive tests: - Runtime: for_basic, for_break, for_continue, for_nested, for_sum, for_empty_range, for_break_nested, for_continue_nested - Typecheck: for_non_i32_start, for_non_i32_end, for_loop_move 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- capc/src/ast.rs | 11 ++ capc/src/codegen/emit.rs | 121 +++++++++++++++++- capc/src/hir.rs | 11 ++ capc/src/lexer.rs | 6 + capc/src/parser.rs | 19 +++ capc/src/typeck/check.rs | 84 ++++++++++++ capc/src/typeck/lower.rs | 22 +++- capc/src/typeck/monomorphize.rs | 12 ++ capc/tests/run.rs | 112 ++++++++++++++++ capc/tests/typecheck.rs | 27 ++++ tests/programs/for_basic.cap | 15 +++ tests/programs/for_break.cap | 18 +++ tests/programs/for_break_nested.cap | 31 +++++ tests/programs/for_continue.cap | 25 ++++ tests/programs/for_continue_nested.cap | 28 ++++ tests/programs/for_empty_range.cap | 24 ++++ tests/programs/for_nested.cap | 22 ++++ tests/programs/for_sum.cap | 20 +++ tests/programs/should_fail_for_loop_move.cap | 12 ++ .../programs/should_fail_for_non_i32_end.cap | 14 ++ .../should_fail_for_non_i32_start.cap | 14 ++ 21 files changed, 644 insertions(+), 4 deletions(-) create mode 100644 tests/programs/for_basic.cap create mode 100644 tests/programs/for_break.cap create mode 100644 tests/programs/for_break_nested.cap create mode 100644 tests/programs/for_continue.cap create mode 100644 tests/programs/for_continue_nested.cap create mode 100644 tests/programs/for_empty_range.cap create mode 100644 tests/programs/for_nested.cap create mode 100644 tests/programs/for_sum.cap create mode 100644 tests/programs/should_fail_for_loop_move.cap create mode 100644 tests/programs/should_fail_for_non_i32_end.cap create mode 100644 tests/programs/should_fail_for_non_i32_start.cap diff --git a/capc/src/ast.rs b/capc/src/ast.rs index 7a27f44..2d9076a 100644 --- a/capc/src/ast.rs +++ b/capc/src/ast.rs @@ -146,6 +146,7 @@ pub enum Stmt { Continue(ContinueStmt), If(IfStmt), While(WhileStmt), + For(ForStmt), Expr(ExprStmt), } @@ -188,6 +189,15 @@ pub struct WhileStmt { pub span: Span, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ForStmt { + pub var: Ident, + pub start: Expr, + pub end: Expr, + pub body: Block, + pub span: Span, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct ExprStmt { pub expr: Expr, @@ -211,6 +221,7 @@ impl Stmt { Stmt::Continue(s) => s.span, Stmt::If(s) => s.span, Stmt::While(s) => s.span, + Stmt::For(s) => s.span, Stmt::Expr(s) => s.span, } } diff --git a/capc/src/codegen/emit.rs b/capc/src/codegen/emit.rs index 89b42ba..05406fd 100644 --- a/capc/src/codegen/emit.rs +++ b/capc/src/codegen/emit.rs @@ -35,7 +35,7 @@ struct ResultStringSlots { /// Target blocks for break/continue inside a loop. #[derive(Copy, Clone, Debug)] pub(super) struct LoopTarget { - pub header_block: ir::Block, + pub continue_block: ir::Block, // for while: header_block, for: increment_block pub exit_block: ir::Block, } @@ -358,8 +358,9 @@ fn emit_hir_stmt_inner( builder.switch_to_block(body_block); // Create loop target for break/continue + // For while loops, continue goes to header (re-evaluate condition) let body_loop_target = Some(LoopTarget { - header_block, + continue_block: header_block, exit_block, }); @@ -397,6 +398,120 @@ fn emit_hir_stmt_inner( // After the loop, restore the pre-loop locals snapshot *locals = saved_locals; } + HirStmt::For(for_stmt) => { + // Snapshot locals so loop-body lets don't leak out of the loop + let saved_locals = locals.clone(); + + // Create stack slot for loop variable + let loop_var_slot = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + 4, // i32 is 4 bytes + )); + + // Evaluate start value and store in slot + let start_val = emit_hir_expr( + builder, + &for_stmt.start, + locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + )?; + let start_i32 = match start_val { + ValueRepr::Single(v) => v, + _ => return Err(CodegenError::Unsupported("for loop start".to_string())), + }; + builder.ins().stack_store(start_i32, loop_var_slot, 0); + + // Evaluate end value (once, before the loop) + let end_val = emit_hir_expr( + builder, + &for_stmt.end, + locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + )?; + let end_i32 = match end_val { + ValueRepr::Single(v) => v, + _ => return Err(CodegenError::Unsupported("for loop end".to_string())), + }; + + let header_block = builder.create_block(); + let body_block = builder.create_block(); + let increment_block = builder.create_block(); + let exit_block = builder.create_block(); + + builder.ins().jump(header_block, &[]); + builder.switch_to_block(header_block); + + // Load loop variable and compare with end + let current_val = builder.ins().stack_load(ir::types::I32, loop_var_slot, 0); + let cond = builder.ins().icmp(ir::condcodes::IntCC::SignedLessThan, current_val, end_i32); + builder.ins().brif(cond, body_block, &[], exit_block, &[]); + + builder.switch_to_block(body_block); + + // Create loop target for break/continue + // For for loops, continue goes to increment block (not header) + let body_loop_target = Some(LoopTarget { + continue_block: increment_block, + exit_block, + }); + + // Loop body gets its own locals with the loop variable bound + let mut body_locals = saved_locals.clone(); + body_locals.insert( + for_stmt.var_id, + LocalValue::Slot(loop_var_slot, ir::types::I32), + ); + + let mut body_terminated = false; + for stmt in &for_stmt.body.stmts { + let flow = emit_hir_stmt( + builder, + stmt, + &mut body_locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + body_loop_target, + )?; + if flow == Flow::Terminated { + body_terminated = true; + break; + } + } + + // Fall through to increment block (if not terminated by break/continue/return) + if !body_terminated { + builder.ins().jump(increment_block, &[]); + } + + // Increment block: increment loop variable and jump back to header + builder.switch_to_block(increment_block); + let current = builder.ins().stack_load(ir::types::I32, loop_var_slot, 0); + let one = builder.ins().iconst(ir::types::I32, 1); + let next = builder.ins().iadd(current, one); + builder.ins().stack_store(next, loop_var_slot, 0); + builder.ins().jump(header_block, &[]); + + builder.seal_block(body_block); + builder.seal_block(increment_block); + builder.seal_block(header_block); + + builder.switch_to_block(exit_block); + builder.seal_block(exit_block); + + // After the loop, restore the pre-loop locals snapshot + *locals = saved_locals; + } HirStmt::Break(_) => { let target = loop_target.expect("break outside of loop (should be caught by typeck)"); builder.ins().jump(target.exit_block, &[]); @@ -404,7 +519,7 @@ fn emit_hir_stmt_inner( } HirStmt::Continue(_) => { let target = loop_target.expect("continue outside of loop (should be caught by typeck)"); - builder.ins().jump(target.header_block, &[]); + builder.ins().jump(target.continue_block, &[]); return Ok(Flow::Terminated); } } diff --git a/capc/src/hir.rs b/capc/src/hir.rs index 33fd4cb..2ef65e0 100644 --- a/capc/src/hir.rs +++ b/capc/src/hir.rs @@ -111,6 +111,7 @@ pub enum HirStmt { Continue(HirContinueStmt), If(HirIfStmt), While(HirWhileStmt), + For(HirForStmt), Expr(HirExprStmt), } @@ -124,6 +125,7 @@ impl HirStmt { HirStmt::Continue(s) => s.span, HirStmt::If(s) => s.span, HirStmt::While(s) => s.span, + HirStmt::For(s) => s.span, HirStmt::Expr(s) => s.span, } } @@ -175,6 +177,15 @@ pub struct HirWhileStmt { pub span: Span, } +#[derive(Debug, Clone)] +pub struct HirForStmt { + pub var_id: LocalId, + pub start: HirExpr, + pub end: HirExpr, + pub body: HirBlock, + pub span: Span, +} + #[derive(Debug, Clone)] pub struct HirExprStmt { pub expr: HirExpr, diff --git a/capc/src/lexer.rs b/capc/src/lexer.rs index c663d60..0347ec6 100644 --- a/capc/src/lexer.rs +++ b/capc/src/lexer.rs @@ -39,6 +39,10 @@ pub enum TokenKind { Else, #[token("while")] While, + #[token("for")] + For, + #[token("in")] + In, #[token("break")] Break, #[token("continue")] @@ -122,6 +126,8 @@ pub enum TokenKind { Colon, #[token(",")] Comma, + #[token("..", priority = 3)] + DotDot, #[token(".")] Dot, #[token(";")] diff --git a/capc/src/parser.rs b/capc/src/parser.rs index 35090db..8afcf2f 100644 --- a/capc/src/parser.rs +++ b/capc/src/parser.rs @@ -470,6 +470,7 @@ impl Parser { Some(TokenKind::Continue) => Ok(Stmt::Continue(self.parse_continue()?)), Some(TokenKind::If) => Ok(Stmt::If(self.parse_if()?)), Some(TokenKind::While) => Ok(Stmt::While(self.parse_while()?)), + Some(TokenKind::For) => Ok(Stmt::For(self.parse_for()?)), Some(TokenKind::Ident) => { if self.peek_token(1).is_some_and(|t| t.kind == TokenKind::Eq) { Ok(Stmt::Assign(self.parse_assign()?)) @@ -592,6 +593,24 @@ impl Parser { }) } + fn parse_for(&mut self) -> Result { + let start = self.expect(TokenKind::For)?.span.start; + let var = self.expect_ident()?; + self.expect(TokenKind::In)?; + let range_start = self.parse_primary()?; + self.expect(TokenKind::DotDot)?; + let range_end = self.parse_primary()?; + let body = self.parse_block()?; + let end = body.span.end; + Ok(ForStmt { + var, + start: range_start, + end: range_end, + body, + span: Span::new(start, end), + }) + } + fn parse_expr_stmt(&mut self) -> Result { let expr = self.parse_expr()?; let expr_span = expr.span(); diff --git a/capc/src/typeck/check.rs b/capc/src/typeck/check.rs index 443032a..0ca72a8 100644 --- a/capc/src/typeck/check.rs +++ b/capc/src/typeck/check.rs @@ -167,6 +167,11 @@ fn block_contains_ptr(block: &Block) -> Option { return Some(span); } } + Stmt::For(for_stmt) => { + if let Some(span) = block_contains_ptr(&for_stmt.body) { + return Some(span); + } + } Stmt::Expr(expr_stmt) => { if let Expr::Match(match_expr) = &expr_stmt.expr { for arm in &match_expr.arms { @@ -609,6 +614,85 @@ fn check_stmt( while_stmt.span, )?; } + Stmt::For(for_stmt) => { + // Check start expression - must be i32 + let start_ty = check_expr( + &for_stmt.start, + functions, + scopes, + UseMode::Read, + recorder, + use_map, + struct_map, + enum_map, + stdlib, + ret_ty, + module_name, + type_params, + )?; + if start_ty != Ty::Builtin(BuiltinType::I32) { + return Err(TypeError::new( + "for loop range start must be i32".to_string(), + for_stmt.start.span(), + )); + } + + // Check end expression - must be i32 + let end_ty = check_expr( + &for_stmt.end, + functions, + scopes, + UseMode::Read, + recorder, + use_map, + struct_map, + enum_map, + stdlib, + ret_ty, + module_name, + type_params, + )?; + if end_ty != Ty::Builtin(BuiltinType::I32) { + return Err(TypeError::new( + "for loop range end must be i32".to_string(), + for_stmt.end.span(), + )); + } + + // Create body scope with loop variable bound + let mut body_scopes = scopes.clone(); + body_scopes.push_scope(); + body_scopes.insert_local( + for_stmt.var.item.clone(), + Ty::Builtin(BuiltinType::I32), + ); + + check_block( + &for_stmt.body, + ret_ty, + functions, + &mut body_scopes, + recorder, + use_map, + struct_map, + enum_map, + stdlib, + module_name, + type_params, + true, // inside loop, break/continue allowed + )?; + + // Pop the loop variable scope before checking affine states + body_scopes.pop_scope(); + + ensure_affine_states_match( + scopes, + &body_scopes, + struct_map, + enum_map, + for_stmt.span, + )?; + } Stmt::Expr(expr_stmt) => { if let Expr::Match(match_expr) = &expr_stmt.expr { let _ = check_match_stmt( diff --git a/capc/src/typeck/lower.rs b/capc/src/typeck/lower.rs index 9b6a78e..69b6ab4 100644 --- a/capc/src/typeck/lower.rs +++ b/capc/src/typeck/lower.rs @@ -6,7 +6,7 @@ use crate::abi::AbiType; use crate::hir::{ HirAssignStmt, HirBinary, HirBlock, HirBreakStmt, HirCall, HirContinueStmt, HirEnum, HirEnumVariant, HirEnumVariantExpr, HirExpr, HirExprStmt, HirExternFunction, HirField, - HirFieldAccess, HirFunction, HirIfStmt, HirLetStmt, HirLiteral, HirLocal, HirMatch, + HirFieldAccess, HirForStmt, HirFunction, HirIfStmt, HirLetStmt, HirLiteral, HirLocal, HirMatch, HirMatchArm, HirParam, HirPattern, HirReturnStmt, HirStmt, HirStruct, HirStructLiteral, HirStructLiteralField, HirType, HirUnary, HirWhileStmt, IntrinsicId, LocalId, ResolvedCallee, }; @@ -354,6 +354,26 @@ fn lower_stmt(stmt: &Stmt, ctx: &mut LoweringCtx, ret_ty: &Ty) -> Result { + let start = lower_expr(&for_stmt.start, ctx, ret_ty)?; + let end = lower_expr(&for_stmt.end, ctx, ret_ty)?; + + // Create a fresh local for the loop variable + let var_id = ctx.fresh_local( + for_stmt.var.item.clone(), + crate::typeck::Ty::Builtin(crate::typeck::BuiltinType::I32), + ); + + let body = lower_block(&for_stmt.body, ctx, ret_ty)?; + + Ok(HirStmt::For(HirForStmt { + var_id, + start, + end, + body, + span: for_stmt.span, + })) + } Stmt::Expr(expr_stmt) => { if let Expr::Match(match_expr) = &expr_stmt.expr { match lower_expr(&expr_stmt.expr, ctx, ret_ty) { diff --git a/capc/src/typeck/monomorphize.rs b/capc/src/typeck/monomorphize.rs index f155264..425659c 100644 --- a/capc/src/typeck/monomorphize.rs +++ b/capc/src/typeck/monomorphize.rs @@ -450,6 +450,18 @@ impl MonoCtx { span: while_stmt.span, })) } + HirStmt::For(for_stmt) => { + let start = self.mono_expr(module, &for_stmt.start, subs)?; + let end = self.mono_expr(module, &for_stmt.end, subs)?; + let body = self.mono_block(module, &for_stmt.body, subs)?; + Ok(HirStmt::For(HirForStmt { + var_id: for_stmt.var_id, + start, + end, + body, + span: for_stmt.span, + })) + } HirStmt::Expr(expr_stmt) => { let expr = self.mono_expr(module, &expr_stmt.expr, subs)?; Ok(HirStmt::Expr(HirExprStmt { diff --git a/capc/tests/run.rs b/capc/tests/run.rs index 9043d09..a007594 100644 --- a/capc/tests/run.rs +++ b/capc/tests/run.rs @@ -698,3 +698,115 @@ fn run_nested_match() { assert_eq!(code, 0); assert!(stdout.contains("nested match ok"), "stdout was: {stdout:?}"); } + +#[test] +fn run_for_basic() { + let out_dir = make_out_dir("for_basic"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/for_basic.cap", + ]); + assert_eq!(code, 0); + assert!(stdout.contains("for_basic ok"), "stdout was: {stdout:?}"); +} + +#[test] +fn run_for_break() { + let out_dir = make_out_dir("for_break"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/for_break.cap", + ]); + assert_eq!(code, 0); + assert!(stdout.contains("for_break ok"), "stdout was: {stdout:?}"); +} + +#[test] +fn run_for_continue() { + let out_dir = make_out_dir("for_continue"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/for_continue.cap", + ]); + assert_eq!(code, 0); + assert!(stdout.contains("for_continue ok"), "stdout was: {stdout:?}"); +} + +#[test] +fn run_for_nested() { + let out_dir = make_out_dir("for_nested"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/for_nested.cap", + ]); + assert_eq!(code, 0); + assert!(stdout.contains("for_nested ok"), "stdout was: {stdout:?}"); +} + +#[test] +fn run_for_sum() { + let out_dir = make_out_dir("for_sum"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/for_sum.cap", + ]); + assert_eq!(code, 0); + assert!(stdout.contains("for_sum ok"), "stdout was: {stdout:?}"); +} + +#[test] +fn run_for_empty_range() { + let out_dir = make_out_dir("for_empty_range"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/for_empty_range.cap", + ]); + assert_eq!(code, 0); + assert!(stdout.contains("for_empty_range ok"), "stdout was: {stdout:?}"); +} + +#[test] +fn run_for_break_nested() { + let out_dir = make_out_dir("for_break_nested"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/for_break_nested.cap", + ]); + assert_eq!(code, 0); + assert!(stdout.contains("for_break_nested ok"), "stdout was: {stdout:?}"); +} + +#[test] +fn run_for_continue_nested() { + let out_dir = make_out_dir("for_continue_nested"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/for_continue_nested.cap", + ]); + assert_eq!(code, 0); + assert!(stdout.contains("for_continue_nested ok"), "stdout was: {stdout:?}"); +} diff --git a/capc/tests/typecheck.rs b/capc/tests/typecheck.rs index f86069e..b105691 100644 --- a/capc/tests/typecheck.rs +++ b/capc/tests/typecheck.rs @@ -908,3 +908,30 @@ fn typecheck_break_in_function_fails() { let err = type_check_program(&module, &stdlib, &[]).expect_err("expected type error"); assert!(err.to_string().contains("break statement outside of loop")); } + +#[test] +fn typecheck_for_non_i32_start_fails() { + let source = load_program("should_fail_for_non_i32_start.cap"); + let module = parse_module(&source).expect("parse module"); + let stdlib = load_stdlib().expect("load stdlib"); + let err = type_check_program(&module, &stdlib, &[]).expect_err("expected type error"); + assert!(err.to_string().contains("for loop range start must be i32")); +} + +#[test] +fn typecheck_for_non_i32_end_fails() { + let source = load_program("should_fail_for_non_i32_end.cap"); + let module = parse_module(&source).expect("parse module"); + let stdlib = load_stdlib().expect("load stdlib"); + let err = type_check_program(&module, &stdlib, &[]).expect_err("expected type error"); + assert!(err.to_string().contains("for loop range end must be i32")); +} + +#[test] +fn typecheck_for_loop_move_fails() { + let source = load_program("should_fail_for_loop_move.cap"); + let module = parse_module(&source).expect("parse module"); + let stdlib = load_stdlib().expect("load stdlib"); + let err = type_check_program(&module, &stdlib, &[]).expect_err("expected type error"); + assert!(err.to_string().contains("moved inside loop")); +} diff --git a/tests/programs/for_basic.cap b/tests/programs/for_basic.cap new file mode 100644 index 0000000..635bf66 --- /dev/null +++ b/tests/programs/for_basic.cap @@ -0,0 +1,15 @@ +module for_basic +use sys::system +use sys::console + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + + // Basic for loop: should print 0 1 2 3 4 + for i in 0..5 { + c.println_i32(i) + } + + c.println("for_basic ok") + return 0 +} diff --git a/tests/programs/for_break.cap b/tests/programs/for_break.cap new file mode 100644 index 0000000..143f721 --- /dev/null +++ b/tests/programs/for_break.cap @@ -0,0 +1,18 @@ +module for_break +use sys::system +use sys::console + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + + // For loop with break: should exit when i == 5 + for i in 0..100 { + if i == 5 { + break + } + c.println_i32(i) + } + + c.println("for_break ok") + return 0 +} diff --git a/tests/programs/for_break_nested.cap b/tests/programs/for_break_nested.cap new file mode 100644 index 0000000..2c0451d --- /dev/null +++ b/tests/programs/for_break_nested.cap @@ -0,0 +1,31 @@ +module for_break_nested +use sys::system +use sys::console + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + + // Break in nested loop: only breaks inner loop + let outer_count = 0 + let inner_count = 0 + + for i in 0..5 { + outer_count = outer_count + 1 + for j in 0..10 { + if j == 3 { + break + } + inner_count = inner_count + 1 + } + } + + // outer_count should be 5 (all iterations) + // inner_count should be 5 * 3 = 15 (break at j=3 each time) + if outer_count == 5 { + if inner_count == 15 { + c.println("for_break_nested ok") + } + } + + return 0 +} diff --git a/tests/programs/for_continue.cap b/tests/programs/for_continue.cap new file mode 100644 index 0000000..2f5b728 --- /dev/null +++ b/tests/programs/for_continue.cap @@ -0,0 +1,25 @@ +module for_continue +use sys::system +use sys::console + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + + // For loop with continue: skip specific values + // Should print 0 1 3 4 6 7 9 (skip 2, 5, 8) + for i in 0..10 { + if i == 2 { + continue + } + if i == 5 { + continue + } + if i == 8 { + continue + } + c.println_i32(i) + } + + c.println("for_continue ok") + return 0 +} diff --git a/tests/programs/for_continue_nested.cap b/tests/programs/for_continue_nested.cap new file mode 100644 index 0000000..9ab600d --- /dev/null +++ b/tests/programs/for_continue_nested.cap @@ -0,0 +1,28 @@ +module for_continue_nested +use sys::system +use sys::console + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + + // Continue in nested loop: only affects inner loop + let sum = 0 + + for i in 0..3 { + for j in 0..5 { + // Skip when j is 2 + if j == 2 { + continue + } + sum = sum + 1 + } + } + + // Each inner loop runs 5 times but skips j=2, so 4 iterations + // Total: 3 * 4 = 12 + if sum == 12 { + c.println("for_continue_nested ok") + } + + return 0 +} diff --git a/tests/programs/for_empty_range.cap b/tests/programs/for_empty_range.cap new file mode 100644 index 0000000..1e63a0e --- /dev/null +++ b/tests/programs/for_empty_range.cap @@ -0,0 +1,24 @@ +module for_empty_range +use sys::system +use sys::console + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + + // Empty range (start >= end): body should not execute + let executed = 0 + for i in 5..5 { + executed = 1 + } + + // Also test negative range + for i in 10..5 { + executed = 1 + } + + if executed == 0 { + c.println("for_empty_range ok") + } + + return 0 +} diff --git a/tests/programs/for_nested.cap b/tests/programs/for_nested.cap new file mode 100644 index 0000000..99a532a --- /dev/null +++ b/tests/programs/for_nested.cap @@ -0,0 +1,22 @@ +module for_nested +use sys::system +use sys::console + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + + // Nested for loops: count total iterations + let count = 0 + for i in 0..3 { + for j in 0..4 { + count = count + 1 + } + } + + // Should be 3 * 4 = 12 + if count == 12 { + c.println("for_nested ok") + } + + return 0 +} diff --git a/tests/programs/for_sum.cap b/tests/programs/for_sum.cap new file mode 100644 index 0000000..45ae5c4 --- /dev/null +++ b/tests/programs/for_sum.cap @@ -0,0 +1,20 @@ +module for_sum +use sys::system +use sys::console + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + + // Sum numbers 0 to 9 + let sum = 0 + for i in 0..10 { + sum = sum + i + } + + // 0+1+2+3+4+5+6+7+8+9 = 45 + if sum == 45 { + c.println("for_sum ok") + } + + return 0 +} diff --git a/tests/programs/should_fail_for_loop_move.cap b/tests/programs/should_fail_for_loop_move.cap new file mode 100644 index 0000000..11ee623 --- /dev/null +++ b/tests/programs/should_fail_for_loop_move.cap @@ -0,0 +1,12 @@ +package safe +module should_fail_for_loop_move + +capability struct Cap + +pub fn main() -> i32 { + let c = Cap{} + for i in 0..5 { + let x = c + } + return 0 +} diff --git a/tests/programs/should_fail_for_non_i32_end.cap b/tests/programs/should_fail_for_non_i32_end.cap new file mode 100644 index 0000000..37cc38a --- /dev/null +++ b/tests/programs/should_fail_for_non_i32_end.cap @@ -0,0 +1,14 @@ +module should_fail_for_non_i32_end +use sys::system +use sys::console + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + + // Error: for loop range end must be i32 + for i in 0..true { + c.println_i32(i) + } + + return 0 +} diff --git a/tests/programs/should_fail_for_non_i32_start.cap b/tests/programs/should_fail_for_non_i32_start.cap new file mode 100644 index 0000000..8748f2c --- /dev/null +++ b/tests/programs/should_fail_for_non_i32_start.cap @@ -0,0 +1,14 @@ +module should_fail_for_non_i32_start +use sys::system +use sys::console + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + + // Error: for loop range start must be i32 + for i in true..5 { + c.println_i32(i) + } + + return 0 +} From a6fe98f23525368b4453546e4f709b4bf2f77292 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Wed, 31 Dec 2025 11:41:26 -0800 Subject: [PATCH 05/30] Add string comparison operators == and != MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add capable_rt_string_eq runtime function for byte-by-byte comparison - Add emit_string_eq helper in codegen to call the runtime function - Add match cases for BinaryOp::Eq/Neq with ValueRepr::Pair (strings) - Add string_compare test program Strings can now be compared directly with == and != operators instead of requiring the .eq() method call. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- capc/src/codegen/emit.rs | 46 +++++++++++++++++++++++++++++++ capc/tests/run.rs | 14 ++++++++++ runtime/src/lib.rs | 21 ++++++++++++++ tests/programs/string_compare.cap | 43 +++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+) create mode 100644 tests/programs/string_compare.cap diff --git a/capc/src/codegen/emit.rs b/capc/src/codegen/emit.rs index 05406fd..3fade58 100644 --- a/capc/src/codegen/emit.rs +++ b/capc/src/codegen/emit.rs @@ -992,6 +992,17 @@ fn emit_hir_expr_inner( let cmp = builder.ins().icmp(IntCC::NotEqual, a, b); Ok(ValueRepr::Single(bool_to_i8(builder, cmp))) } + (BinaryOp::Eq, ValueRepr::Pair(ptr1, len1), ValueRepr::Pair(ptr2, len2)) => { + let result = emit_string_eq(builder, module, ptr1, len1, ptr2, len2)?; + Ok(ValueRepr::Single(result)) + } + (BinaryOp::Neq, ValueRepr::Pair(ptr1, len1), ValueRepr::Pair(ptr2, len2)) => { + let eq_result = emit_string_eq(builder, module, ptr1, len1, ptr2, len2)?; + // Invert the result: 1 becomes 0, 0 becomes 1 + let one = builder.ins().iconst(ir::types::I8, 1); + let neq_result = builder.ins().bxor(eq_result, one); + Ok(ValueRepr::Single(neq_result)) + } (BinaryOp::Lt, ValueRepr::Single(a), ValueRepr::Single(b)) => { let cmp = builder.ins().icmp(cmp_cc(&binary.left, IntCC::SignedLessThan, IntCC::UnsignedLessThan), a, b); Ok(ValueRepr::Single(bool_to_i8(builder, cmp))) @@ -1188,6 +1199,41 @@ fn trap_on_overflow(builder: &mut FunctionBuilder, overflow: Value) { builder.seal_block(ok_block); } +/// Emit a call to the runtime string equality function. +/// Returns an i8 value: 1 if strings are equal, 0 otherwise. +fn emit_string_eq( + builder: &mut FunctionBuilder, + module: &mut ObjectModule, + ptr1: Value, + len1: Value, + ptr2: Value, + len2: Value, +) -> Result { + use cranelift_codegen::ir::{AbiParam, Signature}; + use cranelift_codegen::isa::CallConv; + + let ptr_ty = module.isa().pointer_type(); + + // Build signature: (ptr, i64, ptr, i64) -> i8 + let mut sig = Signature::new(CallConv::SystemV); + sig.params.push(AbiParam::new(ptr_ty)); + sig.params.push(AbiParam::new(ir::types::I64)); + sig.params.push(AbiParam::new(ptr_ty)); + sig.params.push(AbiParam::new(ir::types::I64)); + sig.returns.push(AbiParam::new(ir::types::I8)); + + // Declare and import the runtime function + let func_id = module + .declare_function("capable_rt_string_eq", Linkage::Import, &sig) + .map_err(|err| CodegenError::Codegen(err.to_string()))?; + let local_func = module.declare_func_in_func(func_id, builder.func); + + // Call the function + let call_inst = builder.ins().call(local_func, &[ptr1, len1, ptr2, len2]); + let results = builder.inst_results(call_inst); + Ok(results[0]) +} + fn cmp_cc(expr: &crate::hir::HirExpr, signed: IntCC, unsigned: IntCC) -> IntCC { let ty = match expr { crate::hir::HirExpr::Literal(lit) => &lit.ty, diff --git a/capc/tests/run.rs b/capc/tests/run.rs index a007594..83b8ac0 100644 --- a/capc/tests/run.rs +++ b/capc/tests/run.rs @@ -810,3 +810,17 @@ fn run_for_continue_nested() { assert_eq!(code, 0); assert!(stdout.contains("for_continue_nested ok"), "stdout was: {stdout:?}"); } + +#[test] +fn run_string_compare() { + let out_dir = make_out_dir("string_compare"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/string_compare.cap", + ]); + assert_eq!(code, 0); + assert!(stdout.contains("string_compare ok"), "stdout was: {stdout:?}"); +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 1a6752f..e6bd5a1 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -2086,6 +2086,27 @@ pub extern "C" fn capable_rt_read_stdin_to_string( } } +#[no_mangle] +pub extern "C" fn capable_rt_string_eq( + ptr1: *const u8, + len1: usize, + ptr2: *const u8, + len2: usize, +) -> i8 { + if len1 != len2 { + return 0; + } + if len1 == 0 { + return 1; // Both empty strings are equal + } + if ptr1.is_null() || ptr2.is_null() { + return 0; + } + let s1 = unsafe { std::slice::from_raw_parts(ptr1, len1) }; + let s2 = unsafe { std::slice::from_raw_parts(ptr2, len2) }; + if s1 == s2 { 1 } else { 0 } +} + #[no_mangle] pub extern "C" fn capable_rt_string_len(ptr: *const u8, len: usize) -> i32 { let _ = ptr; diff --git a/tests/programs/string_compare.cap b/tests/programs/string_compare.cap new file mode 100644 index 0000000..c374975 --- /dev/null +++ b/tests/programs/string_compare.cap @@ -0,0 +1,43 @@ +module string_compare +use sys::system +use sys::console + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + + // Test equal strings + let a = "hello" + let b = "hello" + if (a == b) { + c.println("equal ok") + } + + // Test not equal strings + let x = "hello" + let y = "world" + if (x != y) { + c.println("not equal ok") + } + + // Test equal empty strings + let empty1 = "" + let empty2 = "" + if (empty1 == empty2) { + c.println("empty ok") + } + + // Test different lengths + let short = "hi" + let long = "hello" + if (short != long) { + c.println("diff len ok") + } + + // Test string literals directly + if ("test" == "test") { + c.println("literal ok") + } + + c.println("string_compare ok") + return 0 +} From 9b746d3653182c5cb01225822b5d19d415590a7c Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Wed, 31 Dec 2025 12:10:27 -0800 Subject: [PATCH 06/30] Add index syntax [] and change generics to <> MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add string indexing with str[i] syntax (returns u8) - Add Slice/MutSlice indexing support - Change generic syntax from [] to <> (e.g., Result, Box) - [] now unambiguously means indexing - Add heuristic to distinguish <> generics from < comparison - Update all stdlib and test .cap files to new syntax - Add string_index and generic_and_index test programs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- capc/src/ast.rs | 8 + capc/src/codegen/emit.rs | 203 ++++++++++++++++++ capc/src/hir.rs | 11 + capc/src/parser.rs | 92 +++++--- capc/src/typeck/check.rs | 69 ++++++ capc/src/typeck/lower.rs | 11 + capc/src/typeck/mod.rs | 1 + capc/src/typeck/monomorphize.rs | 10 + capc/tests/run.rs | 28 +++ examples/config_loader/config_loader.cap | 6 +- examples/http_server/http_server.cap | 20 +- examples/sort/sort.cap | 2 +- examples/uniq/uniq.cap | 2 +- stdlib/sys/args.cap | 2 +- stdlib/sys/buffer.cap | 22 +- stdlib/sys/fs.cap | 14 +- stdlib/sys/net.cap | 12 +- stdlib/sys/stdin.cap | 2 +- stdlib/sys/string.cap | 4 +- stdlib/sys/vec.cap | 32 +-- tests/programs/generic_and_index.cap | 47 ++++ tests/programs/generics_basic.cap | 8 +- tests/programs/result_construct.cap | 2 +- ...d_fail_capability_borrow_return_result.cap | 2 +- ...hould_fail_match_result_non_exhaustive.cap | 2 +- .../should_fail_result_unwrap_or_mismatch.cap | 2 +- .../programs/should_fail_try_err_mismatch.cap | 4 +- tests/programs/should_fail_try_non_result.cap | 2 +- .../should_pass_result_unwrap_err_or.cap | 4 +- .../programs/should_pass_result_unwrap_or.cap | 4 +- tests/programs/should_pass_try_question.cap | 4 +- tests/programs/slice_safe.cap | 2 +- tests/programs/string_index.cap | 36 ++++ tests/programs/untrusted_logs.cap | 2 +- 34 files changed, 564 insertions(+), 108 deletions(-) create mode 100644 tests/programs/generic_and_index.cap create mode 100644 tests/programs/string_index.cap diff --git a/capc/src/ast.rs b/capc/src/ast.rs index 2d9076a..1371994 100644 --- a/capc/src/ast.rs +++ b/capc/src/ast.rs @@ -234,6 +234,7 @@ pub enum Expr { Call(CallExpr), MethodCall(MethodCallExpr), FieldAccess(FieldAccessExpr), + Index(IndexExpr), StructLiteral(StructLiteralExpr), Unary(UnaryExpr), Binary(BinaryExpr), @@ -313,6 +314,13 @@ pub struct FieldAccessExpr { pub span: Span, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IndexExpr { + pub object: Box, + pub index: Box, + pub span: Span, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct MethodCallExpr { pub receiver: Box, diff --git a/capc/src/codegen/emit.rs b/capc/src/codegen/emit.rs index 3fade58..80cf9fa 100644 --- a/capc/src/codegen/emit.rs +++ b/capc/src/codegen/emit.rs @@ -1103,6 +1103,16 @@ fn emit_hir_expr_inner( module, data_counter, ), + HirExpr::Index(index_expr) => emit_hir_index( + builder, + index_expr, + locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + ), HirExpr::StructLiteral(literal) => { emit_hir_struct_literal( builder, @@ -1234,6 +1244,198 @@ fn emit_string_eq( Ok(results[0]) } +/// Emit an index expression, calling the appropriate runtime function. +fn emit_hir_index( + builder: &mut FunctionBuilder, + index_expr: &crate::hir::HirIndex, + locals: &HashMap, + fn_map: &HashMap, + enum_index: &EnumIndex, + struct_layouts: &StructLayoutIndex, + module: &mut ObjectModule, + data_counter: &mut u32, +) -> Result { + // Emit the object expression + let object = emit_hir_expr( + builder, + &index_expr.object, + locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + )?; + + // Emit the index expression + let index = emit_hir_expr( + builder, + &index_expr.index, + locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + )?; + + let index_val = match index { + ValueRepr::Single(v) => v, + _ => { + return Err(CodegenError::Codegen( + "index must be a single value".to_string(), + )) + } + }; + + // Get the object type to determine what runtime function to call + let object_ty = &index_expr.object.ty().ty; + + match object_ty { + crate::typeck::Ty::Builtin(crate::typeck::BuiltinType::String) => { + // For strings, call capable_rt_string_byte_at(ptr, len, index) -> u8 + let (ptr, len) = match object { + ValueRepr::Pair(p, l) => (p, l), + _ => { + return Err(CodegenError::Codegen( + "expected string to be a pointer-length pair".to_string(), + )) + } + }; + + let result = emit_string_byte_at(builder, module, ptr, len, index_val)?; + Ok(ValueRepr::Single(result)) + } + crate::typeck::Ty::Path(name, _) if name == "Slice" || name == "sys.buffer.Slice" => { + // For Slice[u8], call capable_rt_slice_at(handle, index) -> u8 + let handle = match object { + ValueRepr::Single(h) => h, + _ => { + return Err(CodegenError::Codegen( + "expected Slice to be a handle".to_string(), + )) + } + }; + + let result = emit_slice_at(builder, module, handle, index_val)?; + Ok(ValueRepr::Single(result)) + } + crate::typeck::Ty::Path(name, _) if name == "MutSlice" || name == "sys.buffer.MutSlice" => { + // For MutSlice[u8], call capable_rt_mut_slice_at(handle, index) -> u8 + let handle = match object { + ValueRepr::Single(h) => h, + _ => { + return Err(CodegenError::Codegen( + "expected MutSlice to be a handle".to_string(), + )) + } + }; + + let result = emit_mut_slice_at(builder, module, handle, index_val)?; + Ok(ValueRepr::Single(result)) + } + _ => Err(CodegenError::Codegen(format!( + "cannot index into type {:?}", + object_ty + ))), + } +} + +/// Emit a call to the runtime string byte_at function. +/// Returns a u8 value at the given index. +fn emit_string_byte_at( + builder: &mut FunctionBuilder, + module: &mut ObjectModule, + ptr: Value, + len: Value, + index: Value, +) -> Result { + use cranelift_codegen::ir::{AbiParam, Signature}; + use cranelift_codegen::isa::CallConv; + + let ptr_ty = module.isa().pointer_type(); + + // Build signature: (ptr, i64, i32) -> u8 + let mut sig = Signature::new(CallConv::SystemV); + sig.params.push(AbiParam::new(ptr_ty)); + sig.params.push(AbiParam::new(ir::types::I64)); + sig.params.push(AbiParam::new(ir::types::I32)); + sig.returns.push(AbiParam::new(ir::types::I8)); + + // Declare and import the runtime function + let func_id = module + .declare_function("capable_rt_string_byte_at", Linkage::Import, &sig) + .map_err(|err| CodegenError::Codegen(err.to_string()))?; + let local_func = module.declare_func_in_func(func_id, builder.func); + + // Call the function + let call_inst = builder.ins().call(local_func, &[ptr, len, index]); + let results = builder.inst_results(call_inst); + Ok(results[0]) +} + +/// Emit a call to the runtime slice at function. +/// Returns a u8 value at the given index. +fn emit_slice_at( + builder: &mut FunctionBuilder, + module: &mut ObjectModule, + handle: Value, + index: Value, +) -> Result { + use cranelift_codegen::ir::{AbiParam, Signature}; + use cranelift_codegen::isa::CallConv; + + let ptr_ty = module.isa().pointer_type(); + + // Build signature: (handle, i32) -> u8 + let mut sig = Signature::new(CallConv::SystemV); + sig.params.push(AbiParam::new(ptr_ty)); // Handle is a usize + sig.params.push(AbiParam::new(ir::types::I32)); + sig.returns.push(AbiParam::new(ir::types::I8)); + + // Declare and import the runtime function + let func_id = module + .declare_function("capable_rt_slice_at", Linkage::Import, &sig) + .map_err(|err| CodegenError::Codegen(err.to_string()))?; + let local_func = module.declare_func_in_func(func_id, builder.func); + + // Call the function + let call_inst = builder.ins().call(local_func, &[handle, index]); + let results = builder.inst_results(call_inst); + Ok(results[0]) +} + +/// Emit a call to the runtime mutable slice at function. +/// Returns a u8 value at the given index. +fn emit_mut_slice_at( + builder: &mut FunctionBuilder, + module: &mut ObjectModule, + handle: Value, + index: Value, +) -> Result { + use cranelift_codegen::ir::{AbiParam, Signature}; + use cranelift_codegen::isa::CallConv; + + let ptr_ty = module.isa().pointer_type(); + + // Build signature: (handle, i32) -> u8 + let mut sig = Signature::new(CallConv::SystemV); + sig.params.push(AbiParam::new(ptr_ty)); // Handle is a usize + sig.params.push(AbiParam::new(ir::types::I32)); + sig.returns.push(AbiParam::new(ir::types::I8)); + + // Declare and import the runtime function + let func_id = module + .declare_function("capable_rt_mut_slice_at", Linkage::Import, &sig) + .map_err(|err| CodegenError::Codegen(err.to_string()))?; + let local_func = module.declare_func_in_func(func_id, builder.func); + + // Call the function + let call_inst = builder.ins().call(local_func, &[handle, index]); + let results = builder.inst_results(call_inst); + Ok(results[0]) +} + fn cmp_cc(expr: &crate::hir::HirExpr, signed: IntCC, unsigned: IntCC) -> IntCC { let ty = match expr { crate::hir::HirExpr::Literal(lit) => &lit.ty, @@ -1241,6 +1443,7 @@ fn cmp_cc(expr: &crate::hir::HirExpr, signed: IntCC, unsigned: IntCC) -> IntCC { crate::hir::HirExpr::EnumVariant(variant) => &variant.enum_ty, crate::hir::HirExpr::Call(call) => &call.ret_ty, crate::hir::HirExpr::FieldAccess(field) => &field.field_ty, + crate::hir::HirExpr::Index(idx) => &idx.elem_ty, crate::hir::HirExpr::StructLiteral(lit) => &lit.struct_ty, crate::hir::HirExpr::Unary(unary) => &unary.ty, crate::hir::HirExpr::Binary(binary) => &binary.ty, diff --git a/capc/src/hir.rs b/capc/src/hir.rs index 2ef65e0..28090dd 100644 --- a/capc/src/hir.rs +++ b/capc/src/hir.rs @@ -200,6 +200,7 @@ pub enum HirExpr { EnumVariant(HirEnumVariantExpr), Call(HirCall), FieldAccess(HirFieldAccess), + Index(HirIndex), StructLiteral(HirStructLiteral), Unary(HirUnary), Binary(HirBinary), @@ -215,6 +216,7 @@ impl HirExpr { HirExpr::EnumVariant(e) => e.span, HirExpr::Call(e) => e.span, HirExpr::FieldAccess(e) => e.span, + HirExpr::Index(e) => e.span, HirExpr::StructLiteral(e) => e.span, HirExpr::Unary(e) => e.span, HirExpr::Binary(e) => e.span, @@ -230,6 +232,7 @@ impl HirExpr { HirExpr::EnumVariant(e) => &e.enum_ty, HirExpr::Call(e) => &e.ret_ty, HirExpr::FieldAccess(e) => &e.field_ty, + HirExpr::Index(e) => &e.elem_ty, HirExpr::StructLiteral(e) => &e.struct_ty, HirExpr::Unary(e) => &e.ty, HirExpr::Binary(e) => &e.ty, @@ -303,6 +306,14 @@ pub struct HirFieldAccess { pub span: Span, } +#[derive(Debug, Clone)] +pub struct HirIndex { + pub object: Box, + pub index: Box, + pub elem_ty: HirType, + pub span: Span, +} + #[derive(Debug, Clone)] pub struct HirStructLiteral { pub struct_ty: HirType, diff --git a/capc/src/parser.rs b/capc/src/parser.rs index 8afcf2f..83fe812 100644 --- a/capc/src/parser.rs +++ b/capc/src/parser.rs @@ -643,7 +643,7 @@ impl Parser { let start = lhs.span().start; self.bump(); // consume '.' let field = self.expect_ident()?; - let type_args = if self.peek_kind() == Some(TokenKind::LBracket) { + let type_args = if self.peek_kind() == Some(TokenKind::Lt) { self.parse_type_args()? } else { Vec::new() @@ -709,26 +709,16 @@ impl Parser { continue; } TokenKind::LBracket => { - let type_args = self.parse_type_args()?; - if self.peek_kind() == Some(TokenKind::LBrace) { - let path = match lhs { - Expr::Path(p) => p, - Expr::FieldAccess(ref fa) => self.field_access_to_path(fa)?, - _ => { - return Err(self.error_current( - "expected path before struct literal".to_string(), - )) - } - }; - lhs = self.parse_struct_literal(path, type_args)?; - continue; - } - if self.peek_kind() != Some(TokenKind::LParen) { - return Err(self.error_current( - "type arguments require a call or struct literal".to_string(), - )); - } - lhs = self.finish_call(lhs, type_args)?; + // With <> for generics, [] is unambiguously for indexing + let start = lhs.span().start; + self.bump(); // consume '[' + let index = self.parse_expr()?; + let end = self.expect(TokenKind::RBracket)?.span.end; + lhs = Expr::Index(IndexExpr { + object: Box::new(lhs), + index: Box::new(index), + span: Span::new(start, end), + }); continue; } TokenKind::Question => { @@ -745,6 +735,47 @@ impl Parser { } } + // Special handling for '<' which can be type arguments or less-than + if self.peek_kind() == Some(TokenKind::Lt) { + // Check if this looks like type arguments: path(args) or path{ ... } + if matches!(&lhs, Expr::Path(_) | Expr::FieldAccess(_)) { + // Peek ahead to see if content looks like a type + let looks_like_type = match self.peek_token(1).map(|t| &t.kind) { + Some(TokenKind::Ident) => { + if let Some(tok) = self.peek_token(1) { + let builtin_types = ["i32", "u32", "u8", "bool", "string", "unit"]; + tok.text.starts_with(|c: char| c.is_uppercase()) + || builtin_types.contains(&tok.text.as_str()) + } else { + false + } + } + Some(TokenKind::Star) => true, // pointer type like *u8 + _ => false, + }; + if looks_like_type { + let type_args = self.parse_type_args()?; + if self.peek_kind() == Some(TokenKind::LBrace) { + let path = match lhs { + Expr::Path(p) => p, + Expr::FieldAccess(ref fa) => self.field_access_to_path(fa)?, + _ => unreachable!(), + }; + lhs = self.parse_struct_literal(path, type_args)?; + continue; + } + if self.peek_kind() == Some(TokenKind::LParen) { + lhs = self.finish_call(lhs, type_args)?; + continue; + } + return Err(self.error_current( + "type arguments require a call or struct literal".to_string(), + )); + } + } + // Fall through to treat as less-than comparison + } + // Then, check for binary operators let op = match self.peek_kind() { Some(TokenKind::OrOr) => BinaryOp::Or, @@ -1069,9 +1100,9 @@ impl Parser { let path = self.parse_path()?; let mut args = Vec::new(); let mut end = path.span.end; - if self.peek_kind() == Some(TokenKind::LBracket) { + if self.peek_kind() == Some(TokenKind::Lt) { self.bump(); - if self.peek_kind() != Some(TokenKind::RBracket) { + if self.peek_kind() != Some(TokenKind::Gt) { loop { args.push(self.parse_type()?); if self.maybe_consume(TokenKind::Comma).is_none() { @@ -1079,7 +1110,7 @@ impl Parser { } } } - end = self.expect(TokenKind::RBracket)?.span.end; + end = self.expect(TokenKind::Gt)?.span.end; } let span = Span::new(path.span.start, end); Ok(Type::Path { path, args, span }) @@ -1140,12 +1171,12 @@ impl Parser { } fn parse_type_params(&mut self) -> Result, ParseError> { - if self.peek_kind() != Some(TokenKind::LBracket) { + if self.peek_kind() != Some(TokenKind::Lt) { return Ok(Vec::new()); } self.bump(); let mut params = Vec::new(); - if self.peek_kind() != Some(TokenKind::RBracket) { + if self.peek_kind() != Some(TokenKind::Gt) { loop { let ident = self.expect_ident()?; params.push(ident); @@ -1154,17 +1185,17 @@ impl Parser { } } } - self.expect(TokenKind::RBracket)?; + self.expect(TokenKind::Gt)?; Ok(params) } fn parse_type_args(&mut self) -> Result, ParseError> { - if self.peek_kind() != Some(TokenKind::LBracket) { + if self.peek_kind() != Some(TokenKind::Lt) { return Ok(Vec::new()); } self.bump(); let mut args = Vec::new(); - if self.peek_kind() != Some(TokenKind::RBracket) { + if self.peek_kind() != Some(TokenKind::Gt) { loop { args.push(self.parse_type()?); if self.maybe_consume(TokenKind::Comma).is_none() { @@ -1172,7 +1203,7 @@ impl Parser { } } } - self.expect(TokenKind::RBracket)?; + self.expect(TokenKind::Gt)?; Ok(args) } @@ -1305,6 +1336,7 @@ impl SpanExt for Expr { Expr::Call(call) => call.span, Expr::MethodCall(method_call) => method_call.span, Expr::FieldAccess(field) => field.span, + Expr::Index(index) => index.span, Expr::StructLiteral(lit) => lit.span, Expr::Unary(unary) => unary.span, Expr::Binary(binary) => binary.span, diff --git a/capc/src/typeck/check.rs b/capc/src/typeck/check.rs index 0ca72a8..c0c3208 100644 --- a/capc/src/typeck/check.rs +++ b/capc/src/typeck/check.rs @@ -1875,6 +1875,75 @@ pub(super) fn check_expr( } Ok(field_ty) } + Expr::Index(index_expr) => { + // Type check the object (must be string, Slice[T], or MutSlice[T]) + let object_ty = check_expr( + &index_expr.object, + functions, + scopes, + UseMode::Read, + recorder, + use_map, + struct_map, + enum_map, + stdlib, + ret_ty, + module_name, + type_params, + )?; + + // Type check the index (must be i32) + let index_ty = check_expr( + &index_expr.index, + functions, + scopes, + UseMode::Read, + recorder, + use_map, + struct_map, + enum_map, + stdlib, + ret_ty, + module_name, + type_params, + )?; + + if index_ty != Ty::Builtin(BuiltinType::I32) { + return Err(TypeError::new( + format!("index must be i32, found {:?}", index_ty), + index_expr.index.span(), + )); + } + + // Determine element type based on object type + match &object_ty { + Ty::Builtin(BuiltinType::String) => Ok(Ty::Builtin(BuiltinType::U8)), + Ty::Path(name, args) if name == "Slice" || name == "sys.buffer.Slice" => { + if args.len() == 1 { + Ok(args[0].clone()) + } else { + Err(TypeError::new( + "Slice requires exactly one type argument".to_string(), + index_expr.span, + )) + } + } + Ty::Path(name, args) if name == "MutSlice" || name == "sys.buffer.MutSlice" => { + if args.len() == 1 { + Ok(args[0].clone()) + } else { + Err(TypeError::new( + "MutSlice requires exactly one type argument".to_string(), + index_expr.span, + )) + } + } + _ => Err(TypeError::new( + format!("cannot index into type {:?}; only string, Slice[T], and MutSlice[T] are indexable", object_ty), + index_expr.span, + )), + } + } }?; recorder.record(expr, &ty); Ok(ty) diff --git a/capc/src/typeck/lower.rs b/capc/src/typeck/lower.rs index 69b6ab4..424fe24 100644 --- a/capc/src/typeck/lower.rs +++ b/capc/src/typeck/lower.rs @@ -953,6 +953,17 @@ fn lower_expr(expr: &Expr, ctx: &mut LoweringCtx, ret_ty: &Ty) -> Result { + let object = lower_expr(&index_expr.object, ctx, ret_ty)?; + let index = lower_expr(&index_expr.index, ctx, ret_ty)?; + + Ok(HirExpr::Index(crate::hir::HirIndex { + object: Box::new(object), + index: Box::new(index), + elem_ty: hir_ty, + span: index_expr.span, + })) + } } } diff --git a/capc/src/typeck/mod.rs b/capc/src/typeck/mod.rs index 142d76d..3443adb 100644 --- a/capc/src/typeck/mod.rs +++ b/capc/src/typeck/mod.rs @@ -1086,6 +1086,7 @@ impl SpanExt for Expr { Expr::Call(call) => call.span, Expr::MethodCall(method_call) => method_call.span, Expr::FieldAccess(field) => field.span, + Expr::Index(index) => index.span, Expr::StructLiteral(lit) => lit.span, Expr::Unary(unary) => unary.span, Expr::Binary(binary) => binary.span, diff --git a/capc/src/typeck/monomorphize.rs b/capc/src/typeck/monomorphize.rs index 425659c..2a97b23 100644 --- a/capc/src/typeck/monomorphize.rs +++ b/capc/src/typeck/monomorphize.rs @@ -638,6 +638,16 @@ impl MonoCtx { span: t.span, })) } + HirExpr::Index(idx) => { + let object = self.mono_expr(module, &idx.object, subs)?; + let index = self.mono_expr(module, &idx.index, subs)?; + Ok(HirExpr::Index(crate::hir::HirIndex { + object: Box::new(object), + index: Box::new(index), + elem_ty: self.mono_hir_type(module, &idx.elem_ty, subs)?, + span: idx.span, + })) + } } } diff --git a/capc/tests/run.rs b/capc/tests/run.rs index 83b8ac0..f517924 100644 --- a/capc/tests/run.rs +++ b/capc/tests/run.rs @@ -824,3 +824,31 @@ fn run_string_compare() { assert_eq!(code, 0); assert!(stdout.contains("string_compare ok"), "stdout was: {stdout:?}"); } + +#[test] +fn run_string_index() { + let out_dir = make_out_dir("string_index"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/string_index.cap", + ]); + assert_eq!(code, 0); + assert!(stdout.contains("string_index ok"), "stdout was: {stdout:?}"); +} + +#[test] +fn run_generic_and_index() { + let out_dir = make_out_dir("generic_and_index"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/generic_and_index.cap", + ]); + assert_eq!(code, 0); + assert!(stdout.contains("generic_and_index ok"), "stdout was: {stdout:?}"); +} diff --git a/examples/config_loader/config_loader.cap b/examples/config_loader/config_loader.cap index d1f3274..050a1cd 100644 --- a/examples/config_loader/config_loader.cap +++ b/examples/config_loader/config_loader.cap @@ -14,7 +14,7 @@ fn print_kv(c: Console, key: string, val: string) -> unit { c.println(val) } -fn parse_line(c: Console, alloc: Alloc, line: string) -> Result[unit, VecErr] { +fn parse_line(c: Console, alloc: Alloc, line: string) -> Result { if line.len() == 0 { return Ok(()) } @@ -32,7 +32,7 @@ fn parse_line(c: Console, alloc: Alloc, line: string) -> Result[unit, VecErr] { return Ok(()) } -fn parse_config(c: Console, alloc: Alloc, contents: string) -> Result[unit, VecErr] { +fn parse_config(c: Console, alloc: Alloc, contents: string) -> Result { let lines = contents.lines() let i = 0 while i < lines.len() { @@ -44,7 +44,7 @@ fn parse_config(c: Console, alloc: Alloc, contents: string) -> Result[unit, VecE return Ok(()) } -fn run(c: Console, alloc: Alloc, fs: ReadFS) -> Result[unit, FsErr] { +fn run(c: Console, alloc: Alloc, fs: ReadFS) -> Result { let contents = fs.read_to_string("app.conf")? match (parse_config(c, alloc, contents)) { Ok(_) => { diff --git a/examples/http_server/http_server.cap b/examples/http_server/http_server.cap index b71dd76..ed0e489 100644 --- a/examples/http_server/http_server.cap +++ b/examples/http_server/http_server.cap @@ -25,7 +25,7 @@ fn strip_query(raw_path: string) -> string { } } -fn sanitize_segment(parts: VecString, i: i32, acc: string, seg: string) -> Result[string, unit] { +fn sanitize_segment(parts: VecString, i: i32, acc: string, seg: string) -> Result { if (seg.len() == 0) { return sanitize_parts(parts, i + 1, acc) } @@ -41,7 +41,7 @@ fn sanitize_segment(parts: VecString, i: i32, acc: string, seg: string) -> Resul return sanitize_parts(parts, i + 1, fs::join(acc, seg)) } -fn sanitize_parts(parts: VecString, i: i32, acc: string) -> Result[string, unit] { +fn sanitize_parts(parts: VecString, i: i32, acc: string) -> Result { if (i >= parts.len()) { return Ok(acc) } @@ -51,7 +51,7 @@ fn sanitize_parts(parts: VecString, i: i32, acc: string) -> Result[string, unit] } } -fn sanitize_path(raw_path: string) -> Result[string, unit] { +fn sanitize_path(raw_path: string) -> Result { match (sanitize_parts(raw_path.split(47u8), 0, "")) { Ok(path) => { if (path.len() == 0) { @@ -63,7 +63,7 @@ fn sanitize_path(raw_path: string) -> Result[string, unit] { } } -fn parse_request_line(line: string) -> Result[string, unit] { +fn parse_request_line(line: string) -> Result { let parts = line.trim().split(32u8) match (parts.get(0)) { Ok(method) => { @@ -79,35 +79,35 @@ fn parse_request_line(line: string) -> Result[string, unit] { } } -fn parse_request_path(req: string) -> Result[string, unit] { +fn parse_request_path(req: string) -> Result { match (req.lines().get(0)) { Ok(line) => { return parse_request_line(line) } Err(_) => { return Err(()) } } } -fn respond_ok(conn: &TcpConn, body: string) -> Result[unit, NetErr] { +fn respond_ok(conn: &TcpConn, body: string) -> Result { conn.write("HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\n\r\n")? conn.write(body)? return Ok(()) } -fn respond_not_found(conn: &TcpConn) -> Result[unit, NetErr] { +fn respond_not_found(conn: &TcpConn) -> Result { return conn.write("HTTP/1.0 404 Not Found\r\nContent-Type: text/plain\r\n\r\nnot found\n") } -fn respond_bad_request(conn: &TcpConn) -> Result[unit, NetErr] { +fn respond_bad_request(conn: &TcpConn) -> Result { return conn.write("HTTP/1.0 400 Bad Request\r\nContent-Type: text/plain\r\n\r\nbad request\n") } -fn handle_request(conn: &TcpConn, readfs: ReadFS, path: string) -> Result[unit, NetErr] { +fn handle_request(conn: &TcpConn, readfs: ReadFS, path: string) -> Result { match (readfs.read_to_string(path)) { Ok(body) => { return respond_ok(conn, body) } Err(_) => { return respond_not_found(conn) } } } -fn serve_once(c: Console, net: Net, readfs: ReadFS) -> Result[unit, NetErr] { +fn serve_once(c: Console, net: Net, readfs: ReadFS) -> Result { let listener = net.listen("127.0.0.1", 8080)? let conn = listener.accept()? let req = conn.read(4096)? diff --git a/examples/sort/sort.cap b/examples/sort/sort.cap index 8f59920..9267d72 100644 --- a/examples/sort/sort.cap +++ b/examples/sort/sort.cap @@ -79,7 +79,7 @@ fn sort_indices(lines: VecString, indices: VecI32) -> unit { } } -fn run(c: Console, alloc: Alloc, input: Stdin) -> Result[unit, io::IoErr] { +fn run(c: Console, alloc: Alloc, input: Stdin) -> Result { let contents = input.read_to_string()? let lines = contents.lines() let n = lines.len() diff --git a/examples/uniq/uniq.cap b/examples/uniq/uniq.cap index 9b9bb52..fb9c84f 100644 --- a/examples/uniq/uniq.cap +++ b/examples/uniq/uniq.cap @@ -23,7 +23,7 @@ fn should_print(lines: VecString, i: i32) -> bool { return !prev.eq(curr) } -fn run(c: Console, alloc: Alloc, input: Stdin) -> Result[unit, io::IoErr] { +fn run(c: Console, alloc: Alloc, input: Stdin) -> Result { let contents = input.read_to_string()? let lines = contents.lines() let i = 0 diff --git a/stdlib/sys/args.cap b/stdlib/sys/args.cap index 5cf7d17..64703fd 100644 --- a/stdlib/sys/args.cap +++ b/stdlib/sys/args.cap @@ -12,7 +12,7 @@ impl Args { return 0 } - pub fn at(self, index: i32) -> Result[string, ArgsErr] { + pub fn at(self, index: i32) -> Result { return Err(ArgsErr::OutOfRange) } } diff --git a/stdlib/sys/buffer.cap b/stdlib/sys/buffer.cap index 469a549..ef92dcd 100644 --- a/stdlib/sys/buffer.cap +++ b/stdlib/sys/buffer.cap @@ -4,8 +4,8 @@ use sys::vec pub copy opaque struct Alloc pub copy opaque struct Buffer -pub copy opaque struct Slice[T] -pub copy opaque struct MutSlice[T] +pub copy opaque struct Slice +pub copy opaque struct MutSlice pub enum AllocErr { Oom @@ -28,15 +28,15 @@ impl Alloc { return () } - pub fn slice_from_ptr(self, ptr: *u8, len: i32) -> Slice[u8] { + pub fn slice_from_ptr(self, ptr: *u8, len: i32) -> Slice { return () } - pub fn mut_slice_from_ptr(self, ptr: *u8, len: i32) -> MutSlice[u8] { + pub fn mut_slice_from_ptr(self, ptr: *u8, len: i32) -> MutSlice { return () } - pub fn buffer_new(self, initial_len: i32) -> Result[Buffer, AllocErr] { + pub fn buffer_new(self, initial_len: i32) -> Result { return Err(AllocErr::Oom) } @@ -74,11 +74,11 @@ impl Buffer { return 0 } - pub fn push(self, x: u8) -> Result[unit, AllocErr] { + pub fn push(self, x: u8) -> Result { return Err(AllocErr::Oom) } - pub fn extend(self, data: Slice[u8]) -> Result[unit, AllocErr] { + pub fn extend(self, data: Slice) -> Result { return Err(AllocErr::Oom) } @@ -86,16 +86,16 @@ impl Buffer { return false } - pub fn as_slice(self) -> Slice[u8] { + pub fn as_slice(self) -> Slice { return () } - pub fn as_mut_slice(self) -> MutSlice[u8] { + pub fn as_mut_slice(self) -> MutSlice { return () } } -impl Slice[u8] { +impl Slice { pub fn len(self) -> i32 { return 0 } @@ -105,7 +105,7 @@ impl Slice[u8] { } } -impl MutSlice[u8] { +impl MutSlice { pub fn at(self, i: i32) -> u8 { return 0 } diff --git a/stdlib/sys/fs.cap b/stdlib/sys/fs.cap index 0ecf095..900c18c 100644 --- a/stdlib/sys/fs.cap +++ b/stdlib/sys/fs.cap @@ -11,15 +11,15 @@ pub linear capability struct FileRead pub enum FsErr { NotFound, PermissionDenied, InvalidPath, IoError } impl ReadFS { - pub fn read_to_string(self, path: string) -> Result[string, FsErr] { + pub fn read_to_string(self, path: string) -> Result { return () } - pub fn read_bytes(self, path: string) -> Result[vec::VecU8, FsErr] { + pub fn read_bytes(self, path: string) -> Result { return Err(FsErr::IoError) } - pub fn list_dir(self, path: string) -> Result[vec::VecString, FsErr] { + pub fn list_dir(self, path: string) -> Result { return Err(FsErr::IoError) } @@ -51,11 +51,11 @@ impl Dir { return () } - pub fn read_bytes(self, name: string) -> Result[vec::VecU8, FsErr] { + pub fn read_bytes(self, name: string) -> Result { return Err(FsErr::IoError) } - pub fn list_dir(self) -> Result[vec::VecString, FsErr] { + pub fn list_dir(self) -> Result { return Err(FsErr::IoError) } @@ -63,7 +63,7 @@ impl Dir { return false } - pub fn read_to_string(self, name: string) -> Result[string, FsErr] { + pub fn read_to_string(self, name: string) -> Result { let file = self.open_read(name) return file.read_to_string() } @@ -74,7 +74,7 @@ impl Dir { } impl FileRead { - pub fn read_to_string(self) -> Result[string, FsErr] { + pub fn read_to_string(self) -> Result { return () } diff --git a/stdlib/sys/net.cap b/stdlib/sys/net.cap index 9bbcf76..ca9cd9f 100644 --- a/stdlib/sys/net.cap +++ b/stdlib/sys/net.cap @@ -12,17 +12,17 @@ pub enum NetErr { } impl Net { - pub fn listen(self, host: string, port: i32) -> Result[TcpListener, NetErr] { + pub fn listen(self, host: string, port: i32) -> Result { return Err(NetErr::IoError) } - pub fn connect(self, host: string, port: i32) -> Result[TcpConn, NetErr] { + pub fn connect(self, host: string, port: i32) -> Result { return Err(NetErr::IoError) } } impl TcpListener { - pub fn accept(self) -> Result[TcpConn, NetErr] { + pub fn accept(self) -> Result { return Err(NetErr::IoError) } @@ -32,15 +32,15 @@ impl TcpListener { } impl TcpConn { - pub fn read_to_string(self: &TcpConn) -> Result[string, NetErr] { + pub fn read_to_string(self: &TcpConn) -> Result { return Err(NetErr::IoError) } - pub fn read(self: &TcpConn, max_size: i32) -> Result[string, NetErr] { + pub fn read(self: &TcpConn, max_size: i32) -> Result { return Err(NetErr::IoError) } - pub fn write(self: &TcpConn, data: string) -> Result[unit, NetErr] { + pub fn write(self: &TcpConn, data: string) -> Result { return Err(NetErr::IoError) } diff --git a/stdlib/sys/stdin.cap b/stdlib/sys/stdin.cap index 38faf70..e798b6c 100644 --- a/stdlib/sys/stdin.cap +++ b/stdlib/sys/stdin.cap @@ -6,7 +6,7 @@ use sys::io pub capability struct Stdin impl Stdin { - pub fn read_to_string(self) -> Result[string, io::IoErr] { + pub fn read_to_string(self) -> Result { return Err(io::IoErr::IoError) } } diff --git a/stdlib/sys/string.cap b/stdlib/sys/string.cap index e5f9c3e..b90721e 100644 --- a/stdlib/sys/string.cap +++ b/stdlib/sys/string.cap @@ -17,12 +17,12 @@ impl string { } /// Intrinsic; implemented by the runtime. - pub fn as_slice(self) -> Slice[u8] { + pub fn as_slice(self) -> Slice { return () } /// bytes() is an alias for as_slice(). - pub fn bytes(self) -> Slice[u8] { + pub fn bytes(self) -> Slice { return self.as_slice() } diff --git a/stdlib/sys/vec.cap b/stdlib/sys/vec.cap index 71c15c6..8bb44c6 100644 --- a/stdlib/sys/vec.cap +++ b/stdlib/sys/vec.cap @@ -17,19 +17,19 @@ impl VecU8 { return 0 } - pub fn get(self, i: i32) -> Result[u8, VecErr] { + pub fn get(self, i: i32) -> Result { return Err(VecErr::OutOfRange) } - pub fn set(self, i: i32, x: u8) -> Result[unit, VecErr] { + pub fn set(self, i: i32, x: u8) -> Result { return Err(VecErr::OutOfRange) } - pub fn push(self, x: u8) -> Result[unit, buffer::AllocErr] { + pub fn push(self, x: u8) -> Result { return Err(buffer::AllocErr::Oom) } - pub fn extend(self, other: VecU8) -> Result[unit, buffer::AllocErr] { + pub fn extend(self, other: VecU8) -> Result { return Err(buffer::AllocErr::Oom) } @@ -41,15 +41,15 @@ impl VecU8 { return () } - pub fn slice(self, start: i32, len: i32) -> Result[Slice[u8], VecErr] { + pub fn slice(self, start: i32, len: i32) -> Result, VecErr> { return Err(VecErr::OutOfRange) } - pub fn pop(self) -> Result[u8, VecErr] { + pub fn pop(self) -> Result { return Err(VecErr::Empty) } - pub fn as_slice(self) -> Slice[u8] { + pub fn as_slice(self) -> Slice { return () } } @@ -59,19 +59,19 @@ impl VecI32 { return 0 } - pub fn get(self, i: i32) -> Result[i32, VecErr] { + pub fn get(self, i: i32) -> Result { return Err(VecErr::OutOfRange) } - pub fn set(self, i: i32, x: i32) -> Result[unit, VecErr] { + pub fn set(self, i: i32, x: i32) -> Result { return Err(VecErr::OutOfRange) } - pub fn push(self, x: i32) -> Result[unit, buffer::AllocErr] { + pub fn push(self, x: i32) -> Result { return Err(buffer::AllocErr::Oom) } - pub fn extend(self, other: VecI32) -> Result[unit, buffer::AllocErr] { + pub fn extend(self, other: VecI32) -> Result { return Err(buffer::AllocErr::Oom) } @@ -83,7 +83,7 @@ impl VecI32 { return () } - pub fn pop(self) -> Result[i32, VecErr] { + pub fn pop(self) -> Result { return Err(VecErr::Empty) } } @@ -93,19 +93,19 @@ impl VecString { return 0 } - pub fn get(self, i: i32) -> Result[string, VecErr] { + pub fn get(self, i: i32) -> Result { return Err(VecErr::OutOfRange) } - pub fn push(self, x: string) -> Result[unit, buffer::AllocErr] { + pub fn push(self, x: string) -> Result { return Err(buffer::AllocErr::Oom) } - pub fn extend(self, other: VecString) -> Result[unit, buffer::AllocErr] { + pub fn extend(self, other: VecString) -> Result { return Err(buffer::AllocErr::Oom) } - pub fn pop(self) -> Result[string, VecErr] { + pub fn pop(self) -> Result { return Err(VecErr::Empty) } } diff --git a/tests/programs/generic_and_index.cap b/tests/programs/generic_and_index.cap new file mode 100644 index 0000000..40a0907 --- /dev/null +++ b/tests/programs/generic_and_index.cap @@ -0,0 +1,47 @@ +module generic_and_index +use sys::system +use sys::console + +struct Wrapper { + value: T, +} + +fn identity(x: T) -> T { + return x +} + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + + // Test generics with <> + let w = Wrapper{ value: 42 } + let v = identity(w.value) + if (v == 42) { + c.println("generic ok") + } + + // Test indexing with [] + let s = "hello" + let first = s[0] + if (first == 104u8) { + c.println("index ok") + } + + // Test comparison with < (should not be confused with generics) + let x = 5 + let y = 10 + if (x < y) { + c.println("compare ok") + } + + // Test both in same expression context + let s2 = "world" + let idx = identity(1) + let ch = s2[idx] + if (ch == 111u8) { // 'o' is 111 + c.println("combo ok") + } + + c.println("generic_and_index ok") + return 0 +} diff --git a/tests/programs/generics_basic.cap b/tests/programs/generics_basic.cap index 56ba111..bfa59ce 100644 --- a/tests/programs/generics_basic.cap +++ b/tests/programs/generics_basic.cap @@ -3,18 +3,18 @@ module generics_basic use sys::console use sys::system -struct Box[T] { +struct Box { value: T, } -fn id[T](value: T) -> T { +fn id(value: T) -> T { return value } pub fn main(rc: RootCap) -> i32 { let c = rc.mint_console() - let b = Box[i32]{ value: 42 } - let v = id[i32](b.value) + let b = Box{ value: 42 } + let v = id(b.value) c.print_i32(v) return 0 } diff --git a/tests/programs/result_construct.cap b/tests/programs/result_construct.cap index 25e9b03..31a18da 100644 --- a/tests/programs/result_construct.cap +++ b/tests/programs/result_construct.cap @@ -35,7 +35,7 @@ pub fn main(rc: RootCap) -> i32 { return 0 } -fn make(ok: bool) -> Result[string, i32] { +fn make(ok: bool) -> Result { if (ok) { return Ok("hello") } else { diff --git a/tests/programs/should_fail_capability_borrow_return_result.cap b/tests/programs/should_fail_capability_borrow_return_result.cap index 6dc0357..48c48a5 100644 --- a/tests/programs/should_fail_capability_borrow_return_result.cap +++ b/tests/programs/should_fail_capability_borrow_return_result.cap @@ -4,7 +4,7 @@ module should_fail_capability_borrow_return_result capability struct Cap impl Cap { - pub fn try_dup(self: &Cap) -> Result[Cap, i32] { + pub fn try_dup(self: &Cap) -> Result { return Ok(Cap{}) } } diff --git a/tests/programs/should_fail_match_result_non_exhaustive.cap b/tests/programs/should_fail_match_result_non_exhaustive.cap index ab35377..283e7e1 100644 --- a/tests/programs/should_fail_match_result_non_exhaustive.cap +++ b/tests/programs/should_fail_match_result_non_exhaustive.cap @@ -1,6 +1,6 @@ module should_fail_match_result_non_exhaustive -fn make() -> Result[i32, i32] { +fn make() -> Result { return Ok(1) } diff --git a/tests/programs/should_fail_result_unwrap_or_mismatch.cap b/tests/programs/should_fail_result_unwrap_or_mismatch.cap index b66d04f..12fb500 100644 --- a/tests/programs/should_fail_result_unwrap_or_mismatch.cap +++ b/tests/programs/should_fail_result_unwrap_or_mismatch.cap @@ -1,6 +1,6 @@ module should_fail_result_unwrap_or_mismatch -fn make() -> Result[i32, i32] { +fn make() -> Result { return Ok(1) } diff --git a/tests/programs/should_fail_try_err_mismatch.cap b/tests/programs/should_fail_try_err_mismatch.cap index 5c19036..9cf7167 100644 --- a/tests/programs/should_fail_try_err_mismatch.cap +++ b/tests/programs/should_fail_try_err_mismatch.cap @@ -1,11 +1,11 @@ package safe module should_fail_try_err_mismatch -fn make() -> Result[i32, bool] { +fn make() -> Result { return Err(false) } -fn use_try() -> Result[i32, i32] { +fn use_try() -> Result { let v = make()? return Ok(v) } diff --git a/tests/programs/should_fail_try_non_result.cap b/tests/programs/should_fail_try_non_result.cap index 1d5ad1b..3a60a16 100644 --- a/tests/programs/should_fail_try_non_result.cap +++ b/tests/programs/should_fail_try_non_result.cap @@ -1,7 +1,7 @@ package safe module should_fail_try_non_result -fn make() -> Result[i32, i32] { +fn make() -> Result { return Ok(1) } diff --git a/tests/programs/should_pass_result_unwrap_err_or.cap b/tests/programs/should_pass_result_unwrap_err_or.cap index 405652d..2b2c8ff 100644 --- a/tests/programs/should_pass_result_unwrap_err_or.cap +++ b/tests/programs/should_pass_result_unwrap_err_or.cap @@ -1,10 +1,10 @@ module should_pass_result_unwrap_err_or -fn make_ok() -> Result[i32, i32] { +fn make_ok() -> Result { return Ok(1) } -fn make_err() -> Result[i32, i32] { +fn make_err() -> Result { return Err(2) } diff --git a/tests/programs/should_pass_result_unwrap_or.cap b/tests/programs/should_pass_result_unwrap_or.cap index 952b89a..f4ff486 100644 --- a/tests/programs/should_pass_result_unwrap_or.cap +++ b/tests/programs/should_pass_result_unwrap_or.cap @@ -1,10 +1,10 @@ module should_pass_result_unwrap_or -fn make_ok() -> Result[i32, i32] { +fn make_ok() -> Result { return Ok(1) } -fn make_err() -> Result[i32, i32] { +fn make_err() -> Result { return Err(2) } diff --git a/tests/programs/should_pass_try_question.cap b/tests/programs/should_pass_try_question.cap index ef99399..b8e1ed4 100644 --- a/tests/programs/should_pass_try_question.cap +++ b/tests/programs/should_pass_try_question.cap @@ -1,11 +1,11 @@ package safe module should_pass_try_question -fn make(x: i32) -> Result[i32, i32] { +fn make(x: i32) -> Result { return Ok(x) } -fn use_try() -> Result[i32, i32] { +fn use_try() -> Result { let v = make(7)? return Ok(v) } diff --git a/tests/programs/slice_safe.cap b/tests/programs/slice_safe.cap index f9ae391..d3e8d95 100644 --- a/tests/programs/slice_safe.cap +++ b/tests/programs/slice_safe.cap @@ -2,7 +2,7 @@ package safe module slice_safe use sys::buffer -fn parse_bytes(data: Slice[u8]) -> i32 { +fn parse_bytes(data: Slice) -> i32 { return 0 } diff --git a/tests/programs/string_index.cap b/tests/programs/string_index.cap new file mode 100644 index 0000000..3c11231 --- /dev/null +++ b/tests/programs/string_index.cap @@ -0,0 +1,36 @@ +module string_index +use sys::system +use sys::console + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + + // Test basic string indexing + let s = "hello" + let first = s[0] + if (first == 104u8) { // 'h' is 104 + c.println("first ok") + } + + // Test middle character + let middle = s[2] + if (middle == 108u8) { // 'l' is 108 + c.println("middle ok") + } + + // Test last character + let last = s[4] + if (last == 111u8) { // 'o' is 111 + c.println("last ok") + } + + // Test indexing with variable + let i = 1 + let at_i = s[i] + if (at_i == 101u8) { // 'e' is 101 + c.println("var index ok") + } + + c.println("string_index ok") + return 0 +} diff --git a/tests/programs/untrusted_logs.cap b/tests/programs/untrusted_logs.cap index 95ce8eb..dcf3e82 100644 --- a/tests/programs/untrusted_logs.cap +++ b/tests/programs/untrusted_logs.cap @@ -1,7 +1,7 @@ module untrusted_logs use sys::fs -pub fn read_log(dir: Dir) -> Result[string, FsErr] { +pub fn read_log(dir: Dir) -> Result { let file = dir.open_read("app.log") return file.read_to_string() } From aa09a93375540dd687f701f42b5785422e87e417 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Wed, 31 Dec 2025 13:06:58 -0800 Subject: [PATCH 07/30] check tests properly --- tests/programs/generic_and_index.cap | 24 ++++++++++++++++-------- tests/programs/string_index.cap | 24 ++++++++++++++++-------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/tests/programs/generic_and_index.cap b/tests/programs/generic_and_index.cap index 40a0907..9e81080 100644 --- a/tests/programs/generic_and_index.cap +++ b/tests/programs/generic_and_index.cap @@ -16,31 +16,39 @@ pub fn main(rc: RootCap) -> i32 { // Test generics with <> let w = Wrapper{ value: 42 } let v = identity(w.value) - if (v == 42) { - c.println("generic ok") + if (v != 42) { + c.println("FAIL: generic") + return 1 } + c.println("generic ok") // Test indexing with [] let s = "hello" let first = s[0] - if (first == 104u8) { - c.println("index ok") + if (first != 104u8) { // 'h' is 104 + c.println("FAIL: index") + return 1 } + c.println("index ok") // Test comparison with < (should not be confused with generics) let x = 5 let y = 10 - if (x < y) { - c.println("compare ok") + if (x >= y) { + c.println("FAIL: compare") + return 1 } + c.println("compare ok") // Test both in same expression context let s2 = "world" let idx = identity(1) let ch = s2[idx] - if (ch == 111u8) { // 'o' is 111 - c.println("combo ok") + if (ch != 111u8) { // 'o' is 111 + c.println("FAIL: combo") + return 1 } + c.println("combo ok") c.println("generic_and_index ok") return 0 diff --git a/tests/programs/string_index.cap b/tests/programs/string_index.cap index 3c11231..992f3f6 100644 --- a/tests/programs/string_index.cap +++ b/tests/programs/string_index.cap @@ -8,28 +8,36 @@ pub fn main(rc: RootCap) -> i32 { // Test basic string indexing let s = "hello" let first = s[0] - if (first == 104u8) { // 'h' is 104 - c.println("first ok") + if (first != 104u8) { // 'h' is 104 + c.println("FAIL: first") + return 1 } + c.println("first ok") // Test middle character let middle = s[2] - if (middle == 108u8) { // 'l' is 108 - c.println("middle ok") + if (middle != 108u8) { // 'l' is 108 + c.println("FAIL: middle") + return 1 } + c.println("middle ok") // Test last character let last = s[4] - if (last == 111u8) { // 'o' is 111 - c.println("last ok") + if (last != 111u8) { // 'o' is 111 + c.println("FAIL: last") + return 1 } + c.println("last ok") // Test indexing with variable let i = 1 let at_i = s[i] - if (at_i == 101u8) { // 'e' is 101 - c.println("var index ok") + if (at_i != 101u8) { // 'e' is 101 + c.println("FAIL: var index") + return 1 } + c.println("var index ok") c.println("string_index ok") return 0 From 7555204c195bb41b5707b5ce8990a03f1b985860 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Wed, 31 Dec 2025 16:41:53 -0800 Subject: [PATCH 08/30] Change default target name from capable --- capc/src/main.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/capc/src/main.rs b/capc/src/main.rs index dc1d567..a33ddbd 100644 --- a/capc/src/main.rs +++ b/capc/src/main.rs @@ -188,7 +188,10 @@ fn build_binary( ) .map_err(|err| miette!("failed to write stub: {err}"))?; - let out_path = out.unwrap_or_else(|| build_dir.join("capable")); + let out_path = out.unwrap_or_else(|| { + let name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("a.out"); + build_dir.join(name) + }); let runtime_lib_dir = workspace_root.join("target").join("debug"); let mut rustc = std::process::Command::new("rustc"); rustc From f64de33e43f88c1bf229e46297510c1f2a171435 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Wed, 31 Dec 2025 16:43:18 -0800 Subject: [PATCH 09/30] Remove outdated plan file --- PLAN.md | 85 --------------------------------------------------------- 1 file changed, 85 deletions(-) delete mode 100644 PLAN.md diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 5801b38..0000000 --- a/PLAN.md +++ /dev/null @@ -1,85 +0,0 @@ -# Capable Roadmap (Austral-Aligned) - -This plan keeps Capable aligned with Austral’s goals while staying pragmatic about implementation order. Each phase ends with real examples and tests. - -## Completed (Recently) - -- Borrow‑lite locals with non‑escaping rules and error coverage. -- Capability hardening: root normalization, symlink escape guard, and tests. -- Linear control‑flow checks with attenuation/untrusted tests. -- Match exhaustiveness for `bool`, `Result`, and enums. -- Safe arithmetic with trap‑on‑overflow and modular helpers. -- Result helpers (`unwrap_or`, `unwrap_err_or`) with tests. -- Explicitness policy: shadowing rejected, unsigned ordering enforced. -- Docs and workflow: architecture/policy/samples, justfile tasks, tutorial updates. - -## Priority 1: Backend Cleanup (Highest) - -Goal: keep the compiler/backend simple and trustworthy so future features don’t compound complexity. - -- Typed lowering bridge: codegen should consume typed HIR directly, no re‑inferring shapes. -- Shared type utilities: centralize numeric/orderable/unsigned checks to avoid drift. -- Clean error plumbing: consistent spans/messages across parse → typeck → lower → codegen. -- HIR simplification: fully resolved, no spans, no unresolved paths. -- Runtime handle conventions: unify tables + lifecycle patterns to prep for drop/close. - -Deliverables: -- Typed lowering bridge complete; codegen does not re‑infer type shapes. -- Error messages consistently point at user code spans. -- Small cleanup PRs that reduce “special cases” in codegen. - -## Phase 2: Core Discipline - -Goal: make capability/linearity guarantees reliable without full borrow checking. - -- Linear/affine rules are complete and consistent across control flow. -- Capability attenuation is enforced (consume `self` on narrowing APIs). -- Borrow‑lite: allow `&T` for locals with strict non‑escaping rules. -- Stdlib/runtime tightenings: ensure cap states are checked and canonicalization is consistent. - -Deliverables: -- Tests for linear “must consume” on all paths (if/match/loop). -- Tests for borrow‑lite locals (read‑only, non‑escaping). -- At least two sample programs that stress capabilities and linear resources. - -## Phase 3: Reliability and Predictability - -Goal: make the language predictable under failure and in large programs. - -- Exhaustive `match` checking for enums/Result. -- Safe arithmetic: explicit trap‑on‑overflow vs modular operations. -- Enforce explicitness policies (no implicit conversions, no implicit calls). -- Decide and enforce shadowing policy (prefer “no shadowing” for clarity). - -Deliverables: -- Exhaustiveness tests with helpful diagnostics. -- Arithmetic tests covering overflow behavior. -- Lint/error surfaces for disallowed implicitness. - -## Phase 4: Expressiveness Without Magic - -Goal: add power without hidden control flow or inference. - -- Typeclasses (bounded ad‑hoc polymorphism). -- Better collections/strings APIs for real programs. -- Error ergonomics (`Result` helpers like `map`, `map_err`, `and_then`). - -Deliverables: -- Typeclass MVP with a small stdlib surface (e.g., `Eq`, `Show`). -- Revised sample programs that feel “natural” without macros or inference. - -## Next Horizon - -- Borrow‑lite polish: allow `&T` forwarding through helpers; tighten diagnostics. -- Capability ergonomics: `&self` methods where attenuation should not consume parents. -- Resource lifecycle: decide whether `drop()` stays a sink or add explicit close/free APIs. -- Runtime error clarity: structured error codes/messages for FS/alloc paths. -- Sample apps: config loader with validation, a log tailer, and a dir walker. - -## Ongoing Non‑Goals - -Keep these as invariants: -- No GC, no exceptions, no implicit conversions/calls, no macros, no reflection. -- No global state; no subtyping. -- No uninitialized variables. -- No first‑class async. From 4bd4f97b8b482f3443beb38e6771cdf99bc7de37 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Wed, 31 Dec 2025 16:43:35 -0800 Subject: [PATCH 10/30] remove outdated progress file --- PROGRESS.md | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 PROGRESS.md diff --git a/PROGRESS.md b/PROGRESS.md deleted file mode 100644 index fb99c71..0000000 --- a/PROGRESS.md +++ /dev/null @@ -1,3 +0,0 @@ -# Progress Log - -This file is now intentionally minimal. Recent progress is tracked in `PLAN.md`. From 529b9edf45ce5c0235f29b903d1a14851fbecd2b Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Wed, 31 Dec 2025 16:43:46 -0800 Subject: [PATCH 11/30] move docs --- ATTENUATION.md | 95 ---------------- TUTORIAL.md | 302 ------------------------------------------------- 2 files changed, 397 deletions(-) delete mode 100644 ATTENUATION.md delete mode 100644 TUTORIAL.md diff --git a/ATTENUATION.md b/ATTENUATION.md deleted file mode 100644 index 4f639a9..0000000 --- a/ATTENUATION.md +++ /dev/null @@ -1,95 +0,0 @@ -Yep — Austral’s capability system is explicitly hierarchical / attenuating: you derive a narrower capability only by providing proof of a broader one (e.g., Filesystem -> Path(root) -> Path(subdir/file)), and you can’t “go back up.”  - -Here’s a clean way to model the same thing in capc with what you’ve already built (affine locals + “move whole struct on affine-field move”). - -1) Make “caps” affine + non-forgeable - -Use `capability struct` as “this is a capability/handle.” That gives you: - • cannot copy (your Token test) - • cannot fabricate (capability types are opaque: no public fields / no user construction outside the defining module) - -Capability types default to affine unless marked `copy` or `linear`. - -2) Encode attenuation as “only downward constructors” - -Design the stdlib so the only way to get a capability is from a stronger one, and the API only lets you narrow. - -A minimal pattern: - -capability struct RootCap -capability struct Filesystem -capability struct Dir -capability struct FileRead -capability struct FileWrite - -module fs { - // mint broad cap from root (borrow root so you can mint others too) - pub fn filesystem(root: &RootCap) -> Filesystem - - // attenuate: Filesystem -> Dir(root) - pub fn root_dir(fs: &Filesystem) -> Dir - - // attenuate: Dir -> Dir(subdir) (consume Dir to avoid “backtracking” unless you re-mint) - pub fn subdir(dir: Dir, name: string) -> Dir - - // attenuate: Dir -> File caps - pub fn open_read(dir: &Dir, name: string) -> FileRead - pub fn open_write(dir: &Dir, name: string) -> FileWrite - - pub fn read(f: FileRead) -> string - pub fn write(f: FileWrite, s: string) -> unit -} - -Notes: - • Use borrows (&T) for “authority checks / minting” and moves (T) for “consuming path-like capabilities.” That matches the Austral-style feel: you can derive from a reference, but the derived things themselves are linear-ish.  - • If you don’t have & in the language yet, you can still do attenuation with moves only, but it gets annoying (you’ll constantly lose the parent cap). Borrowing is the ergonomic escape hatch that doesn’t require Rust’s full borrow checker if you keep it simple (read-only refs, no aliasing mutation). - -3) Why your Token test is not “too restrictive” - -For a capability/handle, this is exactly the point: - -capability struct Token - -pub fn main() -> i32 { - let t = Token{} - let u = t - let v = t // should fail - return 0 -} - -It’s only “too restrictive” if Token is meant to be data (copyable value). In that case, don’t make it capability/opaque, or give it a non-affine representation (plain struct of copyable fields). - -4) The tests that actually prove “caps can’t be reused” - -You want tests that cover all the ways a user might accidentally duplicate authority: - -A. Local moves (baseline) - • move into another binding (your Token test) - • move via return - • move via function arg / method receiver - -B. Control-flow joins (the hard part you just implemented) - • if: move in one branch then use after → should fail - • if: move in both branches → after join it’s moved → use after should fail - • match: same as if but N arms - • while: moving an outer affine inside loop → should fail (your conservative rule) - -C. Composition (affine-by-containment) - • struct containing an affine field becomes affine (you already enforce by is_affine_type) - • enum payload containing affine becomes affine - -D. Projection / field access (the “sneaky dup”) - • your Holder{ cap: Cap } test is perfect: - • let a = h.cap; let b = h.cap; must fail - • also add nested: h.inner.cap and h.cap.subcap shapes (to exercise your leftmost_local_in_chain) - -E. Attenuation correctness (when you add it) - • can’t call a privileged API without the right capability type (type mismatch) - • can’t widen: there should be no function that maps Dir -> Filesystem in safe stdlib - • can’t mint without root: fs::filesystem(&fake_root) should be impossible because you can’t fabricate RootCap - -If you want one “big picture” test that demonstrates the point of all this: write a tiny “untrusted dependency” module that only receives a Dir (logs directory), and verify it cannot read /etc/passwd because it can’t obtain a Filesystem or a parent path. - -⸻ - -If you tell me what your reference/borrow syntax is (or if it exists yet), I can translate the attenuation API into exactly your AST/stdlib conventions and suggest 5–10 concrete .cap test programs (pass + fail) that lock the model in. diff --git a/TUTORIAL.md b/TUTORIAL.md deleted file mode 100644 index 6e6722a..0000000 --- a/TUTORIAL.md +++ /dev/null @@ -1,302 +0,0 @@ -# Capable in 15 Minutes - -Capable is a small capability-secure systems language. The main idea: authority is a value. If you didn't receive a capability, you can't do the thing. - -This tutorial is a quick tour of the current language slice and the capability model. - -## 1) Hello, console - -```cap -module hello -use sys::system - -pub fn main(rc: RootCap) -> i32 { - let c = rc.mint_console() - c.println("hello") - return 0 -} -``` - -`RootCap` is the root authority passed to `main`. It can mint narrower capabilities (console, filesystem, etc.). - -## 2) Basic syntax - -```cap -module basics - -pub fn add(a: i32, b: i32) -> i32 { - return a + b -} - -pub fn main() -> i32 { - let x = 1 - let y: i32 = 2 - if (x < y) { - return add(x, y) - } else { - return 0 - } -} -``` - -- Statements: `let`, assignment, `if`, `while`, `return`, `match`. -- Expressions: literals, calls, binary ops, unary ops, method calls. -- Modules + imports: `module ...` and `use ...` (aliases by last path segment). -- If a function returns `unit`, you can omit the `-> unit` annotation. -- Integer arithmetic traps on overflow. -- Variable shadowing is not allowed. - -## 3) Structs and enums - -```cap -module types - -struct Pair { left: i32, right: i32 } - -enum Color { Red, Green, Blue } -``` - -Structs and enums are nominal types. Enums are currently unit variants only. - -## 4) Methods - -Methods are defined in `impl` blocks and lower to `Type__method` at compile time. - -```cap -module methods - -struct Pair { left: i32, right: i32 } - -impl Pair { - pub fn sum(self) -> i32 { return self.left + self.right } - pub fn add(self, x: i32) -> i32 { return self.sum() + x } - pub fn peek(self: &Pair) -> i32 { return self.left } -} -``` - -Method receivers can be `self` (move) or `self: &T` (borrow‑lite, read‑only). - -## 5) Results, match, and `?` - -```cap -module results - -pub fn main() -> i32 { - let ok: Result[i32, i32] = Ok(10) - match ok { - Ok(x) => { return x } - Err(e) => { return 0 } - } -} -``` - -`Result[T, E]` is the only generic type today and is special-cased by the compiler. - -Inside a function that returns `Result`, you can use `?` to unwrap or return early: - -```cap -module results_try - -fn read_value() -> Result[i32, i32] { - return Ok(7) -} - -fn use_value() -> Result[i32, i32] { - let v = read_value()? - return Ok(v + 1) -} -``` - -You can also unwrap with defaults: - -```cap -let v = make().unwrap_or(0) -let e = make().unwrap_err_or(0) -``` - -Matches must be exhaustive; use `_` to cover the rest: - -```cap -match flag { - true => { } - false => { } -} -``` - -## 6) Capabilities and attenuation - -Capabilities live in `sys.*` and are declared with the `capability` keyword (capability types are opaque). You can only get them from `RootCap`. - -```cap -module read_config -use sys::system -use sys::fs - -pub fn main(rc: RootCap) -> i32 { - let fs = rc.mint_filesystem("./config") - let dir = fs.root_dir() - let file = dir.open_read("app.txt") - - match file.read_to_string() { - Ok(s) => { rc.mint_console().println(s); return 0 } - Err(e) => { return 1 } - } -} -``` - -This is attenuation: each step narrows authority. There is no safe API to widen back. - -To make attenuation one-way at compile time, any method that returns a capability must take `self` by value. Methods that take `&self` cannot return capabilities. - -Example of what is rejected (and why): - -```cap -capability struct Dir -capability struct FileRead - -impl Dir { - pub fn open(self: &Dir, name: string) -> FileRead { - let file = self.open_read(name) - return file - } -} -``` - -Why this is rejected: - -- `Dir` can read many files (more power). -- `FileRead` can read one file (less power). -- The bad example lets you keep the more powerful `Dir` and also get a `FileRead`. -- We want “one-way” attenuation: when you make something less powerful, you give up the more powerful one. - -So methods that return capabilities must take `self` by value, which consumes the old capability. - -## 7) Capability, opaque, copy, affine, linear - -`capability struct` is the explicit “this is an authority token” marker. Capability types are always opaque (no public fields, no user construction) and default to affine unless marked `copy` or `linear`. This exists so the capability surface is obvious in code and the compiler can enforce one‑way attenuation (methods returning capabilities must take `self` by value). - -Structs can declare their kind: - -```cap -capability struct Token // affine by default (move-only) -copy capability struct RootCap // unrestricted (copyable) -linear capability struct FileRead // must be consumed -``` - -Kinds: - -- **Unrestricted** (copy): can be reused freely. -- **Affine** (default for capability/opaque): move-only, dropping is OK. -- **Linear**: move-only and must be consumed on all paths. - -Use `capability struct` for authority-bearing tokens. Use `opaque struct` for unforgeable data types that aren’t capabilities. - -In the current stdlib: - -- `copy capability`: `RootCap`, `Console`, `Args` -- `copy opaque`: `Alloc`, `Buffer`, `Slice`, `MutSlice`, `VecU8`, `VecI32`, `VecString` -- `capability` (affine): `ReadFS`, `Filesystem`, `Dir`, `Stdin` -- `linear capability`: `FileRead` - -## 8) Moves and use-after-move - -```cap -module moves - -capability struct Token - -pub fn main() -> i32 { - let t = Token{} - let u = t - let v = t // error: use of moved value - return 0 -} -``` - -Affine and linear values cannot be used after move. If you move in one branch, it's moved after the join. - -## 9) Linear must be consumed - -```cap -module linear - -linear capability struct Ticket - -pub fn main() -> i32 { - let t = Ticket{} - drop(t) // consumes t - return 0 -} -``` - -Linear values must be consumed along every path. You can consume them with a terminal method (like `FileRead.close()` or `read_to_string()`), or with `drop(x)` as a last resort. - -## 10) Borrow-lite: &T parameters - -There is a small borrow feature for read-only access in function parameters and locals. - -```cap -module borrow - -capability struct Cap - -impl Cap { - pub fn ping(self: &Cap) -> i32 { return 1 } -} - -pub fn twice(c: &Cap) -> i32 { - let a = c.ping() - let b = c.ping() - return a + b -} -``` - -Rules: - -- `&T` is allowed on parameters and locals. -- Reference locals must be initialized from another local value. -- References cannot be stored in structs, enums, or returned. -- References are read-only: they can only satisfy `&T` parameters. -- Passing a value to `&T` implicitly borrows it. - -This avoids a full borrow checker while making non-consuming observers ergonomic. - -## 11) Safety boundary - -`package safe` is default. Raw pointers and extern calls require `package unsafe`. - -```cap -package unsafe -module ffi - -extern fn some_ffi(x: i32) -> i32 -``` - -## 12) Raw pointers and unsafe - -Raw pointers are available as `*T`, but **only** in `package unsafe`. - -```cap -package unsafe -module pointers - -pub fn main(rc: RootCap) -> i32 { - let alloc = rc.mint_alloc_default() - let ptr: *u8 = alloc.malloc(16) - alloc.free(ptr) - return 0 -} -``` - -There is no borrow checker for pointers. Use them only inside `package unsafe`. - -## 13) What exists today (quick list) - -- Methods, modules, enums, match, while, if -- Opaque capability handles in `sys.*` -- Linear/affine checking with control-flow joins -- Borrow-lite `&T` parameters - ---- - -That should be enough to read and write small Capable programs, and understand how attenuation and linearity fit together. From 4c6e29ede02631c622a94c1188ab18941bb98e87 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Wed, 31 Dec 2025 17:19:03 -0800 Subject: [PATCH 12/30] Simple ok+err handlers --- capc/src/codegen/emit.rs | 128 +++++++++++--------- capc/src/hir.rs | 12 ++ capc/src/typeck/check.rs | 44 +++++++ capc/src/typeck/lower.rs | 205 +++++++++++++++++++++++++++++++- capc/src/typeck/monomorphize.rs | 4 + capc/tests/run.rs | 28 +++++ capc/tests/typecheck.rs | 16 +++ docs/POLICY.md | 8 +- 8 files changed, 384 insertions(+), 61 deletions(-) diff --git a/capc/src/codegen/emit.rs b/capc/src/codegen/emit.rs index 80cf9fa..78c7c31 100644 --- a/capc/src/codegen/emit.rs +++ b/capc/src/codegen/emit.rs @@ -741,6 +741,13 @@ fn emit_hir_expr_inner( Ok(*ok) } + HirExpr::Trap(_trap) => { + builder.ins().trap(ir::TrapCode::UnreachableCodeReached); + // Return a dummy value - the block is now filled so no more + // instructions can be added. The match handler will detect this + // and skip value storage. + Ok(ValueRepr::Unit) + } HirExpr::Call(call) => { // HIR calls are already fully resolved - no path resolution needed! let (module_path, func_name, _symbol) = match &call.callee { @@ -1449,6 +1456,7 @@ fn cmp_cc(expr: &crate::hir::HirExpr, signed: IntCC, unsigned: IntCC) -> IntCC { crate::hir::HirExpr::Binary(binary) => &binary.ty, crate::hir::HirExpr::Match(m) => &m.result_ty, crate::hir::HirExpr::Try(t) => &t.ok_ty, + crate::hir::HirExpr::Trap(t) => &t.ty, }; if crate::typeck::is_unsigned_type(&ty.ty) { unsigned @@ -2271,17 +2279,22 @@ fn emit_hir_match_expr( } // Last statement should be an expression - let arm_value = match last { - HirStmt::Expr(expr_stmt) => emit_hir_expr( - builder, - &expr_stmt.expr, - &arm_locals, - fn_map, - enum_index, - struct_layouts, - module, - data_counter, - )?, + let (arm_value, arm_diverges) = match last { + HirStmt::Expr(expr_stmt) => { + // Check if this arm ends with a Trap - if so, it diverges + let diverges = matches!(&expr_stmt.expr, crate::hir::HirExpr::Trap(_)); + let value = emit_hir_expr( + builder, + &expr_stmt.expr, + &arm_locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + )?; + (value, diverges) + } _ => { return Err(CodegenError::Unsupported( "match arm must end with expression".to_string(), @@ -2289,55 +2302,58 @@ fn emit_hir_match_expr( } }; - let values = match &arm_value { - ValueRepr::Single(val) => vec![*val], - ValueRepr::Pair(a, b) => vec![*a, *b], - ValueRepr::Unit => vec![], - ValueRepr::Result { .. } => { - return Err(CodegenError::Unsupported("match result value".to_string())) - } - }; + // If the arm diverges (e.g., with a trap), skip value storage + if arm_diverges { + builder.seal_block(arm_block); + } else { + let values = match &arm_value { + ValueRepr::Single(val) => vec![*val], + ValueRepr::Pair(a, b) => vec![*a, *b], + ValueRepr::Unit => vec![], + ValueRepr::Result { .. } => { + return Err(CodegenError::Unsupported("match result value".to_string())) + } + }; - // Set up result shape and stack slots on first arm - if result_shape.is_none() { - let mut types = Vec::new(); - let mut slots = Vec::new(); - for val in &values { - let ty = builder.func.dfg.value_type(*val); - let size = ty.bytes() as u32; - let slot = builder.create_sized_stack_slot(ir::StackSlotData::new( - ir::StackSlotKind::ExplicitSlot, - size.max(1), - )); - types.push(ty); - slots.push(slot); + // Set up result shape and stack slots on first non-terminated arm + if result_shape.is_none() { + let mut types = Vec::new(); + let mut slots = Vec::new(); + for val in &values { + let ty = builder.func.dfg.value_type(*val); + let size = ty.bytes() as u32; + let slot = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + size.max(1), + )); + types.push(ty); + slots.push(slot); + } + result_shape = Some(ResultShape { + kind: match &arm_value { + ValueRepr::Unit => ResultKind::Unit, + ValueRepr::Single(_) => ResultKind::Single, + ValueRepr::Pair(_, _) => ResultKind::Pair, + _ => ResultKind::Single, + }, + slots, + types, + }); } - result_shape = Some(ResultShape { - kind: match &arm_value { - ValueRepr::Unit => ResultKind::Unit, - ValueRepr::Single(_) => ResultKind::Single, - ValueRepr::Pair(_, _) => ResultKind::Pair, - _ => ResultKind::Single, - }, - slots, - types, - }); - } - // Store values to stack slots - let shape = result_shape - .as_ref() - .ok_or_else(|| CodegenError::Codegen("missing match result shape".to_string()))?; - if values.len() != shape.types.len() { - eprintln!("DEBUG: arm value mismatch - values: {:?}, expected: {:?}", values.len(), shape.types.len()); - eprintln!("DEBUG: arm_value = {:?}", arm_value); - return Err(CodegenError::Unsupported("mismatched match arm".to_string())); - } - for (idx, val) in values.iter().enumerate() { - builder.ins().stack_store(*val, shape.slots[idx], 0); + // Store values to stack slots + let shape = result_shape + .as_ref() + .ok_or_else(|| CodegenError::Codegen("missing match result shape".to_string()))?; + if values.len() != shape.types.len() { + return Err(CodegenError::Unsupported("mismatched match arm".to_string())); + } + for (idx, val) in values.iter().enumerate() { + builder.ins().stack_store(*val, shape.slots[idx], 0); + } + builder.ins().jump(merge_block, &[]); + builder.seal_block(arm_block); } - builder.ins().jump(merge_block, &[]); - builder.seal_block(arm_block); if is_last { break; diff --git a/capc/src/hir.rs b/capc/src/hir.rs index 28090dd..c0ea203 100644 --- a/capc/src/hir.rs +++ b/capc/src/hir.rs @@ -206,6 +206,7 @@ pub enum HirExpr { Binary(HirBinary), Match(HirMatch), Try(HirTry), + Trap(HirTrap), } impl HirExpr { @@ -222,6 +223,7 @@ impl HirExpr { HirExpr::Binary(e) => e.span, HirExpr::Match(e) => e.span, HirExpr::Try(e) => e.span, + HirExpr::Trap(e) => e.span, } } @@ -238,6 +240,7 @@ impl HirExpr { HirExpr::Binary(e) => &e.ty, HirExpr::Match(e) => &e.result_ty, HirExpr::Try(e) => &e.ok_ty, + HirExpr::Trap(e) => &e.ty, } } } @@ -360,6 +363,15 @@ pub struct HirTry { pub span: Span, } +/// Unconditional trap/panic. Used for unreachable code paths like +/// calling .ok() on an Err variant. +#[derive(Debug, Clone)] +pub struct HirTrap { + /// The type this expression would have produced (for type checking). + pub ty: HirType, + pub span: Span, +} + #[derive(Debug, Clone)] pub struct HirMatchArm { pub pattern: HirPattern, diff --git a/capc/src/typeck/check.rs b/capc/src/typeck/check.rs index c0c3208..d3a07b2 100644 --- a/capc/src/typeck/check.rs +++ b/capc/src/typeck/check.rs @@ -1355,6 +1355,50 @@ pub(super) fn check_expr( } return record_expr_type(recorder, expr, err_ty.clone()); } + "is_ok" => { + if !method_call.args.is_empty() { + return Err(TypeError::new( + "is_ok takes no arguments".to_string(), + method_call.span, + )); + } + return record_expr_type( + recorder, + expr, + Ty::Builtin(BuiltinType::Bool), + ); + } + "is_err" => { + if !method_call.args.is_empty() { + return Err(TypeError::new( + "is_err takes no arguments".to_string(), + method_call.span, + )); + } + return record_expr_type( + recorder, + expr, + Ty::Builtin(BuiltinType::Bool), + ); + } + "ok" => { + if !method_call.args.is_empty() { + return Err(TypeError::new( + "ok takes no arguments".to_string(), + method_call.span, + )); + } + return record_expr_type(recorder, expr, ok_ty.clone()); + } + "err" => { + if !method_call.args.is_empty() { + return Err(TypeError::new( + "err takes no arguments".to_string(), + method_call.span, + )); + } + return record_expr_type(recorder, expr, err_ty.clone()); + } _ => {} } } diff --git a/capc/src/typeck/lower.rs b/capc/src/typeck/lower.rs index 424fe24..ef77c2e 100644 --- a/capc/src/typeck/lower.rs +++ b/capc/src/typeck/lower.rs @@ -8,13 +8,14 @@ use crate::hir::{ HirEnumVariant, HirEnumVariantExpr, HirExpr, HirExprStmt, HirExternFunction, HirField, HirFieldAccess, HirForStmt, HirFunction, HirIfStmt, HirLetStmt, HirLiteral, HirLocal, HirMatch, HirMatchArm, HirParam, HirPattern, HirReturnStmt, HirStmt, HirStruct, HirStructLiteral, - HirStructLiteralField, HirType, HirUnary, HirWhileStmt, IntrinsicId, LocalId, ResolvedCallee, + HirStructLiteralField, HirTrap, HirType, HirUnary, HirWhileStmt, IntrinsicId, LocalId, + ResolvedCallee, }; use super::{ build_type_params, check, function_key, lower_type, resolve_enum_variant, resolve_method_target, - resolve_type_name, EnumInfo, FunctionSig, FunctionTypeTables, SpanExt, StdlibIndex, StructInfo, - Ty, TypeTable, UseMap, + resolve_type_name, BuiltinType, EnumInfo, FunctionSig, FunctionTypeTables, SpanExt, + StdlibIndex, StructInfo, Ty, TypeTable, UseMap, }; /// Context for HIR lowering (uses the type checker as source of truth). @@ -765,6 +766,204 @@ fn lower_expr(expr: &Expr, ctx: &mut LoweringCtx, ret_ty: &Ty) -> Result { + let bool_ty = HirType { + ty: Ty::Builtin(BuiltinType::Bool), + abi: AbiType::Bool, + }; + let true_lit = HirExpr::Literal(HirLiteral { + value: Literal::Bool(true), + ty: bool_ty.clone(), + span: method_call.span, + }); + let false_lit = HirExpr::Literal(HirLiteral { + value: Literal::Bool(false), + ty: bool_ty.clone(), + span: method_call.span, + }); + let ok_pattern = HirPattern::Variant { + variant_name: "Ok".to_string(), + binding: None, + }; + let err_pattern = HirPattern::Variant { + variant_name: "Err".to_string(), + binding: None, + }; + let ok_block = HirBlock { + stmts: vec![HirStmt::Expr(HirExprStmt { + expr: true_lit, + span: method_call.span, + })], + }; + let err_block = HirBlock { + stmts: vec![HirStmt::Expr(HirExprStmt { + expr: false_lit, + span: method_call.span, + })], + }; + return Ok(HirExpr::Match(HirMatch { + expr: Box::new(receiver), + arms: vec![ + HirMatchArm { + pattern: ok_pattern, + body: ok_block, + }, + HirMatchArm { + pattern: err_pattern, + body: err_block, + }, + ], + result_ty: bool_ty, + span: method_call.span, + })); + } + "is_err" => { + let bool_ty = HirType { + ty: Ty::Builtin(BuiltinType::Bool), + abi: AbiType::Bool, + }; + let true_lit = HirExpr::Literal(HirLiteral { + value: Literal::Bool(true), + ty: bool_ty.clone(), + span: method_call.span, + }); + let false_lit = HirExpr::Literal(HirLiteral { + value: Literal::Bool(false), + ty: bool_ty.clone(), + span: method_call.span, + }); + let ok_pattern = HirPattern::Variant { + variant_name: "Ok".to_string(), + binding: None, + }; + let err_pattern = HirPattern::Variant { + variant_name: "Err".to_string(), + binding: None, + }; + let ok_block = HirBlock { + stmts: vec![HirStmt::Expr(HirExprStmt { + expr: false_lit, + span: method_call.span, + })], + }; + let err_block = HirBlock { + stmts: vec![HirStmt::Expr(HirExprStmt { + expr: true_lit, + span: method_call.span, + })], + }; + return Ok(HirExpr::Match(HirMatch { + expr: Box::new(receiver), + arms: vec![ + HirMatchArm { + pattern: ok_pattern, + body: ok_block, + }, + HirMatchArm { + pattern: err_pattern, + body: err_block, + }, + ], + result_ty: bool_ty, + span: method_call.span, + })); + } + "ok" => { + let ok_name = format!("__ok_{}", ctx.local_counter); + let ok_local_id = ctx.fresh_local(ok_name.clone(), args[0].clone()); + let ok_pattern = HirPattern::Variant { + variant_name: "Ok".to_string(), + binding: Some(ok_local_id), + }; + let err_pattern = HirPattern::Variant { + variant_name: "Err".to_string(), + binding: None, + }; + let ok_expr = HirExpr::Local(HirLocal { + local_id: ok_local_id, + ty: hir_type_for(args[0].clone(), ctx, method_call.span)?, + span: method_call.span, + }); + let trap_expr = HirExpr::Trap(HirTrap { + ty: hir_type_for(args[0].clone(), ctx, method_call.span)?, + span: method_call.span, + }); + let ok_block = HirBlock { + stmts: vec![HirStmt::Expr(HirExprStmt { + expr: ok_expr, + span: method_call.span, + })], + }; + let err_block = HirBlock { + stmts: vec![HirStmt::Expr(HirExprStmt { + expr: trap_expr, + span: method_call.span, + })], + }; + return Ok(HirExpr::Match(HirMatch { + expr: Box::new(receiver), + arms: vec![ + HirMatchArm { + pattern: ok_pattern, + body: ok_block, + }, + HirMatchArm { + pattern: err_pattern, + body: err_block, + }, + ], + result_ty: hir_ty, + span: method_call.span, + })); + } + "err" => { + let err_name = format!("__err_{}", ctx.local_counter); + let err_local_id = ctx.fresh_local(err_name.clone(), args[1].clone()); + let ok_pattern = HirPattern::Variant { + variant_name: "Ok".to_string(), + binding: None, + }; + let err_pattern = HirPattern::Variant { + variant_name: "Err".to_string(), + binding: Some(err_local_id), + }; + let err_expr = HirExpr::Local(HirLocal { + local_id: err_local_id, + ty: hir_type_for(args[1].clone(), ctx, method_call.span)?, + span: method_call.span, + }); + let trap_expr = HirExpr::Trap(HirTrap { + ty: hir_type_for(args[1].clone(), ctx, method_call.span)?, + span: method_call.span, + }); + let ok_block = HirBlock { + stmts: vec![HirStmt::Expr(HirExprStmt { + expr: trap_expr, + span: method_call.span, + })], + }; + let err_block = HirBlock { + stmts: vec![HirStmt::Expr(HirExprStmt { + expr: err_expr, + span: method_call.span, + })], + }; + return Ok(HirExpr::Match(HirMatch { + expr: Box::new(receiver), + arms: vec![ + HirMatchArm { + pattern: ok_pattern, + body: ok_block, + }, + HirMatchArm { + pattern: err_pattern, + body: err_block, + }, + ], + result_ty: hir_ty, + span: method_call.span, + })); + } _ => {} } } diff --git a/capc/src/typeck/monomorphize.rs b/capc/src/typeck/monomorphize.rs index 2a97b23..b990ac0 100644 --- a/capc/src/typeck/monomorphize.rs +++ b/capc/src/typeck/monomorphize.rs @@ -638,6 +638,10 @@ impl MonoCtx { span: t.span, })) } + HirExpr::Trap(t) => Ok(HirExpr::Trap(HirTrap { + ty: self.mono_hir_type(module, &t.ty, subs)?, + span: t.span, + })), HirExpr::Index(idx) => { let object = self.mono_expr(module, &idx.object, subs)?; let index = self.mono_expr(module, &idx.index, subs)?; diff --git a/capc/tests/run.rs b/capc/tests/run.rs index f517924..e78131e 100644 --- a/capc/tests/run.rs +++ b/capc/tests/run.rs @@ -852,3 +852,31 @@ fn run_generic_and_index() { assert_eq!(code, 0); assert!(stdout.contains("generic_and_index ok"), "stdout was: {stdout:?}"); } + +#[test] +fn run_result_is_ok_is_err() { + let out_dir = make_out_dir("result_is_ok_is_err"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/should_pass_result_is_ok_is_err.cap", + ]); + assert_eq!(code, 0); + assert!(stdout.contains("is_ok_is_err ok"), "stdout was: {stdout:?}"); +} + +#[test] +fn run_result_ok_err() { + let out_dir = make_out_dir("result_ok_err"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/should_pass_result_ok_err.cap", + ]); + assert_eq!(code, 0); + assert!(stdout.contains("ok_err ok"), "stdout was: {stdout:?}"); +} diff --git a/capc/tests/typecheck.rs b/capc/tests/typecheck.rs index b105691..b6b06c2 100644 --- a/capc/tests/typecheck.rs +++ b/capc/tests/typecheck.rs @@ -130,6 +130,22 @@ fn typecheck_result_unwrap_or_mismatch_fails() { assert!(err.to_string().contains("unwrap_or type mismatch")); } +#[test] +fn typecheck_result_is_ok_is_err() { + let source = load_program("should_pass_result_is_ok_is_err.cap"); + let module = parse_module(&source).expect("parse module"); + let stdlib = load_stdlib().expect("load stdlib"); + type_check_program(&module, &stdlib, &[]).expect("typecheck module"); +} + +#[test] +fn typecheck_result_ok_err() { + let source = load_program("should_pass_result_ok_err.cap"); + let module = parse_module(&source).expect("parse module"); + let stdlib = load_stdlib().expect("load stdlib"); + type_check_program(&module, &stdlib, &[]).expect("typecheck module"); +} + #[test] fn typecheck_match_expr_ok() { let source = load_program("match_expr.cap"); diff --git a/docs/POLICY.md b/docs/POLICY.md index 248ed43..a7d1749 100644 --- a/docs/POLICY.md +++ b/docs/POLICY.md @@ -45,5 +45,9 @@ Keep these as invariants: ## Result Helpers -- `Result.unwrap_or(default)` returns `Ok` or the default. -- `Result.unwrap_err_or(default)` returns `Err` or the default. +- `Result.is_ok()` returns `true` if `Ok`, `false` if `Err`. +- `Result.is_err()` returns `true` if `Err`, `false` if `Ok`. +- `Result.ok()` returns the `Ok` value (traps if `Err`). +- `Result.err()` returns the `Err` value (traps if `Ok`). +- `Result.unwrap_or(default)` returns `Ok` value or the default. +- `Result.unwrap_err_or(default)` returns `Err` value or the default. From f0b3345686a3c26241f464e65a65628f9864f885 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Wed, 31 Dec 2025 17:41:57 -0800 Subject: [PATCH 13/30] Add Vec indexing syntax and fix for-loop range parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds several improvements to the language: ## Parser fix for for-loop ranges Fixed a bug where `for i in 0..n { ... }` would fail to parse when using a variable as the range bound. The issue was that `parse_primary` would see `n {` and try to parse it as a struct literal `n { ... }`. Added a new `parse_range_bound` function that accepts only: - Integer literals - Boolean literals (for type error at check time, not parse time) - Simple paths (identifiers, possibly with :: separators) This prevents struct literal ambiguity while still allowing variables in range bounds. ## Vec indexing with [] syntax Added support for indexing VecString, VecI32, and VecU8 with bracket syntax: - `lines[i]` now works and returns `Result` - `indices[j]` returns `Result` - `bytes[k]` returns `Result` Implementation: - Type checker recognizes Vec types and returns appropriate Result type - Lowering phase desugars `vec[i]` to `vec.get(i)` call - Reuses existing runtime functions, no codegen changes needed ## Updated examples All examples now use idiomatic Capable with new language features: - For loops with variable bounds (`for i in 0..n`) - Vec indexing with `[]` syntax - `is_ok()`, `is_err()`, `ok()`, `err()` Result methods - `break` and `continue` in loops 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- capc/src/parser.rs | 56 +++++++++++- capc/src/typeck/check.rs | 30 ++++++- capc/src/typeck/lower.rs | 49 +++++++++-- examples/config_loader/config_loader.cap | 44 ++++------ examples/http_server/http_server.cap | 88 +++++++++---------- examples/sort/sort.cap | 61 +++++-------- examples/uniq/uniq.cap | 32 +++---- .../should_pass_result_is_ok_is_err.cap | 31 +++++++ tests/programs/should_pass_result_ok_err.cap | 33 +++++++ 9 files changed, 285 insertions(+), 139 deletions(-) create mode 100644 tests/programs/should_pass_result_is_ok_is_err.cap create mode 100644 tests/programs/should_pass_result_ok_err.cap diff --git a/capc/src/parser.rs b/capc/src/parser.rs index 83fe812..f463d7c 100644 --- a/capc/src/parser.rs +++ b/capc/src/parser.rs @@ -597,9 +597,9 @@ impl Parser { let start = self.expect(TokenKind::For)?.span.start; let var = self.expect_ident()?; self.expect(TokenKind::In)?; - let range_start = self.parse_primary()?; + let range_start = self.parse_range_bound()?; self.expect(TokenKind::DotDot)?; - let range_end = self.parse_primary()?; + let range_end = self.parse_range_bound()?; let body = self.parse_block()?; let end = body.span.end; Ok(ForStmt { @@ -611,6 +611,58 @@ impl Parser { }) } + /// Parse a simple expression for range bounds (no struct literals allowed) + fn parse_range_bound(&mut self) -> Result { + match self.peek_kind() { + Some(TokenKind::Int) => { + let token = self.bump().unwrap(); + let value = token.text.parse::().map_err(|_| { + self.error_at(token.span, "invalid integer literal".to_string()) + })?; + Ok(Expr::Literal(LiteralExpr { + value: Literal::Int(value), + span: token.span, + })) + } + Some(TokenKind::True) => { + let token = self.bump().unwrap(); + Ok(Expr::Literal(LiteralExpr { + value: Literal::Bool(true), + span: token.span, + })) + } + Some(TokenKind::False) => { + let token = self.bump().unwrap(); + Ok(Expr::Literal(LiteralExpr { + value: Literal::Bool(false), + span: token.span, + })) + } + Some(TokenKind::Ident) => { + // Parse just a simple path (no struct literal) + let first_ident = self.expect_ident()?; + let start = first_ident.span.start; + let mut segments = vec![first_ident]; + + while self.peek_kind() == Some(TokenKind::ColonColon) { + self.bump(); + let segment = self.expect_ident()?; + segments.push(segment); + } + + let end = segments.last().unwrap().span.end; + Ok(Expr::Path(Path { + segments, + span: Span::new(start, end), + })) + } + Some(_other) => Err(self.error_current(format!( + "expected integer or identifier in range bound, found {{other:?}}" + ))), + None => Err(self.error_current("unexpected end of input".to_string())), + } + } + fn parse_expr_stmt(&mut self) -> Result { let expr = self.parse_expr()?; let expr_span = expr.span(); diff --git a/capc/src/typeck/check.rs b/capc/src/typeck/check.rs index d3a07b2..f89532d 100644 --- a/capc/src/typeck/check.rs +++ b/capc/src/typeck/check.rs @@ -1982,8 +1982,36 @@ pub(super) fn check_expr( )) } } + // Vec types return Result + Ty::Path(name, _) if name == "VecString" || name == "sys.vec.VecString" => { + Ok(Ty::Path( + "Result".to_string(), + vec![ + Ty::Builtin(BuiltinType::String), + Ty::Path("sys.vec.VecErr".to_string(), vec![]), + ], + )) + } + Ty::Path(name, _) if name == "VecI32" || name == "sys.vec.VecI32" => { + Ok(Ty::Path( + "Result".to_string(), + vec![ + Ty::Builtin(BuiltinType::I32), + Ty::Path("sys.vec.VecErr".to_string(), vec![]), + ], + )) + } + Ty::Path(name, _) if name == "VecU8" || name == "sys.vec.VecU8" => { + Ok(Ty::Path( + "Result".to_string(), + vec![ + Ty::Builtin(BuiltinType::U8), + Ty::Path("sys.vec.VecErr".to_string(), vec![]), + ], + )) + } _ => Err(TypeError::new( - format!("cannot index into type {:?}; only string, Slice[T], and MutSlice[T] are indexable", object_ty), + format!("cannot index into type {:?}; only string, Slice[T], MutSlice[T], and Vec types are indexable", object_ty), index_expr.span, )), } diff --git a/capc/src/typeck/lower.rs b/capc/src/typeck/lower.rs index ef77c2e..cec0fee 100644 --- a/capc/src/typeck/lower.rs +++ b/capc/src/typeck/lower.rs @@ -1156,12 +1156,49 @@ fn lower_expr(expr: &Expr, ctx: &mut LoweringCtx, ret_ty: &Ty) -> Result { + if name.starts_with("sys.vec.") { + name.strip_prefix("sys.vec.").unwrap() + } else { + name.as_str() + } + } + _ => unreachable!(), + }; + let key = format!("sys.vec.{}__get", type_name); + let symbol = format!("capable_{}", key.replace('.', "_").replace("__", "_")); + + Ok(HirExpr::Call(crate::hir::HirCall { + callee: ResolvedCallee::Function { + module: "sys.vec".to_string(), + name: format!("{}__get", type_name), + symbol, + }, + type_args: vec![], + args: vec![object, index], + ret_ty: hir_ty, + span: index_expr.span, + })) + } else { + Ok(HirExpr::Index(crate::hir::HirIndex { + object: Box::new(object), + index: Box::new(index), + elem_ty: hir_ty, + span: index_expr.span, + })) + } } } } diff --git a/examples/config_loader/config_loader.cap b/examples/config_loader/config_loader.cap index 050a1cd..2af5662 100644 --- a/examples/config_loader/config_loader.cap +++ b/examples/config_loader/config_loader.cap @@ -15,17 +15,17 @@ fn print_kv(c: Console, key: string, val: string) -> unit { } fn parse_line(c: Console, alloc: Alloc, line: string) -> Result { - if line.len() == 0 { + if (line.len() == 0) { return Ok(()) } - if line.starts_with("#") { + if (line.starts_with("#")) { return Ok(()) } let parts = line.split(61u8) - if parts.len() == 2 { - let key = parts.get(0)? - let val = parts.get(1)? + if (parts.len() == 2) { + let key = parts[0]? + let val = parts[1]? print_kv(c, key, val) } alloc.vec_string_free(parts) @@ -34,11 +34,10 @@ fn parse_line(c: Console, alloc: Alloc, line: string) -> Result { fn parse_config(c: Console, alloc: Alloc, contents: string) -> Result { let lines = contents.lines() - let i = 0 - while i < lines.len() { - let line = lines.get(i)? + let n = lines.len() + for i in 0..n { + let line = lines[i]? parse_line(c, alloc, line)? - i = i + 1 } alloc.vec_string_free(lines) return Ok(()) @@ -46,29 +45,22 @@ fn parse_config(c: Console, alloc: Alloc, contents: string) -> Result Result { let contents = fs.read_to_string("app.conf")? - match (parse_config(c, alloc, contents)) { - Ok(_) => { - return Ok(()) - } - Err(_) => { - return Err(fs::FsErr::IoError) - } + let result = parse_config(c, alloc, contents) + if (result.is_err()) { + return Err(fs::FsErr::IoError) } + return Ok(()) } pub fn main(rc: RootCap) -> i32 { let c = rc.mint_console() let fs = rc.mint_readfs("examples/config_loader") let alloc = rc.mint_alloc_default() - let res = run(c, alloc, fs) - match (res) { - Ok(_) => { - c.println("config ok") - return 0 - } - Err(_) => { - c.println("config read failed") - return 1 - } + let result = run(c, alloc, fs) + if (result.is_ok()) { + c.println("config ok") + return 0 } + c.println("config read failed") + return 1 } diff --git a/examples/http_server/http_server.cap b/examples/http_server/http_server.cap index ed0e489..a05c378 100644 --- a/examples/http_server/http_server.cap +++ b/examples/http_server/http_server.cap @@ -12,27 +12,21 @@ fn arg_or_default(args: Args, index: i32, default: string) -> string { if (args.len() <= index) { return default } - match (args.at(index)) { - Ok(value) => { return value } - Err(_) => { return default } - } + return args.at(index).unwrap_or(default) } fn strip_query(raw_path: string) -> string { - match (raw_path.split(63u8).get(0)) { - Ok(path) => { return path } - Err(_) => { return "" } - } + return raw_path.split(63u8)[0].unwrap_or("") } fn sanitize_segment(parts: VecString, i: i32, acc: string, seg: string) -> Result { if (seg.len() == 0) { return sanitize_parts(parts, i + 1, acc) } - if (seg.eq(".")) { + if (seg == ".") { return sanitize_parts(parts, i + 1, acc) } - if (seg.eq("..")) { + if (seg == "..") { return Err(()) } if (acc.len() == 0) { @@ -45,45 +39,48 @@ fn sanitize_parts(parts: VecString, i: i32, acc: string) -> Result if (i >= parts.len()) { return Ok(acc) } - match (parts.get(i)) { - Ok(seg) => { return sanitize_segment(parts, i, acc, seg) } - Err(_) => { return Err(()) } + let seg_result = parts[i] + if (seg_result.is_err()) { + return Err(()) } + return sanitize_segment(parts, i, acc, seg_result.ok()) } fn sanitize_path(raw_path: string) -> Result { - match (sanitize_parts(raw_path.split(47u8), 0, "")) { - Ok(path) => { - if (path.len() == 0) { - return Ok("index.html") - } - return Ok(path) - } - Err(_) => { return Err(()) } + let result = sanitize_parts(raw_path.split(47u8), 0, "") + if (result.is_err()) { + return Err(()) + } + let path = result.ok() + if (path.len() == 0) { + return Ok("index.html") } + return Ok(path) } fn parse_request_line(line: string) -> Result { let parts = line.trim().split(32u8) - match (parts.get(0)) { - Ok(method) => { - if (!method.eq("GET")) { - return Err(()) - } - } - Err(_) => { return Err(()) } - } - match (parts.get(1)) { - Ok(raw_path) => { return sanitize_path(strip_query(raw_path)) } - Err(_) => { return Err(()) } + let method_result = parts[0] + if (method_result.is_err()) { + return Err(()) } + let method = method_result.ok() + if (method != "GET") { + return Err(()) + } + let path_result = parts[1] + if (path_result.is_err()) { + return Err(()) + } + return sanitize_path(strip_query(path_result.ok())) } fn parse_request_path(req: string) -> Result { - match (req.lines().get(0)) { - Ok(line) => { return parse_request_line(line) } - Err(_) => { return Err(()) } + let line_result = req.lines()[0] + if (line_result.is_err()) { + return Err(()) } + return parse_request_line(line_result.ok()) } fn respond_ok(conn: &TcpConn, body: string) -> Result { @@ -101,19 +98,22 @@ fn respond_bad_request(conn: &TcpConn) -> Result { } fn handle_request(conn: &TcpConn, readfs: ReadFS, path: string) -> Result { - match (readfs.read_to_string(path)) { - Ok(body) => { return respond_ok(conn, body) } - Err(_) => { return respond_not_found(conn) } + let file_result = readfs.read_to_string(path) + if (file_result.is_ok()) { + return respond_ok(conn, file_result.ok()) } + return respond_not_found(conn) } fn serve_once(c: Console, net: Net, readfs: ReadFS) -> Result { let listener = net.listen("127.0.0.1", 8080)? let conn = listener.accept()? let req = conn.read(4096)? - match (parse_request_path(req)) { - Ok(path) => { handle_request(conn, readfs, path)? } - Err(_) => { respond_bad_request(conn)? } + let path_result = parse_request_path(req) + if (path_result.is_ok()) { + handle_request(conn, readfs, path_result.ok())? + } else { + respond_bad_request(conn)? } conn.close() return Ok(()) @@ -126,9 +126,9 @@ pub fn main(rc: RootCap) -> i32 { let root = arg_or_default(args, 1, ".") let readfs = rc.mint_readfs(root) c.println("listening on 127.0.0.1:8080") - match (serve_once(c, net, readfs)) { - Ok(_) => {} - Err(_) => { c.println("server error") } + let result = serve_once(c, net, readfs) + if (result.is_err()) { + c.println("server error") } return 0 } diff --git a/examples/sort/sort.cap b/examples/sort/sort.cap index 9267d72..5710914 100644 --- a/examples/sort/sort.cap +++ b/examples/sort/sort.cap @@ -7,20 +7,6 @@ use sys::io use sys::string use sys::vec -fn get_line(lines: VecString, i: i32) -> string { - match (lines.get(i)) { - Ok(line) => { return line } - Err(_) => { return "" } - } -} - -fn get_idx(indices: VecI32, i: i32) -> i32 { - match (indices.get(i)) { - Ok(idx) => { return idx } - Err(_) => { return 0 } - } -} - fn min_i32(a: i32, b: i32) -> i32 { if (a < b) { return a @@ -48,34 +34,34 @@ fn str_lt(a: string, b: string) -> bool { // Compare lines at indices i and j fn line_lt(lines: VecString, i: i32, j: i32) -> bool { - return str_lt(get_line(lines, i), get_line(lines, j)) + let line_i = lines[i].unwrap_or("") + let line_j = lines[j].unwrap_or("") + return str_lt(line_i, line_j) } // Insertion sort on indices fn sort_indices(lines: VecString, indices: VecI32) -> unit { let n = indices.len() - let i = 1 - while (i < n) { + for i in 1..n { let j = i while (j > 0) { - let curr_idx = get_idx(indices, j) - let prev_idx = get_idx(indices, j - 1) + let curr_idx = indices[j].unwrap_or(0) + let prev_idx = indices[j - 1].unwrap_or(0) if (line_lt(lines, curr_idx, prev_idx)) { // swap indices[j] and indices[j-1] match (indices.set(j, prev_idx)) { - Ok(_) => {} - Err(_) => {} + Ok(ok) => {} + Err(e) => {} } match (indices.set(j - 1, curr_idx)) { - Ok(_) => {} - Err(_) => {} + Ok(ok) => {} + Err(e) => {} } j = j - 1 } else { break } } - i = i + 1 } } @@ -86,26 +72,22 @@ fn run(c: Console, alloc: Alloc, input: Stdin) -> Result { // Create index array [0, 1, 2, ...] let indices = alloc.vec_i32_new() - let i = 0 - while (i < n) { + for i in 0..n { match (indices.push(i)) { - Ok(_) => {} - Err(_) => {} + Ok(ok) => {} + Err(e) => {} } - i = i + 1 } sort_indices(lines, indices) // Print lines in sorted order (skip empty lines) - i = 0 - while (i < n) { - let idx = get_idx(indices, i) - let line = get_line(lines, idx) + for i in 0..n { + let idx = indices[i].unwrap_or(0) + let line = lines[idx].unwrap_or("") if (line.len() > 0) { c.println(line) } - i = i + 1 } alloc.vec_i32_free(indices) @@ -117,11 +99,10 @@ pub fn main(rc: RootCap) -> i32 { let c = rc.mint_console() let input = rc.mint_stdin() let alloc = rc.mint_alloc_default() - match (run(c, alloc, input)) { - Ok(_) => { return 0 } - Err(_) => { - c.println("error reading input") - return 1 - } + let result = run(c, alloc, input) + if (result.is_err()) { + c.println("error reading input") + return 1 } + return 0 } diff --git a/examples/uniq/uniq.cap b/examples/uniq/uniq.cap index fb9c84f..6a7ac4a 100644 --- a/examples/uniq/uniq.cap +++ b/examples/uniq/uniq.cap @@ -7,31 +7,24 @@ use sys::io use sys::string use sys::vec -fn get_line(lines: VecString, i: i32) -> string { - match (lines.get(i)) { - Ok(line) => { return line } - Err(_) => { return "" } - } -} - fn should_print(lines: VecString, i: i32) -> bool { if (i == 0) { return true } - let curr = get_line(lines, i) - let prev = get_line(lines, i - 1) - return !prev.eq(curr) + let curr = lines[i].unwrap_or("") + let prev = lines[i - 1].unwrap_or("") + return prev != curr } fn run(c: Console, alloc: Alloc, input: Stdin) -> Result { let contents = input.read_to_string()? let lines = contents.lines() - let i = 0 - while (i < lines.len()) { + let n = lines.len() + for i in 0..n { if (should_print(lines, i)) { - c.println(get_line(lines, i)) + let line = lines[i].unwrap_or("") + c.println(line) } - i = i + 1 } alloc.vec_string_free(lines) return Ok(()) @@ -41,11 +34,10 @@ pub fn main(rc: RootCap) -> i32 { let c = rc.mint_console() let input = rc.mint_stdin() let alloc = rc.mint_alloc_default() - match (run(c, alloc, input)) { - Ok(_) => { return 0 } - Err(_) => { - c.println("error reading input") - return 1 - } + let result = run(c, alloc, input) + if (result.is_err()) { + c.println("error reading input") + return 1 } + return 0 } diff --git a/tests/programs/should_pass_result_is_ok_is_err.cap b/tests/programs/should_pass_result_is_ok_is_err.cap new file mode 100644 index 0000000..5382c11 --- /dev/null +++ b/tests/programs/should_pass_result_is_ok_is_err.cap @@ -0,0 +1,31 @@ +module should_pass_result_is_ok_is_err +use sys::system + +fn make_ok() -> Result { + return Ok(1) +} + +fn make_err() -> Result { + return Err(2) +} + +pub fn main(rc: RootCap) -> i32 { + let con = rc.mint_console() + let ok_result = make_ok() + let err_result = make_err() + + if ok_result.is_ok() == false { + return 1 + } + if ok_result.is_err() == true { + return 2 + } + if err_result.is_ok() == true { + return 3 + } + if err_result.is_err() == false { + return 4 + } + con.print("is_ok_is_err ok") + return 0 +} diff --git a/tests/programs/should_pass_result_ok_err.cap b/tests/programs/should_pass_result_ok_err.cap new file mode 100644 index 0000000..9c4bad3 --- /dev/null +++ b/tests/programs/should_pass_result_ok_err.cap @@ -0,0 +1,33 @@ +module should_pass_result_ok_err +use sys::system + +fn make_ok() -> Result { + return Ok(42) +} + +fn make_err() -> Result { + return Err(99) +} + +pub fn main(rc: RootCap) -> i32 { + let con = rc.mint_console() + let ok_result = make_ok() + let err_result = make_err() + + if ok_result.is_ok() { + let val = ok_result.ok() + if val != 42 { + return 1 + } + } + + if err_result.is_err() { + let e = err_result.err() + if e != 99 { + return 2 + } + } + + con.print("ok_err ok") + return 0 +} From c167d11d1dbc56cfdfa2c84f6dfa8e62a22895d4 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Wed, 31 Dec 2025 18:13:18 -0800 Subject: [PATCH 14/30] Move Result enum from compiler builtin to stdlib MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Result is now defined as a regular generic enum in stdlib/sys/result.cap instead of being a compiler-hardcoded type. Changes: - Remove "Result" from RESERVED_TYPE_PARAMS - Add stdlib/sys/result.cap with Result enum definition - Update all path checks from "Result" to "sys.result.Result" in: - typeck/mod.rs (type capability/kind checks) - typeck/check.rs (pattern matching, ? operator, Vec indexing) - typeck/lower.rs (pattern lowering, try operator) - typeck/monomorphize.rs (type monomorphization, ABI handling) - codegen/emit.rs (code generation for Result patterns) - Fix parser error messages with broken format strings The is_ok/is_err/ok/err/unwrap_or methods are still compiler special-cased and will be moved to stdlib in a follow-up change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- capc/src/codegen/emit.rs | 12 ++++++------ capc/src/parser.rs | 16 ++++++++-------- capc/src/typeck/check.rs | 20 ++++++++++---------- capc/src/typeck/lower.rs | 8 ++++---- capc/src/typeck/mod.rs | 17 +++++------------ capc/src/typeck/monomorphize.rs | 4 ++-- stdlib/sys/result.cap | 7 +++++++ 7 files changed, 42 insertions(+), 42 deletions(-) create mode 100644 stdlib/sys/result.cap diff --git a/capc/src/codegen/emit.rs b/capc/src/codegen/emit.rs index 78c7c31..2b13b89 100644 --- a/capc/src/codegen/emit.rs +++ b/capc/src/codegen/emit.rs @@ -588,7 +588,7 @@ fn emit_hir_expr_inner( HirExpr::EnumVariant(variant) => { // Check if this is a Result type with payload (Ok/Err) if let crate::typeck::Ty::Path(ty_name, args) = &variant.enum_ty.ty { - if ty_name == "Result" && args.len() == 2 { + if ty_name == "sys.result.Result" && args.len() == 2 { let AbiType::Result(ok_abi, err_abi) = &variant.enum_ty.abi else { return Err(CodegenError::Unsupported( abi_quirks::result_abi_mismatch_error().to_string(), @@ -706,7 +706,7 @@ fn emit_hir_expr_inner( builder.switch_to_block(err_block); let ret_value = match &try_expr.ret_ty.ty { - crate::typeck::Ty::Path(name, args) if name == "Result" && args.len() == 2 => { + crate::typeck::Ty::Path(name, args) if name == "sys.result.Result" && args.len() == 2 => { let AbiType::Result(ok_abi, _err_abi) = &try_expr.ret_ty.abi else { return Err(CodegenError::Unsupported( abi_quirks::result_abi_mismatch_error().to_string(), @@ -1648,7 +1648,7 @@ fn store_value_by_ty( "generic type parameters must be monomorphized before codegen".to_string(), )), Ty::Path(name, args) => { - if name == "Result" && args.len() == 2 { + if name == "sys.result.Result" && args.len() == 2 { let ValueRepr::Result { tag, ok, err } = value else { return Err(CodegenError::Unsupported("store result".to_string())); }; @@ -1803,7 +1803,7 @@ fn load_value_by_ty( "generic type parameters must be monomorphized before codegen".to_string(), )), Ty::Path(name, args) => { - if name == "Result" && args.len() == 2 { + if name == "sys.result.Result" && args.len() == 2 { let AbiType::Result(ok_abi, err_abi) = &ty.abi else { return Err(CodegenError::Unsupported( abi_quirks::result_abi_mismatch_error().to_string(), @@ -2436,7 +2436,7 @@ fn hir_match_pattern_cond( let val_ty = builder.func.dfg.value_type(match_val); // Special handling for Result type (built-in, not in enum_index) - if qualified == "Result" { + if qualified == "sys.result.Result" { let discr = match variant_name.as_str() { "Ok" => 0i64, "Err" => 1i64, @@ -2662,7 +2662,7 @@ fn zero_value_for_ty( "generic type parameters must be monomorphized before codegen".to_string(), )), Ty::Path(name, args) => { - if name == "Result" && args.len() == 2 { + if name == "sys.result.Result" && args.len() == 2 { let AbiType::Result(ok_abi, err_abi) = &ty.abi else { return Err(CodegenError::Unsupported( abi_quirks::result_abi_mismatch_error().to_string(), diff --git a/capc/src/parser.rs b/capc/src/parser.rs index f463d7c..c33df55 100644 --- a/capc/src/parser.rs +++ b/capc/src/parser.rs @@ -222,8 +222,8 @@ impl Parser { } Ok(Item::Impl(self.parse_impl_block(doc)?)) } - Some(_other) => Err(self.error_current(format!( - "expected item, found {{other:?}}" + Some(other) => Err(self.error_current(format!( + "expected item, found {other:?}" ))), None => Err(self.error_current("unexpected end of input".to_string())), } @@ -656,8 +656,8 @@ impl Parser { span: Span::new(start, end), })) } - Some(_other) => Err(self.error_current(format!( - "expected integer or identifier in range bound, found {{other:?}}" + Some(other) => Err(self.error_current(format!( + "expected integer or identifier in range bound, found {other:?}" ))), None => Err(self.error_current("unexpected end of input".to_string())), } @@ -1262,8 +1262,8 @@ impl Parser { fn expect(&mut self, kind: TokenKind) -> Result { match self.peek_kind() { Some(k) if k == kind => Ok(self.bump().unwrap()), - Some(_other) => Err(self.error_current(format!( - "expected {{kind:?}}, found {{other:?}}" + Some(other) => Err(self.error_current(format!( + "expected {kind:?}, found {other:?}" ))), None => Err(self.error_current("unexpected end of input".to_string())), } @@ -1272,8 +1272,8 @@ impl Parser { fn expect_ident(&mut self) -> Result { match self.peek_kind() { Some(TokenKind::Ident) => Ok(to_ident(&self.bump().unwrap())), - Some(_other) => Err(self.error_current(format!( - "expected identifier, found {{other:?}}" + Some(other) => Err(self.error_current(format!( + "expected identifier, found {other:?}" ))), None => Err(self.error_current("unexpected end of input".to_string())), } diff --git a/capc/src/typeck/check.rs b/capc/src/typeck/check.rs index f89532d..69e9b6c 100644 --- a/capc/src/typeck/check.rs +++ b/capc/src/typeck/check.rs @@ -1051,7 +1051,7 @@ pub(super) fn check_expr( type_params, )?; if let Ty::Path(ty_name, args) = ret_ty { - if ty_name == "Result" && args.len() == 2 { + if ty_name == "sys.result.Result" && args.len() == 2 { let expected = if name == "Ok" { &args[0] } else { &args[1] }; if &arg_ty != expected { return Err(TypeError::new( @@ -1066,7 +1066,7 @@ pub(super) fn check_expr( recorder, expr, Ty::Path( - "Result".to_string(), + "sys.result.Result".to_string(), if name == "Ok" { vec![arg_ty, Ty::Builtin(BuiltinType::Unit)] } else { @@ -1289,7 +1289,7 @@ pub(super) fn check_expr( type_params, )?; if let Ty::Path(name, args) = &receiver_ty { - if name == "Result" && args.len() == 2 { + if name == "sys.result.Result" && args.len() == 2 { let ok_ty = &args[0]; let err_ty = &args[1]; match method_call.method.item.as_str() { @@ -1772,7 +1772,7 @@ pub(super) fn check_expr( type_params, )?; let (ok_ty, err_ty) = match inner_ty { - Ty::Path(name, args) if name == "Result" && args.len() == 2 => { + Ty::Path(name, args) if name == "sys.result.Result" && args.len() == 2 => { (args[0].clone(), args[1].clone()) } _ => { @@ -1784,7 +1784,7 @@ pub(super) fn check_expr( }; let ret_err = match ret_ty { - Ty::Path(name, args) if name == "Result" && args.len() == 2 => &args[1], + Ty::Path(name, args) if name == "sys.result.Result" && args.len() == 2 => &args[1], _ => { return Err(TypeError::new( "the `?` operator can only be used in functions returning Result".to_string(), @@ -1985,7 +1985,7 @@ pub(super) fn check_expr( // Vec types return Result Ty::Path(name, _) if name == "VecString" || name == "sys.vec.VecString" => { Ok(Ty::Path( - "Result".to_string(), + "sys.result.Result".to_string(), vec![ Ty::Builtin(BuiltinType::String), Ty::Path("sys.vec.VecErr".to_string(), vec![]), @@ -1994,7 +1994,7 @@ pub(super) fn check_expr( } Ty::Path(name, _) if name == "VecI32" || name == "sys.vec.VecI32" => { Ok(Ty::Path( - "Result".to_string(), + "sys.result.Result".to_string(), vec![ Ty::Builtin(BuiltinType::I32), Ty::Path("sys.vec.VecErr".to_string(), vec![]), @@ -2003,7 +2003,7 @@ pub(super) fn check_expr( } Ty::Path(name, _) if name == "VecU8" || name == "sys.vec.VecU8" => { Ok(Ty::Path( - "Result".to_string(), + "sys.result.Result".to_string(), vec![ Ty::Builtin(BuiltinType::U8), Ty::Path("sys.vec.VecErr".to_string(), vec![]), @@ -2268,7 +2268,7 @@ fn check_match_exhaustive( span, )); } - Ty::Path(name, args) if name == "Result" && args.len() == 2 => { + Ty::Path(name, args) if name == "sys.result.Result" && args.len() == 2 => { let mut seen_ok = false; let mut seen_err = false; for arm in arms { @@ -2634,7 +2634,7 @@ fn bind_pattern( .collect::>() .join("."); if let Ty::Path(ty_name, args) = match_ty { - if ty_name == "Result" && args.len() == 2 { + if ty_name == "sys.result.Result" && args.len() == 2 { let ty = if name == "Ok" { args[0].clone() } else if name == "Err" { diff --git a/capc/src/typeck/lower.rs b/capc/src/typeck/lower.rs index cec0fee..38e4efb 100644 --- a/capc/src/typeck/lower.rs +++ b/capc/src/typeck/lower.rs @@ -469,7 +469,7 @@ fn abi_type_for(ty: &Ty, ctx: &LoweringCtx, span: Span) -> Result abi_type_for(inner, ctx, span), Ty::Param(_) => Ok(AbiType::Ptr), Ty::Path(name, args) => { - if name == "Result" && args.len() == 2 { + if name == "sys.result.Result" && args.len() == 2 { let ok = abi_type_for(&args[0], ctx, span)?; let err = abi_type_for(&args[1], ctx, span)?; return Ok(AbiType::Result(Box::new(ok), Box::new(err))); @@ -674,7 +674,7 @@ fn lower_expr(expr: &Expr, ctx: &mut LoweringCtx, ret_ty: &Ty) -> Result { let default_expr = lower_expr(&method_call.args[0], ctx, ret_ty)?; @@ -1136,7 +1136,7 @@ fn lower_expr(expr: &Expr, ctx: &mut LoweringCtx, ret_ty: &Ty) -> Result args[0].clone(), + Ty::Path(name, args) if name == "sys.result.Result" && args.len() == 2 => args[0].clone(), _ => { return Err(TypeError::new( "the `?` operator expects a Result value".to_string(), @@ -1278,7 +1278,7 @@ fn lower_pattern( let name = &path.segments[0].item; if name == "Ok" || name == "Err" { if let Ty::Path(ty_name, args) = &match_ty.ty { - if ty_name == "Result" && args.len() == 2 { + if ty_name == "sys.result.Result" && args.len() == 2 { let variant_name = name.clone(); let binding_info = if let Some(bind_ident) = binding { diff --git a/capc/src/typeck/mod.rs b/capc/src/typeck/mod.rs index 3443adb..eec646e 100644 --- a/capc/src/typeck/mod.rs +++ b/capc/src/typeck/mod.rs @@ -19,8 +19,8 @@ use crate::ast::*; use crate::error::TypeError; use crate::hir::HirModule; -pub(super) const RESERVED_TYPE_PARAMS: [&str; 8] = [ - "i32", "i64", "u32", "u8", "bool", "string", "unit", "Result", +pub(super) const RESERVED_TYPE_PARAMS: [&str; 7] = [ + "i32", "i64", "u32", "u8", "bool", "string", "unit", ]; /// Resolved type used after lowering. No spans, fully qualified paths. @@ -754,7 +754,7 @@ fn type_contains_capability_inner( Ty::Builtin(_) | Ty::Ptr(_) | Ty::Ref(_) => false, Ty::Param(_) => true, Ty::Path(name, args) => { - if name == "Result" { + if name == "sys.result.Result" { return args .iter() .any(|arg| type_contains_capability_inner(arg, struct_map, enum_map, visiting)); @@ -836,7 +836,7 @@ fn type_kind_inner( Ty::Builtin(_) | Ty::Ptr(_) | Ty::Ref(_) => TypeKind::Unrestricted, Ty::Param(_) => TypeKind::Affine, Ty::Path(name, args) => { - if name == "Result" { + if name == "sys.result.Result" { return args.iter().fold(TypeKind::Unrestricted, |acc, arg| { combine_kind(acc, type_kind_inner(arg, struct_map, enum_map, visiting)) }); @@ -879,14 +879,7 @@ fn validate_type_args( Ty::Builtin(_) | Ty::Param(_) => Ok(()), Ty::Ptr(inner) | Ty::Ref(inner) => validate_type_args(inner, struct_map, enum_map, span), Ty::Path(name, args) => { - if name == "Result" { - if args.len() != 2 { - return Err(TypeError::new( - format!("Result expects 2 type arguments, found {}", args.len()), - span, - )); - } - } else if let Some(info) = struct_map.get(name) { + if let Some(info) = struct_map.get(name) { if args.len() != info.type_params.len() { return Err(TypeError::new( format!( diff --git a/capc/src/typeck/monomorphize.rs b/capc/src/typeck/monomorphize.rs index b990ac0..c2b97c1 100644 --- a/capc/src/typeck/monomorphize.rs +++ b/capc/src/typeck/monomorphize.rs @@ -738,7 +738,7 @@ impl MonoCtx { Ty::Ptr(inner) => Ok(Ty::Ptr(Box::new(self.mono_ty(module, inner, subs)?))), Ty::Ref(inner) => Ok(Ty::Ref(Box::new(self.mono_ty(module, inner, subs)?))), Ty::Path(name, args) => { - if name == "Result" { + if name == "sys.result.Result" { let args = args .iter() .map(|arg| self.mono_ty(module, arg, subs)) @@ -862,7 +862,7 @@ impl MonoCtx { DUMMY_SPAN, )), Ty::Path(name, args) => { - if name == "Result" && args.len() == 2 { + if name == "sys.result.Result" && args.len() == 2 { let ok = self.abi_type_for(module, &args[0])?; let err = self.abi_type_for(module, &args[1])?; return Ok(AbiType::Result(Box::new(ok), Box::new(err))); diff --git a/stdlib/sys/result.cap b/stdlib/sys/result.cap new file mode 100644 index 0000000..9a19f90 --- /dev/null +++ b/stdlib/sys/result.cap @@ -0,0 +1,7 @@ +package safe +module sys::result + +pub enum Result { + Ok(T), + Err(E) +} From 2f28e942a29ef0ba3b290477fac152a6075ea176 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Wed, 31 Dec 2025 21:43:07 -0800 Subject: [PATCH 15/30] [WIP] Move Result methods from compiler special-case to stdlib MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Progress: - Added panic() intrinsic that lowers to HirTrap - Fixed parser bug: struct literals in match/if/while scrutinee positions - Added parse_expr_no_struct() to prevent `x {` being parsed as struct literal - Applied consistently to match, if, while conditions - Added impl block support for enums (was only structs before) - Added Result methods to stdlib/sys/result.cap: - is_ok(), is_err(), unwrap_or(), unwrap_err_or() - Removed Result method special-casing from check.rs (~115 lines) - Removed Result method desugaring from lower.rs (~295 lines) - Fixed pattern matching for qualified paths like Result::Ok(_) Remaining issues: - ok() and err() methods cannot be implemented yet because panic() is typed as unit, not "never" (a diverging type) - Need to add a "never" type that can unify with any type to properly support panic() in expression contexts - The test should_pass_result_ok_err.cap uses .ok() and .err() which are now missing - this test will fail until never type is added The stdlib Result now has 4 working methods. The .ok() and .err() methods require panic() to have a proper never/diverging type. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- capc/src/parser.rs | 55 +++++-- capc/src/typeck/check.rs | 126 ++------------ capc/src/typeck/lower.rs | 343 +++------------------------------------ capc/src/typeck/mod.rs | 21 ++- capc/tests/typecheck.rs | 2 +- stdlib/sys/result.cap | 30 ++++ 6 files changed, 128 insertions(+), 449 deletions(-) diff --git a/capc/src/parser.rs b/capc/src/parser.rs index c33df55..641c287 100644 --- a/capc/src/parser.rs +++ b/capc/src/parser.rs @@ -554,7 +554,8 @@ impl Parser { fn parse_if(&mut self) -> Result { let start = self.expect(TokenKind::If)?.span.start; - let cond = self.parse_expr()?; + // Use parse_expr_no_struct because `{` after condition starts the then-block, not a struct literal + let cond = self.parse_expr_no_struct()?; let then_block = self.parse_block()?; let else_block = if self.peek_kind() == Some(TokenKind::Else) { self.bump(); @@ -583,7 +584,8 @@ impl Parser { fn parse_while(&mut self) -> Result { let start = self.expect(TokenKind::While)?.span.start; - let cond = self.parse_expr()?; + // Use parse_expr_no_struct because `{` after condition starts the loop body, not a struct literal + let cond = self.parse_expr_no_struct()?; let body = self.parse_block()?; let end = body.span.end; Ok(WhileStmt { @@ -676,11 +678,21 @@ impl Parser { } fn parse_expr(&mut self) -> Result { - self.parse_expr_bp(0) + self.parse_expr_inner(true) } - fn parse_expr_bp(&mut self, min_bp: u8) -> Result { - let mut lhs = self.parse_prefix()?; + /// Parse an expression where struct literals are not allowed. + /// Used in if/while/for/match scrutinee positions where `{` starts a block, not a struct literal. + fn parse_expr_no_struct(&mut self) -> Result { + self.parse_expr_inner(false) + } + + fn parse_expr_inner(&mut self, allow_struct_literal: bool) -> Result { + self.parse_expr_bp(0, allow_struct_literal) + } + + fn parse_expr_bp(&mut self, min_bp: u8, allow_struct_literal: bool) -> Result { + let mut lhs = self.parse_prefix(allow_struct_literal)?; loop { // First, check for postfix operators @@ -702,7 +714,7 @@ impl Parser { }; // Check if this is a struct literal (followed by '{') - if self.peek_kind() == Some(TokenKind::LBrace) { + if allow_struct_literal && self.peek_kind() == Some(TokenKind::LBrace) { // Convert the lhs and field into a path for the struct literal let mut path = match lhs { Expr::Path(p) => p, @@ -807,7 +819,7 @@ impl Parser { }; if looks_like_type { let type_args = self.parse_type_args()?; - if self.peek_kind() == Some(TokenKind::LBrace) { + if allow_struct_literal && self.peek_kind() == Some(TokenKind::LBrace) { let path = match lhs { Expr::Path(p) => p, Expr::FieldAccess(ref fa) => self.field_access_to_path(fa)?, @@ -820,6 +832,15 @@ impl Parser { lhs = self.finish_call(lhs, type_args)?; continue; } + // In no-struct context, if we see LBrace it's the start of a block, not an error + if !allow_struct_literal && self.peek_kind() == Some(TokenKind::LBrace) { + // Type args without call in no-struct context - this is actually ok, + // the LBrace is a block. But we parsed type args that don't belong. + // This is a parse error - generic expressions need parens in this context. + return Err(self.error_current( + "generic expressions in this context require parentheses".to_string(), + )); + } return Err(self.error_current( "type arguments require a call or struct literal".to_string(), )); @@ -851,7 +872,8 @@ impl Parser { } self.bump(); - let rhs = self.parse_expr_bp(r_bp)?; + // RHS of binary operator always allows struct literals (no ambiguity) + let rhs = self.parse_expr_bp(r_bp, true)?; let span = Span::new(lhs.span().start, rhs.span().end); lhs = Expr::Binary(BinaryExpr { op, @@ -864,11 +886,12 @@ impl Parser { Ok(lhs) } - fn parse_prefix(&mut self) -> Result { + fn parse_prefix(&mut self, allow_struct_literal: bool) -> Result { match self.peek_kind() { Some(TokenKind::Minus) => { let start = self.bump().unwrap().span.start; - let expr = self.parse_expr_bp(7)?; + // Unary operands always allow struct literals (no ambiguity) + let expr = self.parse_expr_bp(7, true)?; Ok(Expr::Unary(UnaryExpr { op: UnaryOp::Neg, span: Span::new(start, expr.span().end), @@ -877,7 +900,8 @@ impl Parser { } Some(TokenKind::Bang) => { let start = self.bump().unwrap().span.start; - let expr = self.parse_expr_bp(7)?; + // Unary operands always allow struct literals (no ambiguity) + let expr = self.parse_expr_bp(7, true)?; Ok(Expr::Unary(UnaryExpr { op: UnaryOp::Not, span: Span::new(start, expr.span().end), @@ -885,11 +909,11 @@ impl Parser { })) } Some(TokenKind::Match) => self.parse_match(), - _ => self.parse_primary(), + _ => self.parse_primary(allow_struct_literal), } } - fn parse_primary(&mut self) -> Result { + fn parse_primary(&mut self, allow_struct_literal: bool) -> Result { match self.peek_kind() { Some(TokenKind::Int) => { let token = self.bump().unwrap(); @@ -979,7 +1003,7 @@ impl Parser { span: Span::new(start, end), }; - if self.peek_kind() == Some(TokenKind::LBrace) { + if allow_struct_literal && self.peek_kind() == Some(TokenKind::LBrace) { self.parse_struct_literal(path, Vec::new()) } else { Ok(Expr::Path(path)) @@ -995,7 +1019,8 @@ impl Parser { fn parse_match(&mut self) -> Result { let match_token = self.expect(TokenKind::Match)?; let start = match_token.span.start; - let expr = self.parse_expr()?; + // Use parse_expr_no_struct because `{` after the scrutinee starts the match arms, not a struct literal + let expr = self.parse_expr_no_struct()?; self.expect(TokenKind::LBrace)?; let mut arms = Vec::new(); while self.peek_kind() != Some(TokenKind::RBrace) { diff --git a/capc/src/typeck/check.rs b/capc/src/typeck/check.rs index 69e9b6c..7505745 100644 --- a/capc/src/typeck/check.rs +++ b/capc/src/typeck/check.rs @@ -1029,6 +1029,17 @@ pub(super) fn check_expr( )?; return record_expr_type(recorder, expr, Ty::Builtin(BuiltinType::Unit)); } + if name == "panic" { + if !call.args.is_empty() { + return Err(TypeError::new( + "panic takes no arguments".to_string(), + call.span, + )); + } + // panic() is a diverging expression - it never returns. + // We type it as unit but it will trap at runtime. + return record_expr_type(recorder, expr, Ty::Builtin(BuiltinType::Unit)); + } if name == "Ok" || name == "Err" { if call.args.len() != 1 { return Err(TypeError::new( @@ -1288,121 +1299,6 @@ pub(super) fn check_expr( module_name, type_params, )?; - if let Ty::Path(name, args) = &receiver_ty { - if name == "sys.result.Result" && args.len() == 2 { - let ok_ty = &args[0]; - let err_ty = &args[1]; - match method_call.method.item.as_str() { - "unwrap_or" => { - if method_call.args.len() != 1 { - return Err(TypeError::new( - "unwrap_or expects one argument".to_string(), - method_call.span, - )); - } - let arg_ty = check_expr( - &method_call.args[0], - functions, - scopes, - UseMode::Move, - recorder, - use_map, - struct_map, - enum_map, - stdlib, - ret_ty, - module_name, - type_params, - )?; - if &arg_ty != ok_ty { - return Err(TypeError::new( - format!( - "unwrap_or type mismatch: expected {ok_ty:?}, found {arg_ty:?}" - ), - method_call.args[0].span(), - )); - } - return record_expr_type(recorder, expr, ok_ty.clone()); - } - "unwrap_err_or" => { - if method_call.args.len() != 1 { - return Err(TypeError::new( - "unwrap_err_or expects one argument".to_string(), - method_call.span, - )); - } - let arg_ty = check_expr( - &method_call.args[0], - functions, - scopes, - UseMode::Move, - recorder, - use_map, - struct_map, - enum_map, - stdlib, - ret_ty, - module_name, - type_params, - )?; - if &arg_ty != err_ty { - return Err(TypeError::new( - format!( - "unwrap_err_or type mismatch: expected {err_ty:?}, found {arg_ty:?}" - ), - method_call.args[0].span(), - )); - } - return record_expr_type(recorder, expr, err_ty.clone()); - } - "is_ok" => { - if !method_call.args.is_empty() { - return Err(TypeError::new( - "is_ok takes no arguments".to_string(), - method_call.span, - )); - } - return record_expr_type( - recorder, - expr, - Ty::Builtin(BuiltinType::Bool), - ); - } - "is_err" => { - if !method_call.args.is_empty() { - return Err(TypeError::new( - "is_err takes no arguments".to_string(), - method_call.span, - )); - } - return record_expr_type( - recorder, - expr, - Ty::Builtin(BuiltinType::Bool), - ); - } - "ok" => { - if !method_call.args.is_empty() { - return Err(TypeError::new( - "ok takes no arguments".to_string(), - method_call.span, - )); - } - return record_expr_type(recorder, expr, ok_ty.clone()); - } - "err" => { - if !method_call.args.is_empty() { - return Err(TypeError::new( - "err takes no arguments".to_string(), - method_call.span, - )); - } - return record_expr_type(recorder, expr, err_ty.clone()); - } - _ => {} - } - } - } let (method_module, type_name, receiver_args) = resolve_method_target( &receiver_ty, module_name, diff --git a/capc/src/typeck/lower.rs b/capc/src/typeck/lower.rs index 38e4efb..ea9a181 100644 --- a/capc/src/typeck/lower.rs +++ b/capc/src/typeck/lower.rs @@ -572,6 +572,13 @@ fn lower_expr(expr: &Expr, ctx: &mut LoweringCtx, ret_ty: &Ty) -> Result Result { - let default_expr = lower_expr(&method_call.args[0], ctx, ret_ty)?; - let ok_name = format!("__unwrap_ok_{}", ctx.local_counter); - let ok_local_id = ctx.fresh_local(ok_name.clone(), args[0].clone()); - let ok_pattern = HirPattern::Variant { - variant_name: "Ok".to_string(), - binding: Some(ok_local_id), - }; - let err_pattern = HirPattern::Variant { - variant_name: "Err".to_string(), - binding: None, - }; - let ok_expr = HirExpr::Local(HirLocal { - local_id: ok_local_id, - ty: hir_type_for(args[0].clone(), ctx, method_call.span)?, - span: method_call.span, - }); - let ok_block = HirBlock { - stmts: vec![HirStmt::Expr(HirExprStmt { - expr: ok_expr, - span: method_call.span, - })], - }; - let err_block = HirBlock { - stmts: vec![HirStmt::Expr(HirExprStmt { - expr: default_expr, - span: method_call.span, - })], - }; - return Ok(HirExpr::Match(HirMatch { - expr: Box::new(receiver), - arms: vec![ - HirMatchArm { - pattern: ok_pattern, - body: ok_block, - }, - HirMatchArm { - pattern: err_pattern, - body: err_block, - }, - ], - result_ty: hir_ty, - span: method_call.span, - })); - } - "unwrap_err_or" => { - let default_expr = lower_expr(&method_call.args[0], ctx, ret_ty)?; - let err_name = format!("__unwrap_err_{}", ctx.local_counter); - let err_local_id = ctx.fresh_local(err_name.clone(), args[1].clone()); - let ok_pattern = HirPattern::Variant { - variant_name: "Ok".to_string(), - binding: None, - }; - let err_pattern = HirPattern::Variant { - variant_name: "Err".to_string(), - binding: Some(err_local_id), - }; - let err_expr = HirExpr::Local(HirLocal { - local_id: err_local_id, - ty: hir_type_for(args[1].clone(), ctx, method_call.span)?, - span: method_call.span, - }); - let ok_block = HirBlock { - stmts: vec![HirStmt::Expr(HirExprStmt { - expr: default_expr, - span: method_call.span, - })], - }; - let err_block = HirBlock { - stmts: vec![HirStmt::Expr(HirExprStmt { - expr: err_expr, - span: method_call.span, - })], - }; - return Ok(HirExpr::Match(HirMatch { - expr: Box::new(receiver), - arms: vec![ - HirMatchArm { - pattern: ok_pattern, - body: ok_block, - }, - HirMatchArm { - pattern: err_pattern, - body: err_block, - }, - ], - result_ty: hir_ty, - span: method_call.span, - })); - } - "is_ok" => { - let bool_ty = HirType { - ty: Ty::Builtin(BuiltinType::Bool), - abi: AbiType::Bool, - }; - let true_lit = HirExpr::Literal(HirLiteral { - value: Literal::Bool(true), - ty: bool_ty.clone(), - span: method_call.span, - }); - let false_lit = HirExpr::Literal(HirLiteral { - value: Literal::Bool(false), - ty: bool_ty.clone(), - span: method_call.span, - }); - let ok_pattern = HirPattern::Variant { - variant_name: "Ok".to_string(), - binding: None, - }; - let err_pattern = HirPattern::Variant { - variant_name: "Err".to_string(), - binding: None, - }; - let ok_block = HirBlock { - stmts: vec![HirStmt::Expr(HirExprStmt { - expr: true_lit, - span: method_call.span, - })], - }; - let err_block = HirBlock { - stmts: vec![HirStmt::Expr(HirExprStmt { - expr: false_lit, - span: method_call.span, - })], - }; - return Ok(HirExpr::Match(HirMatch { - expr: Box::new(receiver), - arms: vec![ - HirMatchArm { - pattern: ok_pattern, - body: ok_block, - }, - HirMatchArm { - pattern: err_pattern, - body: err_block, - }, - ], - result_ty: bool_ty, - span: method_call.span, - })); - } - "is_err" => { - let bool_ty = HirType { - ty: Ty::Builtin(BuiltinType::Bool), - abi: AbiType::Bool, - }; - let true_lit = HirExpr::Literal(HirLiteral { - value: Literal::Bool(true), - ty: bool_ty.clone(), - span: method_call.span, - }); - let false_lit = HirExpr::Literal(HirLiteral { - value: Literal::Bool(false), - ty: bool_ty.clone(), - span: method_call.span, - }); - let ok_pattern = HirPattern::Variant { - variant_name: "Ok".to_string(), - binding: None, - }; - let err_pattern = HirPattern::Variant { - variant_name: "Err".to_string(), - binding: None, - }; - let ok_block = HirBlock { - stmts: vec![HirStmt::Expr(HirExprStmt { - expr: false_lit, - span: method_call.span, - })], - }; - let err_block = HirBlock { - stmts: vec![HirStmt::Expr(HirExprStmt { - expr: true_lit, - span: method_call.span, - })], - }; - return Ok(HirExpr::Match(HirMatch { - expr: Box::new(receiver), - arms: vec![ - HirMatchArm { - pattern: ok_pattern, - body: ok_block, - }, - HirMatchArm { - pattern: err_pattern, - body: err_block, - }, - ], - result_ty: bool_ty, - span: method_call.span, - })); - } - "ok" => { - let ok_name = format!("__ok_{}", ctx.local_counter); - let ok_local_id = ctx.fresh_local(ok_name.clone(), args[0].clone()); - let ok_pattern = HirPattern::Variant { - variant_name: "Ok".to_string(), - binding: Some(ok_local_id), - }; - let err_pattern = HirPattern::Variant { - variant_name: "Err".to_string(), - binding: None, - }; - let ok_expr = HirExpr::Local(HirLocal { - local_id: ok_local_id, - ty: hir_type_for(args[0].clone(), ctx, method_call.span)?, - span: method_call.span, - }); - let trap_expr = HirExpr::Trap(HirTrap { - ty: hir_type_for(args[0].clone(), ctx, method_call.span)?, - span: method_call.span, - }); - let ok_block = HirBlock { - stmts: vec![HirStmt::Expr(HirExprStmt { - expr: ok_expr, - span: method_call.span, - })], - }; - let err_block = HirBlock { - stmts: vec![HirStmt::Expr(HirExprStmt { - expr: trap_expr, - span: method_call.span, - })], - }; - return Ok(HirExpr::Match(HirMatch { - expr: Box::new(receiver), - arms: vec![ - HirMatchArm { - pattern: ok_pattern, - body: ok_block, - }, - HirMatchArm { - pattern: err_pattern, - body: err_block, - }, - ], - result_ty: hir_ty, - span: method_call.span, - })); - } - "err" => { - let err_name = format!("__err_{}", ctx.local_counter); - let err_local_id = ctx.fresh_local(err_name.clone(), args[1].clone()); - let ok_pattern = HirPattern::Variant { - variant_name: "Ok".to_string(), - binding: None, - }; - let err_pattern = HirPattern::Variant { - variant_name: "Err".to_string(), - binding: Some(err_local_id), - }; - let err_expr = HirExpr::Local(HirLocal { - local_id: err_local_id, - ty: hir_type_for(args[1].clone(), ctx, method_call.span)?, - span: method_call.span, - }); - let trap_expr = HirExpr::Trap(HirTrap { - ty: hir_type_for(args[1].clone(), ctx, method_call.span)?, - span: method_call.span, - }); - let ok_block = HirBlock { - stmts: vec![HirStmt::Expr(HirExprStmt { - expr: trap_expr, - span: method_call.span, - })], - }; - let err_block = HirBlock { - stmts: vec![HirStmt::Expr(HirExprStmt { - expr: err_expr, - span: method_call.span, - })], - }; - return Ok(HirExpr::Match(HirMatch { - expr: Box::new(receiver), - arms: vec![ - HirMatchArm { - pattern: ok_pattern, - body: ok_block, - }, - HirMatchArm { - pattern: err_pattern, - body: err_block, - }, - ], - result_ty: hir_ty, - span: method_call.span, - })); - } - _ => {} - } - } - } let (method_module, type_name, _) = resolve_method_target( &receiver_ty, ctx.module_name, @@ -1274,30 +986,29 @@ fn lower_pattern( } Pattern::Call { path, binding, span } => { - if path.segments.len() == 1 { - let name = &path.segments[0].item; - if name == "Ok" || name == "Err" { - if let Ty::Path(ty_name, args) = &match_ty.ty { - if ty_name == "sys.result.Result" && args.len() == 2 { - let variant_name = name.clone(); - - let binding_info = if let Some(bind_ident) = binding { - let bind_ty = if variant_name == "Ok" { - args[0].clone() - } else { - args[1].clone() - }; - let local_id = ctx.fresh_local(bind_ident.item.clone(), bind_ty); - Some(local_id) + // Check for Result::Ok/Err variants (both qualified and unqualified) + let variant_name = path.segments.last().map(|s| s.item.as_str()); + if variant_name == Some("Ok") || variant_name == Some("Err") { + if let Ty::Path(ty_name, args) = &match_ty.ty { + if ty_name == "sys.result.Result" && args.len() == 2 { + let variant_name = variant_name.unwrap().to_string(); + + let binding_info = if let Some(bind_ident) = binding { + let bind_ty = if variant_name == "Ok" { + args[0].clone() } else { - None + args[1].clone() }; + let local_id = ctx.fresh_local(bind_ident.item.clone(), bind_ty); + Some(local_id) + } else { + None + }; - return Ok(HirPattern::Variant { - variant_name, - binding: binding_info, - }); - } + return Ok(HirPattern::Variant { + variant_name, + binding: binding_info, + }); } } } diff --git a/capc/src/typeck/mod.rs b/capc/src/typeck/mod.rs index eec646e..08693d7 100644 --- a/capc/src/typeck/mod.rs +++ b/capc/src/typeck/mod.rs @@ -420,6 +420,7 @@ fn resolve_impl_target( use_map: &UseMap, stdlib: &StdlibIndex, struct_map: &HashMap, + enum_map: &HashMap, type_params: &HashSet, module_name: &str, span: Span, @@ -427,6 +428,7 @@ fn resolve_impl_target( let target_ty = lower_type(target, use_map, stdlib, type_params)?; let (impl_module, type_name) = match &target_ty { Ty::Path(target_name, _target_args) => { + // Check struct_map first if let Some(info) = struct_map.get(target_name) { let type_name = target_name .rsplit_once('.') @@ -434,6 +436,18 @@ fn resolve_impl_target( .unwrap_or(target_name) .to_string(); (info.module.clone(), type_name) + // Check enum_map + } else if enum_map.contains_key(target_name) { + let type_name = target_name + .rsplit_once('.') + .map(|(_, t)| t) + .unwrap_or(target_name) + .to_string(); + let mod_part = target_name + .rsplit_once('.') + .map(|(m, _)| m) + .unwrap_or(module_name); + (mod_part.to_string(), type_name) } else if target_name.contains('.') { let (mod_part, type_part) = target_name.rsplit_once('.').ok_or_else(|| { TypeError::new("invalid type path".to_string(), span) @@ -441,9 +455,11 @@ fn resolve_impl_target( (mod_part.to_string(), type_part.to_string()) } else if let Some(info) = struct_map.get(&format!("{module_name}.{target_name}")) { (info.module.clone(), target_name.clone()) + } else if enum_map.contains_key(&format!("{module_name}.{target_name}")) { + (module_name.to_string(), target_name.clone()) } else { return Err(TypeError::new( - "impl target must be a struct type name".to_string(), + "impl target must be a struct or enum type name".to_string(), span, )); } @@ -452,7 +468,7 @@ fn resolve_impl_target( Ty::Builtin(BuiltinType::U8) => ("sys.bytes".to_string(), "u8".to_string()), _ => { return Err(TypeError::new( - "impl target must be a struct type name".to_string(), + "impl target must be a struct or enum type name".to_string(), span, )); } @@ -576,6 +592,7 @@ fn desugar_impl_methods( use_map, stdlib, struct_map, + enum_map, &impl_type_params, module_name, impl_block.span, diff --git a/capc/tests/typecheck.rs b/capc/tests/typecheck.rs index b6b06c2..e624678 100644 --- a/capc/tests/typecheck.rs +++ b/capc/tests/typecheck.rs @@ -127,7 +127,7 @@ fn typecheck_result_unwrap_or_mismatch_fails() { let module = parse_module(&source).expect("parse module"); let stdlib = load_stdlib().expect("load stdlib"); let err = type_check_program(&module, &stdlib, &[]).expect_err("expected type error"); - assert!(err.to_string().contains("unwrap_or type mismatch")); + assert!(err.to_string().contains("argument type mismatch")); } #[test] diff --git a/stdlib/sys/result.cap b/stdlib/sys/result.cap index 9a19f90..e54bf6f 100644 --- a/stdlib/sys/result.cap +++ b/stdlib/sys/result.cap @@ -5,3 +5,33 @@ pub enum Result { Ok(T), Err(E) } + +impl Result { + pub fn is_ok(self) -> bool { + match self { + Result::Ok(_) => { return true } + Result::Err(_) => { return false } + } + } + + pub fn is_err(self) -> bool { + match self { + Result::Ok(_) => { return false } + Result::Err(_) => { return true } + } + } + + pub fn unwrap_or(self, default: T) -> T { + match self { + Result::Ok(val) => { return val } + Result::Err(_) => { return default } + } + } + + pub fn unwrap_err_or(self, default: E) -> E { + match self { + Result::Ok(_) => { return default } + Result::Err(err) => { return err } + } + } +} From 993e805020330c9b77a5ae2c5444398654b50b43 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Thu, 1 Jan 2026 13:18:44 -0800 Subject: [PATCH 16/30] Fix some stuff --- capc/src/parser.rs | 12 ++-- capc/src/typeck/check.rs | 1 + capc/src/typeck/lower.rs | 1 + capc/src/typeck/mod.rs | 21 ++++++- capc/tests/typecheck.rs | 4 +- examples/http_server/http_server.cap | 61 ++++++++++---------- tests/programs/should_pass_result_ok_err.cap | 16 ++--- 7 files changed, 65 insertions(+), 51 deletions(-) diff --git a/capc/src/parser.rs b/capc/src/parser.rs index 641c287..caa07d6 100644 --- a/capc/src/parser.rs +++ b/capc/src/parser.rs @@ -872,8 +872,8 @@ impl Parser { } self.bump(); - // RHS of binary operator always allows struct literals (no ambiguity) - let rhs = self.parse_expr_bp(r_bp, true)?; + // Propagate struct-literal allowance to avoid block ambiguity in no-struct contexts. + let rhs = self.parse_expr_bp(r_bp, allow_struct_literal)?; let span = Span::new(lhs.span().start, rhs.span().end); lhs = Expr::Binary(BinaryExpr { op, @@ -890,8 +890,8 @@ impl Parser { match self.peek_kind() { Some(TokenKind::Minus) => { let start = self.bump().unwrap().span.start; - // Unary operands always allow struct literals (no ambiguity) - let expr = self.parse_expr_bp(7, true)?; + // Propagate struct-literal allowance to avoid block ambiguity in no-struct contexts. + let expr = self.parse_expr_bp(7, allow_struct_literal)?; Ok(Expr::Unary(UnaryExpr { op: UnaryOp::Neg, span: Span::new(start, expr.span().end), @@ -900,8 +900,8 @@ impl Parser { } Some(TokenKind::Bang) => { let start = self.bump().unwrap().span.start; - // Unary operands always allow struct literals (no ambiguity) - let expr = self.parse_expr_bp(7, true)?; + // Propagate struct-literal allowance to avoid block ambiguity in no-struct contexts. + let expr = self.parse_expr_bp(7, allow_struct_literal)?; Ok(Expr::Unary(UnaryExpr { op: UnaryOp::Not, span: Span::new(start, expr.span().end), diff --git a/capc/src/typeck/check.rs b/capc/src/typeck/check.rs index 7505745..2e58b03 100644 --- a/capc/src/typeck/check.rs +++ b/capc/src/typeck/check.rs @@ -1303,6 +1303,7 @@ pub(super) fn check_expr( &receiver_ty, module_name, struct_map, + enum_map, method_call.receiver.span(), )?; let method_fn = format!("{type_name}__{}", method_call.method.item); diff --git a/capc/src/typeck/lower.rs b/capc/src/typeck/lower.rs index ea9a181..7b5eabc 100644 --- a/capc/src/typeck/lower.rs +++ b/capc/src/typeck/lower.rs @@ -684,6 +684,7 @@ fn lower_expr(expr: &Expr, ctx: &mut LoweringCtx, ret_ty: &Ty) -> Result, + enum_map: &HashMap, span: Span, ) -> Result<(String, String, Vec), TypeError> { let base_ty = match receiver_ty { @@ -379,7 +380,7 @@ fn resolve_method_target( } _ => { return Err(TypeError::new( - "method receiver must be a struct value".to_string(), + "method receiver must be a struct or enum value".to_string(), span, )); } @@ -394,6 +395,19 @@ fn resolve_method_target( return Ok((info.module.clone(), type_name, receiver_args.clone())); } + if enum_map.contains_key(receiver_name) { + let type_name = receiver_name + .rsplit_once('.') + .map(|(_, t)| t) + .unwrap_or(receiver_name) + .to_string(); + let mod_part = receiver_name + .rsplit_once('.') + .map(|(m, _)| m) + .unwrap_or(module_name); + return Ok((mod_part.to_string(), type_name, receiver_args.clone())); + } + if receiver_name.contains('.') { let (mod_part, type_part) = receiver_name.rsplit_once('.').ok_or_else(|| { TypeError::new("invalid type path".to_string(), span) @@ -408,9 +422,12 @@ fn resolve_method_target( if let Some(info) = struct_map.get(&format!("{module_name}.{receiver_name}")) { return Ok((info.module.clone(), receiver_name.to_string(), receiver_args.clone())); } + if enum_map.contains_key(&format!("{module_name}.{receiver_name}")) { + return Ok((module_name.to_string(), receiver_name.to_string(), receiver_args.clone())); + } Err(TypeError::new( - format!("unknown struct `{receiver_name}`"), + format!("unknown struct or enum `{receiver_name}`"), span, )) } diff --git a/capc/tests/typecheck.rs b/capc/tests/typecheck.rs index e624678..97a1057 100644 --- a/capc/tests/typecheck.rs +++ b/capc/tests/typecheck.rs @@ -237,7 +237,7 @@ fn typecheck_console_wrong_type() { let stdlib = load_stdlib().expect("load stdlib"); let err = type_check_program(&module, &stdlib, &[]).expect_err("expected type error"); let text = err.to_string(); - assert!(text.contains("method receiver must be a struct value")); + assert!(text.contains("method receiver must be a struct or enum value")); } #[test] @@ -247,7 +247,7 @@ fn typecheck_mint_without_system() { let stdlib = load_stdlib().expect("load stdlib"); let err = type_check_program(&module, &stdlib, &[]).expect_err("expected type error"); let text = err.to_string(); - assert!(text.contains("method receiver must be a struct value")); + assert!(text.contains("method receiver must be a struct or enum value")); } #[test] diff --git a/examples/http_server/http_server.cap b/examples/http_server/http_server.cap index a05c378..ab1a112 100644 --- a/examples/http_server/http_server.cap +++ b/examples/http_server/http_server.cap @@ -40,47 +40,49 @@ fn sanitize_parts(parts: VecString, i: i32, acc: string) -> Result return Ok(acc) } let seg_result = parts[i] - if (seg_result.is_err()) { - return Err(()) + match (seg_result) { + Ok(seg) => { return sanitize_segment(parts, i, acc, seg) } + Err(_) => { return Err(()) } } - return sanitize_segment(parts, i, acc, seg_result.ok()) } fn sanitize_path(raw_path: string) -> Result { let result = sanitize_parts(raw_path.split(47u8), 0, "") - if (result.is_err()) { - return Err(()) + match (result) { + Ok(path) => { + if (path.len() == 0) { + return Ok("index.html") + } + return Ok(path) + } + Err(_) => { return Err(()) } } - let path = result.ok() - if (path.len() == 0) { - return Ok("index.html") - } - return Ok(path) } fn parse_request_line(line: string) -> Result { let parts = line.trim().split(32u8) let method_result = parts[0] - if (method_result.is_err()) { - return Err(()) - } - let method = method_result.ok() - if (method != "GET") { - return Err(()) + match (method_result) { + Ok(method) => { + if (method != "GET") { + return Err(()) + } + } + Err(_) => { return Err(()) } } let path_result = parts[1] - if (path_result.is_err()) { - return Err(()) + match (path_result) { + Ok(raw_path) => { return sanitize_path(strip_query(raw_path)) } + Err(_) => { return Err(()) } } - return sanitize_path(strip_query(path_result.ok())) } fn parse_request_path(req: string) -> Result { let line_result = req.lines()[0] - if (line_result.is_err()) { - return Err(()) + match (line_result) { + Ok(line) => { return parse_request_line(line) } + Err(_) => { return Err(()) } } - return parse_request_line(line_result.ok()) } fn respond_ok(conn: &TcpConn, body: string) -> Result { @@ -98,22 +100,19 @@ fn respond_bad_request(conn: &TcpConn) -> Result { } fn handle_request(conn: &TcpConn, readfs: ReadFS, path: string) -> Result { - let file_result = readfs.read_to_string(path) - if (file_result.is_ok()) { - return respond_ok(conn, file_result.ok()) + match (readfs.read_to_string(path)) { + Ok(body) => { return respond_ok(conn, body) } + Err(_) => { return respond_not_found(conn) } } - return respond_not_found(conn) } fn serve_once(c: Console, net: Net, readfs: ReadFS) -> Result { let listener = net.listen("127.0.0.1", 8080)? let conn = listener.accept()? let req = conn.read(4096)? - let path_result = parse_request_path(req) - if (path_result.is_ok()) { - handle_request(conn, readfs, path_result.ok())? - } else { - respond_bad_request(conn)? + match (parse_request_path(req)) { + Ok(path) => { handle_request(conn, readfs, path)? } + Err(_) => { respond_bad_request(conn)? } } conn.close() return Ok(()) diff --git a/tests/programs/should_pass_result_ok_err.cap b/tests/programs/should_pass_result_ok_err.cap index 9c4bad3..4482671 100644 --- a/tests/programs/should_pass_result_ok_err.cap +++ b/tests/programs/should_pass_result_ok_err.cap @@ -14,18 +14,14 @@ pub fn main(rc: RootCap) -> i32 { let ok_result = make_ok() let err_result = make_err() - if ok_result.is_ok() { - let val = ok_result.ok() - if val != 42 { - return 1 - } + let val = ok_result.unwrap_or(0) + if val != 42 { + return 1 } - if err_result.is_err() { - let e = err_result.err() - if e != 99 { - return 2 - } + let e = err_result.unwrap_err_or(0) + if e != 99 { + return 2 } con.print("ok_err ok") From 7416ad846d7cd348a5d1e40c85c4ed98670dc75d Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Thu, 1 Jan 2026 13:42:06 -0800 Subject: [PATCH 17/30] Fix loop linearity, indexing constraints, and call conv --- FIXME.md | 11 +++++ capc/src/codegen/emit.rs | 24 ++++++----- capc/src/codegen/mod.rs | 16 +++++-- capc/src/parser.rs | 92 +++++++++++++++++++++++----------------- capc/src/typeck/check.rs | 73 +++++++++++++++++++++++++------ capc/src/typeck/mod.rs | 19 ++++++++- 6 files changed, 168 insertions(+), 67 deletions(-) create mode 100644 FIXME.md diff --git a/FIXME.md b/FIXME.md new file mode 100644 index 0000000..02a5383 --- /dev/null +++ b/FIXME.md @@ -0,0 +1,11 @@ +FIXME +===== + +Resolved (current branch) +------------------------- +- break/continue linear-consumption checks now only validate scopes exited by + the loop, not all scopes. +- Slice/MutSlice indexing now requires to match runtime behavior. +- Direct runtime calls now use the target's default calling convention. +- Generic-vs-comparison parsing uses a lookahead for `>` followed by `(` or `{` + to avoid false-positive generic parsing. diff --git a/capc/src/codegen/emit.rs b/capc/src/codegen/emit.rs index 2b13b89..5d608c8 100644 --- a/capc/src/codegen/emit.rs +++ b/capc/src/codegen/emit.rs @@ -841,7 +841,11 @@ fn emit_hir_expr_inner( } // Emit the call - let sig = sig_to_clif(abi_sig, module.isa().pointer_type()); + let sig = sig_to_clif( + abi_sig, + module.isa().pointer_type(), + module.isa().default_call_conv(), + ); let call_symbol = info.runtime_symbol.as_deref().unwrap_or(&info.symbol); let func_id = module .declare_function( @@ -1227,12 +1231,11 @@ fn emit_string_eq( len2: Value, ) -> Result { use cranelift_codegen::ir::{AbiParam, Signature}; - use cranelift_codegen::isa::CallConv; let ptr_ty = module.isa().pointer_type(); // Build signature: (ptr, i64, ptr, i64) -> i8 - let mut sig = Signature::new(CallConv::SystemV); + let mut sig = Signature::new(module.isa().default_call_conv()); sig.params.push(AbiParam::new(ptr_ty)); sig.params.push(AbiParam::new(ir::types::I64)); sig.params.push(AbiParam::new(ptr_ty)); @@ -1358,12 +1361,11 @@ fn emit_string_byte_at( index: Value, ) -> Result { use cranelift_codegen::ir::{AbiParam, Signature}; - use cranelift_codegen::isa::CallConv; let ptr_ty = module.isa().pointer_type(); // Build signature: (ptr, i64, i32) -> u8 - let mut sig = Signature::new(CallConv::SystemV); + let mut sig = Signature::new(module.isa().default_call_conv()); sig.params.push(AbiParam::new(ptr_ty)); sig.params.push(AbiParam::new(ir::types::I64)); sig.params.push(AbiParam::new(ir::types::I32)); @@ -1390,12 +1392,11 @@ fn emit_slice_at( index: Value, ) -> Result { use cranelift_codegen::ir::{AbiParam, Signature}; - use cranelift_codegen::isa::CallConv; let ptr_ty = module.isa().pointer_type(); // Build signature: (handle, i32) -> u8 - let mut sig = Signature::new(CallConv::SystemV); + let mut sig = Signature::new(module.isa().default_call_conv()); sig.params.push(AbiParam::new(ptr_ty)); // Handle is a usize sig.params.push(AbiParam::new(ir::types::I32)); sig.returns.push(AbiParam::new(ir::types::I8)); @@ -1421,12 +1422,11 @@ fn emit_mut_slice_at( index: Value, ) -> Result { use cranelift_codegen::ir::{AbiParam, Signature}; - use cranelift_codegen::isa::CallConv; let ptr_ty = module.isa().pointer_type(); // Build signature: (handle, i32) -> u8 - let mut sig = Signature::new(CallConv::SystemV); + let mut sig = Signature::new(module.isa().default_call_conv()); sig.params.push(AbiParam::new(ptr_ty)); // Handle is a usize sig.params.push(AbiParam::new(ir::types::I32)); sig.returns.push(AbiParam::new(ir::types::I8)); @@ -2836,7 +2836,11 @@ pub(super) fn emit_runtime_wrapper_call( result_out = Some((ok_slot, err_slot, ok_ty.clone(), err_ty.clone())); } - let sig = sig_to_clif(abi_sig, module.isa().pointer_type()); + let sig = sig_to_clif( + abi_sig, + module.isa().pointer_type(), + module.isa().default_call_conv(), + ); let call_symbol = info .runtime_symbol .as_deref() diff --git a/capc/src/codegen/mod.rs b/capc/src/codegen/mod.rs index 4b796b4..db4c937 100644 --- a/capc/src/codegen/mod.rs +++ b/capc/src/codegen/mod.rs @@ -276,13 +276,21 @@ pub fn build_object( .declare_function( &info.symbol, Linkage::Export, - &sig_to_clif(&info.sig, module.isa().pointer_type()), + &sig_to_clif( + &info.sig, + module.isa().pointer_type(), + module.isa().default_call_conv(), + ), ) .map_err(|err| CodegenError::Codegen(err.to_string()))?; let mut ctx = module.make_context(); ctx.func = Function::with_name_signature( ir::UserFuncName::user(0, func_id.as_u32()), - sig_to_clif(&info.sig, module.isa().pointer_type()), + sig_to_clif( + &info.sig, + module.isa().pointer_type(), + module.isa().default_call_conv(), + ), ); let mut builder_ctx = FunctionBuilderContext::new(); @@ -372,8 +380,8 @@ pub fn build_object( } /// Lower a codegen signature into a Cranelift signature. -fn sig_to_clif(sig: &FnSig, ptr_ty: Type) -> Signature { - let mut signature = Signature::new(CallConv::SystemV); +fn sig_to_clif(sig: &FnSig, ptr_ty: Type, call_conv: CallConv) -> Signature { + let mut signature = Signature::new(call_conv); for param in &sig.params { append_ty_params(&mut signature, param, ptr_ty); } diff --git a/capc/src/parser.rs b/capc/src/parser.rs index caa07d6..5020e93 100644 --- a/capc/src/parser.rs +++ b/capc/src/parser.rs @@ -691,6 +691,36 @@ impl Parser { self.parse_expr_bp(0, allow_struct_literal) } + /// Look ahead to see if a `<...>` type-arg list is closed and followed by + /// a call `(` or struct literal `{`. + fn type_args_followed_by_call_or_struct(&self) -> bool { + if self.peek_kind() != Some(TokenKind::Lt) { + return false; + } + let mut depth = 0usize; + let mut idx = self.index; + while idx < self.tokens.len() { + match self.tokens[idx].kind { + TokenKind::Lt => depth += 1, + TokenKind::Gt => { + if depth == 0 { + return false; + } + depth -= 1; + if depth == 0 { + return matches!( + self.tokens.get(idx + 1).map(|t| &t.kind), + Some(TokenKind::LParen) | Some(TokenKind::LBrace) + ); + } + } + _ => {} + } + idx += 1; + } + false + } + fn parse_expr_bp(&mut self, min_bp: u8, allow_struct_literal: bool) -> Result { let mut lhs = self.parse_prefix(allow_struct_literal)?; @@ -802,49 +832,31 @@ impl Parser { // Special handling for '<' which can be type arguments or less-than if self.peek_kind() == Some(TokenKind::Lt) { // Check if this looks like type arguments: path(args) or path{ ... } - if matches!(&lhs, Expr::Path(_) | Expr::FieldAccess(_)) { - // Peek ahead to see if content looks like a type - let looks_like_type = match self.peek_token(1).map(|t| &t.kind) { - Some(TokenKind::Ident) => { - if let Some(tok) = self.peek_token(1) { - let builtin_types = ["i32", "u32", "u8", "bool", "string", "unit"]; - tok.text.starts_with(|c: char| c.is_uppercase()) - || builtin_types.contains(&tok.text.as_str()) - } else { - false - } - } - Some(TokenKind::Star) => true, // pointer type like *u8 - _ => false, - }; - if looks_like_type { - let type_args = self.parse_type_args()?; - if allow_struct_literal && self.peek_kind() == Some(TokenKind::LBrace) { - let path = match lhs { - Expr::Path(p) => p, - Expr::FieldAccess(ref fa) => self.field_access_to_path(fa)?, - _ => unreachable!(), - }; - lhs = self.parse_struct_literal(path, type_args)?; - continue; - } - if self.peek_kind() == Some(TokenKind::LParen) { - lhs = self.finish_call(lhs, type_args)?; - continue; - } - // In no-struct context, if we see LBrace it's the start of a block, not an error - if !allow_struct_literal && self.peek_kind() == Some(TokenKind::LBrace) { - // Type args without call in no-struct context - this is actually ok, - // the LBrace is a block. But we parsed type args that don't belong. - // This is a parse error - generic expressions need parens in this context. - return Err(self.error_current( - "generic expressions in this context require parentheses".to_string(), - )); - } + if matches!(&lhs, Expr::Path(_) | Expr::FieldAccess(_)) + && self.type_args_followed_by_call_or_struct() + { + let type_args = self.parse_type_args()?; + if allow_struct_literal && self.peek_kind() == Some(TokenKind::LBrace) { + let path = match lhs { + Expr::Path(p) => p, + Expr::FieldAccess(ref fa) => self.field_access_to_path(fa)?, + _ => unreachable!(), + }; + lhs = self.parse_struct_literal(path, type_args)?; + continue; + } + if self.peek_kind() == Some(TokenKind::LParen) { + lhs = self.finish_call(lhs, type_args)?; + continue; + } + if !allow_struct_literal && self.peek_kind() == Some(TokenKind::LBrace) { return Err(self.error_current( - "type arguments require a call or struct literal".to_string(), + "generic expressions in this context require parentheses".to_string(), )); } + return Err(self.error_current( + "type arguments require a call or struct literal".to_string(), + )); } // Fall through to treat as less-than comparison } diff --git a/capc/src/typeck/check.rs b/capc/src/typeck/check.rs index 2e58b03..29ce90c 100644 --- a/capc/src/typeck/check.rs +++ b/capc/src/typeck/check.rs @@ -497,7 +497,10 @@ fn check_stmt( break_stmt.span, )); } - ensure_linear_all_consumed(scopes, struct_map, enum_map, break_stmt.span)?; + let depth = scopes.current_loop_depth().ok_or_else(|| { + TypeError::new("break statement outside of loop".to_string(), break_stmt.span) + })?; + ensure_linear_scopes_consumed_from(scopes, depth, struct_map, enum_map, break_stmt.span)?; } Stmt::Continue(continue_stmt) => { if !in_loop { @@ -506,7 +509,16 @@ fn check_stmt( continue_stmt.span, )); } - ensure_linear_all_consumed(scopes, struct_map, enum_map, continue_stmt.span)?; + let depth = scopes.current_loop_depth().ok_or_else(|| { + TypeError::new("continue statement outside of loop".to_string(), continue_stmt.span) + })?; + ensure_linear_scopes_consumed_from( + scopes, + depth, + struct_map, + enum_map, + continue_stmt.span, + )?; } Stmt::If(if_stmt) => { let cond_ty = check_expr( @@ -592,6 +604,7 @@ fn check_stmt( )); } let mut body_scopes = scopes.clone(); + body_scopes.push_loop(); check_block( &while_stmt.body, ret_ty, @@ -606,6 +619,7 @@ fn check_stmt( type_params, true, // inside loop, break/continue allowed )?; + body_scopes.pop_loop(); ensure_affine_states_match( scopes, &body_scopes, @@ -667,6 +681,7 @@ fn check_stmt( Ty::Builtin(BuiltinType::I32), ); + body_scopes.push_loop(); check_block( &for_stmt.body, ret_ty, @@ -681,6 +696,7 @@ fn check_stmt( type_params, true, // inside loop, break/continue allowed )?; + body_scopes.pop_loop(); // Pop the loop variable scope before checking affine states body_scopes.pop_scope(); @@ -864,6 +880,29 @@ fn ensure_linear_scope_consumed( Ok(()) } +/// Enforce that linear locals in scopes starting at a depth are consumed. +fn ensure_linear_scopes_consumed_from( + scopes: &Scopes, + depth: usize, + struct_map: &HashMap, + enum_map: &HashMap, + span: Span, +) -> Result<(), TypeError> { + for scope in scopes.stack.iter().skip(depth) { + for (name, info) in scope { + if type_kind(&info.ty, struct_map, enum_map) == TypeKind::Linear + && info.state != MoveState::Moved + { + return Err(TypeError::new( + format!("linear value `{name}` not consumed"), + span, + )); + } + } + } + Ok(()) +} + /// Enforce that all linear locals across scopes are consumed. fn ensure_linear_all_consumed( scopes: &Scopes, @@ -1860,24 +1899,34 @@ pub(super) fn check_expr( match &object_ty { Ty::Builtin(BuiltinType::String) => Ok(Ty::Builtin(BuiltinType::U8)), Ty::Path(name, args) if name == "Slice" || name == "sys.buffer.Slice" => { - if args.len() == 1 { - Ok(args[0].clone()) - } else { - Err(TypeError::new( + if args.len() != 1 { + return Err(TypeError::new( "Slice requires exactly one type argument".to_string(), index_expr.span, - )) + )); } + if args[0] != Ty::Builtin(BuiltinType::U8) { + return Err(TypeError::new( + "Slice indexing is only supported for Slice".to_string(), + index_expr.span, + )); + } + Ok(Ty::Builtin(BuiltinType::U8)) } Ty::Path(name, args) if name == "MutSlice" || name == "sys.buffer.MutSlice" => { - if args.len() == 1 { - Ok(args[0].clone()) - } else { - Err(TypeError::new( + if args.len() != 1 { + return Err(TypeError::new( "MutSlice requires exactly one type argument".to_string(), index_expr.span, - )) + )); + } + if args[0] != Ty::Builtin(BuiltinType::U8) { + return Err(TypeError::new( + "MutSlice indexing is only supported for MutSlice".to_string(), + index_expr.span, + )); } + Ok(Ty::Builtin(BuiltinType::U8)) } // Vec types return Result Ty::Path(name, _) if name == "VecString" || name == "sys.vec.VecString" => { diff --git a/capc/src/typeck/mod.rs b/capc/src/typeck/mod.rs index 31a6f14..1d571d7 100644 --- a/capc/src/typeck/mod.rs +++ b/capc/src/typeck/mod.rs @@ -212,6 +212,8 @@ struct Scopes { /// Stack of scopes, where each scope is a map from name to local info. /// The last element is the innermost (current) scope. stack: Vec>, + /// Stack of scope depths that mark loop bodies for break/continue checks. + loop_stack: Vec, } impl Scopes { @@ -280,7 +282,22 @@ impl Scopes { }, ); } - Scopes { stack: vec![scope] } + Scopes { + stack: vec![scope], + loop_stack: Vec::new(), + } + } + + fn push_loop(&mut self) { + self.loop_stack.push(self.stack.len()); + } + + fn pop_loop(&mut self) { + self.loop_stack.pop(); + } + + fn current_loop_depth(&self) -> Option { + self.loop_stack.last().copied() } fn contains(&self, name: &str) -> bool { From 3f2aca389e3032ddd09a25b2a81c440df7612198 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Thu, 1 Jan 2026 13:43:33 -0800 Subject: [PATCH 18/30] Add docs and TODO list --- TODO.md | 5 + docs/ATTENUATION.md | 95 ++++++++++++++ docs/TUTORIAL.md | 302 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 402 insertions(+) create mode 100644 TODO.md create mode 100644 docs/ATTENUATION.md create mode 100644 docs/TUTORIAL.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..6c6f31b --- /dev/null +++ b/TODO.md @@ -0,0 +1,5 @@ +Implement Result and is_ok and friends as stdlib functions with geneircs, not weird compile rhardcoded stuff. + +Stdlib types for generics instead of Vec32, VecString, etc + +Have codex review the claude generated commits diff --git a/docs/ATTENUATION.md b/docs/ATTENUATION.md new file mode 100644 index 0000000..4f639a9 --- /dev/null +++ b/docs/ATTENUATION.md @@ -0,0 +1,95 @@ +Yep — Austral’s capability system is explicitly hierarchical / attenuating: you derive a narrower capability only by providing proof of a broader one (e.g., Filesystem -> Path(root) -> Path(subdir/file)), and you can’t “go back up.”  + +Here’s a clean way to model the same thing in capc with what you’ve already built (affine locals + “move whole struct on affine-field move”). + +1) Make “caps” affine + non-forgeable + +Use `capability struct` as “this is a capability/handle.” That gives you: + • cannot copy (your Token test) + • cannot fabricate (capability types are opaque: no public fields / no user construction outside the defining module) + +Capability types default to affine unless marked `copy` or `linear`. + +2) Encode attenuation as “only downward constructors” + +Design the stdlib so the only way to get a capability is from a stronger one, and the API only lets you narrow. + +A minimal pattern: + +capability struct RootCap +capability struct Filesystem +capability struct Dir +capability struct FileRead +capability struct FileWrite + +module fs { + // mint broad cap from root (borrow root so you can mint others too) + pub fn filesystem(root: &RootCap) -> Filesystem + + // attenuate: Filesystem -> Dir(root) + pub fn root_dir(fs: &Filesystem) -> Dir + + // attenuate: Dir -> Dir(subdir) (consume Dir to avoid “backtracking” unless you re-mint) + pub fn subdir(dir: Dir, name: string) -> Dir + + // attenuate: Dir -> File caps + pub fn open_read(dir: &Dir, name: string) -> FileRead + pub fn open_write(dir: &Dir, name: string) -> FileWrite + + pub fn read(f: FileRead) -> string + pub fn write(f: FileWrite, s: string) -> unit +} + +Notes: + • Use borrows (&T) for “authority checks / minting” and moves (T) for “consuming path-like capabilities.” That matches the Austral-style feel: you can derive from a reference, but the derived things themselves are linear-ish.  + • If you don’t have & in the language yet, you can still do attenuation with moves only, but it gets annoying (you’ll constantly lose the parent cap). Borrowing is the ergonomic escape hatch that doesn’t require Rust’s full borrow checker if you keep it simple (read-only refs, no aliasing mutation). + +3) Why your Token test is not “too restrictive” + +For a capability/handle, this is exactly the point: + +capability struct Token + +pub fn main() -> i32 { + let t = Token{} + let u = t + let v = t // should fail + return 0 +} + +It’s only “too restrictive” if Token is meant to be data (copyable value). In that case, don’t make it capability/opaque, or give it a non-affine representation (plain struct of copyable fields). + +4) The tests that actually prove “caps can’t be reused” + +You want tests that cover all the ways a user might accidentally duplicate authority: + +A. Local moves (baseline) + • move into another binding (your Token test) + • move via return + • move via function arg / method receiver + +B. Control-flow joins (the hard part you just implemented) + • if: move in one branch then use after → should fail + • if: move in both branches → after join it’s moved → use after should fail + • match: same as if but N arms + • while: moving an outer affine inside loop → should fail (your conservative rule) + +C. Composition (affine-by-containment) + • struct containing an affine field becomes affine (you already enforce by is_affine_type) + • enum payload containing affine becomes affine + +D. Projection / field access (the “sneaky dup”) + • your Holder{ cap: Cap } test is perfect: + • let a = h.cap; let b = h.cap; must fail + • also add nested: h.inner.cap and h.cap.subcap shapes (to exercise your leftmost_local_in_chain) + +E. Attenuation correctness (when you add it) + • can’t call a privileged API without the right capability type (type mismatch) + • can’t widen: there should be no function that maps Dir -> Filesystem in safe stdlib + • can’t mint without root: fs::filesystem(&fake_root) should be impossible because you can’t fabricate RootCap + +If you want one “big picture” test that demonstrates the point of all this: write a tiny “untrusted dependency” module that only receives a Dir (logs directory), and verify it cannot read /etc/passwd because it can’t obtain a Filesystem or a parent path. + +⸻ + +If you tell me what your reference/borrow syntax is (or if it exists yet), I can translate the attenuation API into exactly your AST/stdlib conventions and suggest 5–10 concrete .cap test programs (pass + fail) that lock the model in. diff --git a/docs/TUTORIAL.md b/docs/TUTORIAL.md new file mode 100644 index 0000000..6e6722a --- /dev/null +++ b/docs/TUTORIAL.md @@ -0,0 +1,302 @@ +# Capable in 15 Minutes + +Capable is a small capability-secure systems language. The main idea: authority is a value. If you didn't receive a capability, you can't do the thing. + +This tutorial is a quick tour of the current language slice and the capability model. + +## 1) Hello, console + +```cap +module hello +use sys::system + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + c.println("hello") + return 0 +} +``` + +`RootCap` is the root authority passed to `main`. It can mint narrower capabilities (console, filesystem, etc.). + +## 2) Basic syntax + +```cap +module basics + +pub fn add(a: i32, b: i32) -> i32 { + return a + b +} + +pub fn main() -> i32 { + let x = 1 + let y: i32 = 2 + if (x < y) { + return add(x, y) + } else { + return 0 + } +} +``` + +- Statements: `let`, assignment, `if`, `while`, `return`, `match`. +- Expressions: literals, calls, binary ops, unary ops, method calls. +- Modules + imports: `module ...` and `use ...` (aliases by last path segment). +- If a function returns `unit`, you can omit the `-> unit` annotation. +- Integer arithmetic traps on overflow. +- Variable shadowing is not allowed. + +## 3) Structs and enums + +```cap +module types + +struct Pair { left: i32, right: i32 } + +enum Color { Red, Green, Blue } +``` + +Structs and enums are nominal types. Enums are currently unit variants only. + +## 4) Methods + +Methods are defined in `impl` blocks and lower to `Type__method` at compile time. + +```cap +module methods + +struct Pair { left: i32, right: i32 } + +impl Pair { + pub fn sum(self) -> i32 { return self.left + self.right } + pub fn add(self, x: i32) -> i32 { return self.sum() + x } + pub fn peek(self: &Pair) -> i32 { return self.left } +} +``` + +Method receivers can be `self` (move) or `self: &T` (borrow‑lite, read‑only). + +## 5) Results, match, and `?` + +```cap +module results + +pub fn main() -> i32 { + let ok: Result[i32, i32] = Ok(10) + match ok { + Ok(x) => { return x } + Err(e) => { return 0 } + } +} +``` + +`Result[T, E]` is the only generic type today and is special-cased by the compiler. + +Inside a function that returns `Result`, you can use `?` to unwrap or return early: + +```cap +module results_try + +fn read_value() -> Result[i32, i32] { + return Ok(7) +} + +fn use_value() -> Result[i32, i32] { + let v = read_value()? + return Ok(v + 1) +} +``` + +You can also unwrap with defaults: + +```cap +let v = make().unwrap_or(0) +let e = make().unwrap_err_or(0) +``` + +Matches must be exhaustive; use `_` to cover the rest: + +```cap +match flag { + true => { } + false => { } +} +``` + +## 6) Capabilities and attenuation + +Capabilities live in `sys.*` and are declared with the `capability` keyword (capability types are opaque). You can only get them from `RootCap`. + +```cap +module read_config +use sys::system +use sys::fs + +pub fn main(rc: RootCap) -> i32 { + let fs = rc.mint_filesystem("./config") + let dir = fs.root_dir() + let file = dir.open_read("app.txt") + + match file.read_to_string() { + Ok(s) => { rc.mint_console().println(s); return 0 } + Err(e) => { return 1 } + } +} +``` + +This is attenuation: each step narrows authority. There is no safe API to widen back. + +To make attenuation one-way at compile time, any method that returns a capability must take `self` by value. Methods that take `&self` cannot return capabilities. + +Example of what is rejected (and why): + +```cap +capability struct Dir +capability struct FileRead + +impl Dir { + pub fn open(self: &Dir, name: string) -> FileRead { + let file = self.open_read(name) + return file + } +} +``` + +Why this is rejected: + +- `Dir` can read many files (more power). +- `FileRead` can read one file (less power). +- The bad example lets you keep the more powerful `Dir` and also get a `FileRead`. +- We want “one-way” attenuation: when you make something less powerful, you give up the more powerful one. + +So methods that return capabilities must take `self` by value, which consumes the old capability. + +## 7) Capability, opaque, copy, affine, linear + +`capability struct` is the explicit “this is an authority token” marker. Capability types are always opaque (no public fields, no user construction) and default to affine unless marked `copy` or `linear`. This exists so the capability surface is obvious in code and the compiler can enforce one‑way attenuation (methods returning capabilities must take `self` by value). + +Structs can declare their kind: + +```cap +capability struct Token // affine by default (move-only) +copy capability struct RootCap // unrestricted (copyable) +linear capability struct FileRead // must be consumed +``` + +Kinds: + +- **Unrestricted** (copy): can be reused freely. +- **Affine** (default for capability/opaque): move-only, dropping is OK. +- **Linear**: move-only and must be consumed on all paths. + +Use `capability struct` for authority-bearing tokens. Use `opaque struct` for unforgeable data types that aren’t capabilities. + +In the current stdlib: + +- `copy capability`: `RootCap`, `Console`, `Args` +- `copy opaque`: `Alloc`, `Buffer`, `Slice`, `MutSlice`, `VecU8`, `VecI32`, `VecString` +- `capability` (affine): `ReadFS`, `Filesystem`, `Dir`, `Stdin` +- `linear capability`: `FileRead` + +## 8) Moves and use-after-move + +```cap +module moves + +capability struct Token + +pub fn main() -> i32 { + let t = Token{} + let u = t + let v = t // error: use of moved value + return 0 +} +``` + +Affine and linear values cannot be used after move. If you move in one branch, it's moved after the join. + +## 9) Linear must be consumed + +```cap +module linear + +linear capability struct Ticket + +pub fn main() -> i32 { + let t = Ticket{} + drop(t) // consumes t + return 0 +} +``` + +Linear values must be consumed along every path. You can consume them with a terminal method (like `FileRead.close()` or `read_to_string()`), or with `drop(x)` as a last resort. + +## 10) Borrow-lite: &T parameters + +There is a small borrow feature for read-only access in function parameters and locals. + +```cap +module borrow + +capability struct Cap + +impl Cap { + pub fn ping(self: &Cap) -> i32 { return 1 } +} + +pub fn twice(c: &Cap) -> i32 { + let a = c.ping() + let b = c.ping() + return a + b +} +``` + +Rules: + +- `&T` is allowed on parameters and locals. +- Reference locals must be initialized from another local value. +- References cannot be stored in structs, enums, or returned. +- References are read-only: they can only satisfy `&T` parameters. +- Passing a value to `&T` implicitly borrows it. + +This avoids a full borrow checker while making non-consuming observers ergonomic. + +## 11) Safety boundary + +`package safe` is default. Raw pointers and extern calls require `package unsafe`. + +```cap +package unsafe +module ffi + +extern fn some_ffi(x: i32) -> i32 +``` + +## 12) Raw pointers and unsafe + +Raw pointers are available as `*T`, but **only** in `package unsafe`. + +```cap +package unsafe +module pointers + +pub fn main(rc: RootCap) -> i32 { + let alloc = rc.mint_alloc_default() + let ptr: *u8 = alloc.malloc(16) + alloc.free(ptr) + return 0 +} +``` + +There is no borrow checker for pointers. Use them only inside `package unsafe`. + +## 13) What exists today (quick list) + +- Methods, modules, enums, match, while, if +- Opaque capability handles in `sys.*` +- Linear/affine checking with control-flow joins +- Borrow-lite `&T` parameters + +--- + +That should be enough to read and write small Capable programs, and understand how attenuation and linearity fit together. From fc93b8727659b14d99a53af18fe91c0e8b23ad0d Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Thu, 1 Jan 2026 14:15:20 -0800 Subject: [PATCH 19/30] Add never type and Vec specializations --- TODO.md | 9 ++-- capc/src/codegen/emit.rs | 17 +++++- capc/src/typeck/check.rs | 47 +++++++++++++--- capc/src/typeck/lower.rs | 5 +- capc/src/typeck/mod.rs | 56 ++++++++++++++++++-- capc/src/typeck/monomorphize.rs | 2 + examples/http_server/http_server.cap | 4 +- examples/sort/sort.cap | 4 +- examples/uniq/uniq.cap | 2 +- stdlib/sys/buffer.cap | 12 ++--- stdlib/sys/fs.cap | 8 +-- stdlib/sys/result.cap | 14 +++++ stdlib/sys/string.cap | 8 +-- stdlib/sys/vec.cap | 1 + tests/programs/should_pass_result_ok_err.cap | 16 +++--- 15 files changed, 159 insertions(+), 46 deletions(-) diff --git a/TODO.md b/TODO.md index 6c6f31b..347d1ca 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,4 @@ -Implement Result and is_ok and friends as stdlib functions with geneircs, not weird compile rhardcoded stuff. - -Stdlib types for generics instead of Vec32, VecString, etc - -Have codex review the claude generated commits +Done: +- Result + is_ok/is_err/ok/err/unwrap_* implemented in stdlib (panic uses never type). +- Stdlib APIs updated to use Vec (compiler maps Vec/Vec/Vec to VecU8/VecI32/VecString). +- Codex reviewed Claude-generated commits. diff --git a/capc/src/codegen/emit.rs b/capc/src/codegen/emit.rs index 5d608c8..2eb7478 100644 --- a/capc/src/codegen/emit.rs +++ b/capc/src/codegen/emit.rs @@ -207,6 +207,19 @@ fn emit_hir_stmt_inner( return Ok(Flow::Terminated); } HirStmt::Expr(expr_stmt) => { + if matches!(expr_stmt.expr, crate::hir::HirExpr::Trap(_)) { + let _ = emit_hir_expr( + builder, + &expr_stmt.expr, + locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + )?; + return Ok(Flow::Terminated); + } // Special handling for match expressions that might diverge if let crate::hir::HirExpr::Match(match_expr) = &expr_stmt.expr { if matches!( @@ -1594,7 +1607,7 @@ fn store_value_by_ty( let ptr_ty = module.isa().pointer_type(); match &ty.ty { Ty::Builtin(b) => match b { - BuiltinType::Unit => Ok(()), + BuiltinType::Unit | BuiltinType::Never => Ok(()), BuiltinType::I32 | BuiltinType::U32 => { let ValueRepr::Single(val) = value else { return Err(CodegenError::Unsupported("store i32".to_string())); @@ -1761,7 +1774,7 @@ fn load_value_by_ty( let ptr_ty = module.isa().pointer_type(); match &ty.ty { Ty::Builtin(b) => match b { - BuiltinType::Unit => Ok(ValueRepr::Unit), + BuiltinType::Unit | BuiltinType::Never => Ok(ValueRepr::Unit), BuiltinType::I32 | BuiltinType::U32 => Ok(ValueRepr::Single( builder.ins().load(ir::types::I32, MemFlags::new(), addr, 0), )), diff --git a/capc/src/typeck/check.rs b/capc/src/typeck/check.rs index 29ce90c..3112b39 100644 --- a/capc/src/typeck/check.rs +++ b/capc/src/typeck/check.rs @@ -385,7 +385,10 @@ fn check_stmt( } else { false }; - if annot_ty != expr_ty && !matches_ref { + if annot_ty != expr_ty + && !matches_ref + && !matches!(expr_ty, Ty::Builtin(BuiltinType::Never)) + { return Err(TypeError::new( format!("type mismatch: expected {annot_ty:?}, found {expr_ty:?}"), let_stmt.span, @@ -453,7 +456,7 @@ fn check_stmt( module_name, type_params, )?; - if expr_ty != existing { + if expr_ty != existing && !matches!(expr_ty, Ty::Builtin(BuiltinType::Never)) { return Err(TypeError::new( format!( "assignment type mismatch: expected {existing:?}, found {expr_ty:?}" @@ -483,6 +486,10 @@ fn check_stmt( Ty::Builtin(BuiltinType::Unit) }; if &expr_ty != ret_ty { + if matches!(expr_ty, Ty::Builtin(BuiltinType::Never)) { + ensure_linear_all_consumed(scopes, struct_map, enum_map, ret_stmt.span)?; + return Ok(()); + } return Err(TypeError::new( format!("return type mismatch: expected {ret_ty:?}, found {expr_ty:?}"), ret_stmt.span, @@ -1076,8 +1083,7 @@ pub(super) fn check_expr( )); } // panic() is a diverging expression - it never returns. - // We type it as unit but it will trap at runtime. - return record_expr_type(recorder, expr, Ty::Builtin(BuiltinType::Unit)); + return record_expr_type(recorder, expr, Ty::Builtin(BuiltinType::Never)); } if name == "Ok" || name == "Err" { if call.args.len() != 1 { @@ -1202,7 +1208,10 @@ pub(super) fn check_expr( } else { false }; - if &arg_ty != expected_inner && !matches_ref { + if &arg_ty != expected_inner + && !matches_ref + && !matches!(arg_ty, Ty::Builtin(BuiltinType::Never)) + { return Err(TypeError::new( format!("argument type mismatch: expected {expected:?}, found {arg_ty:?}"), arg.span(), @@ -1312,7 +1321,10 @@ pub(super) fn check_expr( } else { false }; - if &arg_ty != expected_inner && !matches_ref { + if &arg_ty != expected_inner + && !matches_ref + && !matches!(arg_ty, Ty::Builtin(BuiltinType::Never)) + { return Err(TypeError::new( format!( "argument type mismatch: expected {expected:?}, found {arg_ty:?}" @@ -1521,7 +1533,10 @@ pub(super) fn check_expr( } else { false }; - if &arg_ty != expected_inner && !matches_ref { + if &arg_ty != expected_inner + && !matches_ref + && !matches!(arg_ty, Ty::Builtin(BuiltinType::Never)) + { return Err(TypeError::new( format!("argument type mismatch: expected {expected:?}, found {arg_ty:?}"), arg.span(), @@ -1621,6 +1636,10 @@ pub(super) fn check_expr( || left == Ty::Builtin(BuiltinType::I64)) { Ok(left) + } else if matches!(left, Ty::Builtin(BuiltinType::Never)) + || matches!(right, Ty::Builtin(BuiltinType::Never)) + { + Ok(Ty::Builtin(BuiltinType::Never)) } else if left != right && is_numeric_type(&left) && is_numeric_type(&right) { Err(TypeError::new( "implicit numeric conversions are not allowed".to_string(), @@ -1636,6 +1655,10 @@ pub(super) fn check_expr( BinaryOp::Eq | BinaryOp::Neq => { if left == right { Ok(Ty::Builtin(BuiltinType::Bool)) + } else if matches!(left, Ty::Builtin(BuiltinType::Never)) + || matches!(right, Ty::Builtin(BuiltinType::Never)) + { + Ok(Ty::Builtin(BuiltinType::Bool)) } else if left != right && is_numeric_type(&left) && is_numeric_type(&right) { Err(TypeError::new( "implicit numeric conversions are not allowed".to_string(), @@ -1651,6 +1674,10 @@ pub(super) fn check_expr( BinaryOp::Lt | BinaryOp::Lte | BinaryOp::Gt | BinaryOp::Gte => { if left == right && is_orderable_type(&left) { Ok(Ty::Builtin(BuiltinType::Bool)) + } else if matches!(left, Ty::Builtin(BuiltinType::Never)) + || matches!(right, Ty::Builtin(BuiltinType::Never)) + { + Ok(Ty::Builtin(BuiltinType::Bool)) } else if left != right && is_numeric_type(&left) && is_numeric_type(&right) { Err(TypeError::new( "implicit numeric conversions are not allowed".to_string(), @@ -2084,7 +2111,11 @@ fn check_match_expr_value( arm_scope.pop_scope(); arm_scopes.push(arm_scope); if let Some(prev) = &result_ty { - if prev != &arm_ty { + if matches!(prev, Ty::Builtin(BuiltinType::Never)) { + result_ty = Some(arm_ty); + } else if matches!(arm_ty, Ty::Builtin(BuiltinType::Never)) { + // Keep the previous type; never can coerce to any type. + } else if prev != &arm_ty { return Err(TypeError::new( format!("match arm type mismatch: expected {prev:?}, found {arm_ty:?}"), arm.body.span, diff --git a/capc/src/typeck/lower.rs b/capc/src/typeck/lower.rs index 7b5eabc..3fedbb5 100644 --- a/capc/src/typeck/lower.rs +++ b/capc/src/typeck/lower.rs @@ -14,8 +14,8 @@ use crate::hir::{ use super::{ build_type_params, check, function_key, lower_type, resolve_enum_variant, resolve_method_target, - resolve_type_name, BuiltinType, EnumInfo, FunctionSig, FunctionTypeTables, SpanExt, - StdlibIndex, StructInfo, Ty, TypeTable, UseMap, + resolve_type_name, EnumInfo, FunctionSig, FunctionTypeTables, SpanExt, StdlibIndex, StructInfo, + Ty, TypeTable, UseMap, }; /// Context for HIR lowering (uses the type checker as source of truth). @@ -464,6 +464,7 @@ fn abi_type_for(ty: &Ty, ctx: &LoweringCtx, span: Span) -> Result Ok(AbiType::Bool), BuiltinType::String => Ok(AbiType::String), BuiltinType::Unit => Ok(AbiType::Unit), + BuiltinType::Never => Ok(AbiType::Unit), }, Ty::Ptr(_) => Ok(AbiType::Ptr), Ty::Ref(inner) => abi_type_for(inner, ctx, span), diff --git a/capc/src/typeck/mod.rs b/capc/src/typeck/mod.rs index 1d571d7..9ed2345 100644 --- a/capc/src/typeck/mod.rs +++ b/capc/src/typeck/mod.rs @@ -19,8 +19,8 @@ use crate::ast::*; use crate::error::TypeError; use crate::hir::HirModule; -pub(super) const RESERVED_TYPE_PARAMS: [&str; 7] = [ - "i32", "i64", "u32", "u8", "bool", "string", "unit", +pub(super) const RESERVED_TYPE_PARAMS: [&str; 8] = [ + "i32", "i64", "u32", "u8", "bool", "string", "unit", "never", ]; /// Resolved type used after lowering. No spans, fully qualified paths. @@ -90,6 +90,7 @@ pub enum BuiltinType { Bool, String, Unit, + Never, } pub(super) fn function_key(module_name: &str, func_name: &str) -> String { @@ -711,17 +712,64 @@ fn lower_type( "bool" => Some(BuiltinType::Bool), "string" => Some(BuiltinType::String), "unit" => Some(BuiltinType::Unit), + "never" => Some(BuiltinType::Never), _ => None, }; if let Some(builtin) = builtin { return Ok(Ty::Builtin(builtin)); } + let resolved_joined = resolved.join("."); let alias = resolve_type_name(path, use_map, stdlib); - if alias != resolved.join(".") { - return Ok(Ty::Path(alias, args)); + let joined = if alias != resolved_joined { + alias + } else { + resolved_joined + }; + if joined == "Vec" || joined == "sys.vec.Vec" { + if args.len() != 1 { + return Err(TypeError::new( + format!("Vec expects 1 type argument, found {}", args.len()), + path.span, + )); + } + let elem = &args[0]; + let vec_name = match elem { + Ty::Builtin(BuiltinType::U8) => "sys.vec.VecU8", + Ty::Builtin(BuiltinType::I32) => "sys.vec.VecI32", + Ty::Builtin(BuiltinType::String) => "sys.vec.VecString", + _ => { + return Err(TypeError::new( + "Vec only supports u8, i32, and string element types".to_string(), + path.span, + )) + } + }; + return Ok(Ty::Path(vec_name.to_string(), Vec::new())); } + return Ok(Ty::Path(joined, args)); } let joined = path_segments.join("."); + if joined == "Vec" || joined == "sys.vec.Vec" { + if args.len() != 1 { + return Err(TypeError::new( + format!("Vec expects 1 type argument, found {}", args.len()), + path.span, + )); + } + let elem = &args[0]; + let vec_name = match elem { + Ty::Builtin(BuiltinType::U8) => "sys.vec.VecU8", + Ty::Builtin(BuiltinType::I32) => "sys.vec.VecI32", + Ty::Builtin(BuiltinType::String) => "sys.vec.VecString", + _ => { + return Err(TypeError::new( + "Vec only supports u8, i32, and string element types".to_string(), + path.span, + )) + } + }; + return Ok(Ty::Path(vec_name.to_string(), Vec::new())); + } Ok(Ty::Path(joined, args)) } } diff --git a/capc/src/typeck/monomorphize.rs b/capc/src/typeck/monomorphize.rs index c2b97c1..42fcba8 100644 --- a/capc/src/typeck/monomorphize.rs +++ b/capc/src/typeck/monomorphize.rs @@ -854,6 +854,7 @@ impl MonoCtx { BuiltinType::Bool => Ok(AbiType::Bool), BuiltinType::String => Ok(AbiType::String), BuiltinType::Unit => Ok(AbiType::Unit), + BuiltinType::Never => Ok(AbiType::Unit), }, Ty::Ptr(_) => Ok(AbiType::Ptr), Ty::Ref(inner) => self.abi_type_for(module, inner), @@ -1062,6 +1063,7 @@ fn mangle_type(ty: &Ty) -> String { crate::typeck::BuiltinType::Bool => "bool".to_string(), crate::typeck::BuiltinType::String => "string".to_string(), crate::typeck::BuiltinType::Unit => "unit".to_string(), + crate::typeck::BuiltinType::Never => "never".to_string(), }, Ty::Ptr(inner) => format!("ptr_{}", mangle_type(inner)), Ty::Ref(inner) => format!("ref_{}", mangle_type(inner)), diff --git a/examples/http_server/http_server.cap b/examples/http_server/http_server.cap index ab1a112..23abef0 100644 --- a/examples/http_server/http_server.cap +++ b/examples/http_server/http_server.cap @@ -19,7 +19,7 @@ fn strip_query(raw_path: string) -> string { return raw_path.split(63u8)[0].unwrap_or("") } -fn sanitize_segment(parts: VecString, i: i32, acc: string, seg: string) -> Result { +fn sanitize_segment(parts: Vec, i: i32, acc: string, seg: string) -> Result { if (seg.len() == 0) { return sanitize_parts(parts, i + 1, acc) } @@ -35,7 +35,7 @@ fn sanitize_segment(parts: VecString, i: i32, acc: string, seg: string) -> Resul return sanitize_parts(parts, i + 1, fs::join(acc, seg)) } -fn sanitize_parts(parts: VecString, i: i32, acc: string) -> Result { +fn sanitize_parts(parts: Vec, i: i32, acc: string) -> Result { if (i >= parts.len()) { return Ok(acc) } diff --git a/examples/sort/sort.cap b/examples/sort/sort.cap index 5710914..679c504 100644 --- a/examples/sort/sort.cap +++ b/examples/sort/sort.cap @@ -33,14 +33,14 @@ fn str_lt(a: string, b: string) -> bool { } // Compare lines at indices i and j -fn line_lt(lines: VecString, i: i32, j: i32) -> bool { +fn line_lt(lines: Vec, i: i32, j: i32) -> bool { let line_i = lines[i].unwrap_or("") let line_j = lines[j].unwrap_or("") return str_lt(line_i, line_j) } // Insertion sort on indices -fn sort_indices(lines: VecString, indices: VecI32) -> unit { +fn sort_indices(lines: Vec, indices: Vec) -> unit { let n = indices.len() for i in 1..n { let j = i diff --git a/examples/uniq/uniq.cap b/examples/uniq/uniq.cap index 6a7ac4a..0ff818f 100644 --- a/examples/uniq/uniq.cap +++ b/examples/uniq/uniq.cap @@ -7,7 +7,7 @@ use sys::io use sys::string use sys::vec -fn should_print(lines: VecString, i: i32) -> bool { +fn should_print(lines: Vec, i: i32) -> bool { if (i == 0) { return true } diff --git a/stdlib/sys/buffer.cap b/stdlib/sys/buffer.cap index ef92dcd..37891d9 100644 --- a/stdlib/sys/buffer.cap +++ b/stdlib/sys/buffer.cap @@ -44,27 +44,27 @@ impl Alloc { return () } - pub fn vec_u8_new(self) -> vec::VecU8 { + pub fn vec_u8_new(self) -> vec::Vec { return () } - pub fn vec_u8_free(self, v: vec::VecU8) -> unit { + pub fn vec_u8_free(self, v: vec::Vec) -> unit { return () } - pub fn vec_i32_new(self) -> vec::VecI32 { + pub fn vec_i32_new(self) -> vec::Vec { return () } - pub fn vec_i32_free(self, v: vec::VecI32) -> unit { + pub fn vec_i32_free(self, v: vec::Vec) -> unit { return () } - pub fn vec_string_new(self) -> vec::VecString { + pub fn vec_string_new(self) -> vec::Vec { return () } - pub fn vec_string_free(self, v: vec::VecString) -> unit { + pub fn vec_string_free(self, v: vec::Vec) -> unit { return () } } diff --git a/stdlib/sys/fs.cap b/stdlib/sys/fs.cap index 900c18c..3882215 100644 --- a/stdlib/sys/fs.cap +++ b/stdlib/sys/fs.cap @@ -15,11 +15,11 @@ impl ReadFS { return () } - pub fn read_bytes(self, path: string) -> Result { + pub fn read_bytes(self, path: string) -> Result, FsErr> { return Err(FsErr::IoError) } - pub fn list_dir(self, path: string) -> Result { + pub fn list_dir(self, path: string) -> Result, FsErr> { return Err(FsErr::IoError) } @@ -51,11 +51,11 @@ impl Dir { return () } - pub fn read_bytes(self, name: string) -> Result { + pub fn read_bytes(self, name: string) -> Result, FsErr> { return Err(FsErr::IoError) } - pub fn list_dir(self) -> Result { + pub fn list_dir(self) -> Result, FsErr> { return Err(FsErr::IoError) } diff --git a/stdlib/sys/result.cap b/stdlib/sys/result.cap index e54bf6f..7488497 100644 --- a/stdlib/sys/result.cap +++ b/stdlib/sys/result.cap @@ -34,4 +34,18 @@ impl Result { Result::Err(err) => { return err } } } + + pub fn ok(self) -> T { + match self { + Result::Ok(val) => { return val } + Result::Err(_) => { panic() } + } + } + + pub fn err(self) -> E { + match self { + Result::Ok(_) => { panic() } + Result::Err(err) => { return err } + } + } } diff --git a/stdlib/sys/string.cap b/stdlib/sys/string.cap index b90721e..c698489 100644 --- a/stdlib/sys/string.cap +++ b/stdlib/sys/string.cap @@ -27,17 +27,17 @@ impl string { } /// Intrinsic; implemented by the runtime. - pub fn split_whitespace(self) -> VecString { + pub fn split_whitespace(self) -> Vec { return () } /// Intrinsic; implemented by the runtime. - pub fn lines(self) -> VecString { + pub fn lines(self) -> Vec { return () } /// Intrinsic; implemented by the runtime. - pub fn split(self, delim: u8) -> VecString { + pub fn split(self, delim: u8) -> Vec { return () } @@ -57,7 +57,7 @@ impl string { } /// split_lines() is an alias for lines(). - pub fn split_lines(self) -> VecString { + pub fn split_lines(self) -> Vec { return self.lines() } diff --git a/stdlib/sys/vec.cap b/stdlib/sys/vec.cap index 8bb44c6..b2484ed 100644 --- a/stdlib/sys/vec.cap +++ b/stdlib/sys/vec.cap @@ -3,6 +3,7 @@ module sys::vec use sys::buffer +pub copy opaque struct Vec pub copy opaque struct VecU8 pub copy opaque struct VecI32 pub copy opaque struct VecString diff --git a/tests/programs/should_pass_result_ok_err.cap b/tests/programs/should_pass_result_ok_err.cap index 4482671..9c4bad3 100644 --- a/tests/programs/should_pass_result_ok_err.cap +++ b/tests/programs/should_pass_result_ok_err.cap @@ -14,14 +14,18 @@ pub fn main(rc: RootCap) -> i32 { let ok_result = make_ok() let err_result = make_err() - let val = ok_result.unwrap_or(0) - if val != 42 { - return 1 + if ok_result.is_ok() { + let val = ok_result.ok() + if val != 42 { + return 1 + } } - let e = err_result.unwrap_err_or(0) - if e != 99 { - return 2 + if err_result.is_err() { + let e = err_result.err() + if e != 99 { + return 2 + } } con.print("ok_err ok") From d925d5bce361703a5f7a40ed2a90cd6b755781c2 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Thu, 1 Jan 2026 14:57:08 -0800 Subject: [PATCH 20/30] Add top-level defer statement --- capc/src/ast.rs | 8 + capc/src/codegen/emit.rs | 57 +++++++ capc/src/codegen/mod.rs | 16 +- capc/src/hir.rs | 1 + capc/src/lexer.rs | 2 + capc/src/parser.rs | 13 ++ capc/src/typeck/check.rs | 43 +++++ capc/src/typeck/lower.rs | 235 +++++++++++++++++++++++---- capc/src/typeck/monomorphize.rs | 6 + capc/tests/run.rs | 17 ++ docs/DEFER.md | 36 ++++ docs/TUTORIAL.md | 40 +++-- tests/programs/should_pass_defer.cap | 12 ++ 13 files changed, 445 insertions(+), 41 deletions(-) create mode 100644 docs/DEFER.md create mode 100644 tests/programs/should_pass_defer.cap diff --git a/capc/src/ast.rs b/capc/src/ast.rs index 1371994..26099d8 100644 --- a/capc/src/ast.rs +++ b/capc/src/ast.rs @@ -141,6 +141,7 @@ pub struct Block { pub enum Stmt { Let(LetStmt), Assign(AssignStmt), + Defer(DeferStmt), Return(ReturnStmt), Break(BreakStmt), Continue(ContinueStmt), @@ -211,11 +212,18 @@ pub struct AssignStmt { pub span: Span, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeferStmt { + pub expr: Expr, + pub span: Span, +} + impl Stmt { pub fn span(&self) -> Span { match self { Stmt::Let(s) => s.span, Stmt::Assign(s) => s.span, + Stmt::Defer(s) => s.span, Stmt::Return(s) => s.span, Stmt::Break(s) => s.span, Stmt::Continue(s) => s.span, diff --git a/capc/src/codegen/emit.rs b/capc/src/codegen/emit.rs index 2eb7478..d86a0d5 100644 --- a/capc/src/codegen/emit.rs +++ b/capc/src/codegen/emit.rs @@ -50,6 +50,7 @@ pub(super) fn emit_hir_stmt( module: &mut ObjectModule, data_counter: &mut u32, loop_target: Option, + defers: &[crate::hir::HirExpr], ) -> Result { emit_hir_stmt_inner( builder, @@ -61,6 +62,7 @@ pub(super) fn emit_hir_stmt( module, data_counter, loop_target, + defers, ) .map_err(|err| err.with_span(stmt.span())) } @@ -75,6 +77,7 @@ fn emit_hir_stmt_inner( module: &mut ObjectModule, data_counter: &mut u32, loop_target: Option, + defers: &[crate::hir::HirExpr], ) -> Result { use crate::hir::HirStmt; @@ -193,6 +196,16 @@ fn emit_hir_stmt_inner( module, data_counter, )?; + emit_defer_calls( + builder, + defers, + locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + )?; match value { ValueRepr::Unit => builder.ins().return_(&[]), ValueRepr::Single(val) => builder.ins().return_(&[val]), @@ -202,6 +215,16 @@ fn emit_hir_stmt_inner( } }; } else { + emit_defer_calls( + builder, + defers, + locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + )?; builder.ins().return_(&[]); } return Ok(Flow::Terminated); @@ -236,6 +259,7 @@ fn emit_hir_stmt_inner( module, data_counter, loop_target, + defers, )?; if diverged { return Ok(Flow::Terminated); @@ -297,6 +321,7 @@ fn emit_hir_stmt_inner( module, data_counter, loop_target, + defers, )?; if flow == Flow::Terminated { then_terminated = true; @@ -324,6 +349,7 @@ fn emit_hir_stmt_inner( module, data_counter, loop_target, + defers, )?; if flow == Flow::Terminated { else_terminated = true; @@ -391,6 +417,7 @@ fn emit_hir_stmt_inner( module, data_counter, body_loop_target, + defers, )?; if flow == Flow::Terminated { body_terminated = true; @@ -495,6 +522,7 @@ fn emit_hir_stmt_inner( module, data_counter, body_loop_target, + defers, )?; if flow == Flow::Terminated { body_terminated = true; @@ -539,6 +567,31 @@ fn emit_hir_stmt_inner( Ok(Flow::Continues) } +pub(super) fn emit_defer_calls( + builder: &mut FunctionBuilder, + defers: &[crate::hir::HirExpr], + locals: &HashMap, + fn_map: &HashMap, + enum_index: &EnumIndex, + struct_layouts: &StructLayoutIndex, + module: &mut ObjectModule, + data_counter: &mut u32, +) -> Result<(), CodegenError> { + for defer_expr in defers.iter().rev() { + let _ = emit_hir_expr( + builder, + defer_expr, + locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + )?; + } + Ok(()) +} + /// Emit a single HIR expression and return its lowered value representation. fn emit_hir_expr( builder: &mut FunctionBuilder, @@ -1102,6 +1155,7 @@ fn emit_hir_expr_inner( module, data_counter, None, // break/continue not supported in expression-context matches + &[], )?; Ok(ValueRepr::Unit) } else { @@ -2074,6 +2128,7 @@ fn emit_hir_match_stmt( module: &mut ObjectModule, data_counter: &mut u32, loop_target: Option, + defers: &[crate::hir::HirExpr], ) -> Result { // Emit the scrutinee expression let value = emit_hir_expr( @@ -2157,6 +2212,7 @@ fn emit_hir_match_stmt( module, data_counter, loop_target, + defers, )?; if flow == Flow::Terminated { arm_terminated = true; @@ -2277,6 +2333,7 @@ fn emit_hir_match_expr( module, data_counter, None, // break/continue not allowed in value-producing match + &[], )?; if flow == Flow::Terminated { prefix_terminated = true; diff --git a/capc/src/codegen/mod.rs b/capc/src/codegen/mod.rs index db4c937..523f3e2 100644 --- a/capc/src/codegen/mod.rs +++ b/capc/src/codegen/mod.rs @@ -28,7 +28,10 @@ mod abi_quirks; mod intrinsics; mod layout; -use emit::{emit_hir_stmt, emit_runtime_wrapper_call, flatten_value, store_local, value_from_params}; +use emit::{ + emit_defer_calls, emit_hir_stmt, emit_runtime_wrapper_call, flatten_value, store_local, + value_from_params, +}; use layout::{build_enum_index, build_struct_layout_index}; #[derive(Debug, Error, Diagnostic)] @@ -345,6 +348,7 @@ pub fn build_object( &mut module, &mut data_counter, None, // no loop context at function top level + &func.defers, )?; if flow == Flow::Terminated { terminated = true; @@ -353,6 +357,16 @@ pub fn build_object( } if info.sig.ret == AbiType::Unit && !terminated { + emit_defer_calls( + &mut builder, + &func.defers, + &locals, + &fn_map, + &enum_index, + &struct_layouts, + &mut module, + &mut data_counter, + )?; builder.ins().return_(&[]); } diff --git a/capc/src/hir.rs b/capc/src/hir.rs index c0ea203..d05f9dd 100644 --- a/capc/src/hir.rs +++ b/capc/src/hir.rs @@ -50,6 +50,7 @@ pub struct HirFunction { pub params: Vec, pub ret_ty: HirType, pub body: HirBlock, + pub defers: Vec, } #[derive(Debug, Clone)] diff --git a/capc/src/lexer.rs b/capc/src/lexer.rs index 0347ec6..ff2a5b9 100644 --- a/capc/src/lexer.rs +++ b/capc/src/lexer.rs @@ -47,6 +47,8 @@ pub enum TokenKind { Break, #[token("continue")] Continue, + #[token("defer")] + Defer, #[token("return")] Return, #[token("struct")] diff --git a/capc/src/parser.rs b/capc/src/parser.rs index 5020e93..756e0d4 100644 --- a/capc/src/parser.rs +++ b/capc/src/parser.rs @@ -468,6 +468,7 @@ impl Parser { Some(TokenKind::Return) => Ok(Stmt::Return(self.parse_return()?)), Some(TokenKind::Break) => Ok(Stmt::Break(self.parse_break()?)), Some(TokenKind::Continue) => Ok(Stmt::Continue(self.parse_continue()?)), + Some(TokenKind::Defer) => Ok(Stmt::Defer(self.parse_defer()?)), Some(TokenKind::If) => Ok(Stmt::If(self.parse_if()?)), Some(TokenKind::While) => Ok(Stmt::While(self.parse_while()?)), Some(TokenKind::For) => Ok(Stmt::For(self.parse_for()?)), @@ -552,6 +553,18 @@ impl Parser { }) } + fn parse_defer(&mut self) -> Result { + let start = self.expect(TokenKind::Defer)?.span.start; + let expr = self.parse_expr()?; + let end = self + .maybe_consume(TokenKind::Semi) + .map_or(expr.span().end, |t| t.span.end); + Ok(DeferStmt { + expr, + span: Span::new(start, end), + }) + } + fn parse_if(&mut self) -> Result { let start = self.expect(TokenKind::If)?.span.start; // Use parse_expr_no_struct because `{` after condition starts the then-block, not a struct literal diff --git a/capc/src/typeck/check.rs b/capc/src/typeck/check.rs index 3112b39..0a92375 100644 --- a/capc/src/typeck/check.rs +++ b/capc/src/typeck/check.rs @@ -148,6 +148,7 @@ fn block_contains_ptr(block: &Block) -> Option { } } Stmt::Assign(_) => {} + Stmt::Defer(_) => {} Stmt::Break(_) => {} Stmt::Continue(_) => {} Stmt::If(if_stmt) => { @@ -192,6 +193,7 @@ fn block_contains_ptr(block: &Block) -> Option { fn stmt_is_total(stmt: &Stmt) -> bool { match stmt { Stmt::Return(ret_stmt) => ret_stmt.expr.is_some(), + Stmt::Defer(_) => false, Stmt::Expr(expr_stmt) => { if let Expr::Match(match_expr) = &expr_stmt.expr { match_is_total(match_expr) @@ -288,6 +290,7 @@ pub(super) fn check_function( module_name, &type_params, false, // not inside a loop at function top level + true, // allow defer only at top level )?; } @@ -326,6 +329,7 @@ fn check_stmt( module_name: &str, type_params: &HashSet, in_loop: bool, + allow_defer: bool, ) -> Result<(), TypeError> { match stmt { Stmt::Let(let_stmt) => { @@ -466,6 +470,37 @@ fn check_stmt( } scopes.assign(&assign.name.item, expr_ty); } + Stmt::Defer(defer_stmt) => { + if !allow_defer { + return Err(TypeError::new( + "defer statements are only allowed at the top level of a function".to_string(), + defer_stmt.span, + )); + } + match &defer_stmt.expr { + Expr::Call(_) | Expr::MethodCall(_) => {} + _ => { + return Err(TypeError::new( + "defer expects a function or method call".to_string(), + defer_stmt.span, + )) + } + } + let _ = check_expr( + &defer_stmt.expr, + functions, + scopes, + UseMode::Move, + recorder, + use_map, + struct_map, + enum_map, + stdlib, + ret_ty, + module_name, + type_params, + )?; + } Stmt::Return(ret_stmt) => { let expr_ty = if let Some(expr) = &ret_stmt.expr { check_expr( @@ -562,6 +597,7 @@ fn check_stmt( module_name, type_params, in_loop, + false, )?; let mut else_scopes = scopes.clone(); if let Some(block) = &if_stmt.else_block { @@ -578,6 +614,7 @@ fn check_stmt( module_name, type_params, in_loop, + false, )?; } merge_branch_states( @@ -625,6 +662,7 @@ fn check_stmt( module_name, type_params, true, // inside loop, break/continue allowed + false, )?; body_scopes.pop_loop(); ensure_affine_states_match( @@ -702,6 +740,7 @@ fn check_stmt( module_name, type_params, true, // inside loop, break/continue allowed + false, )?; body_scopes.pop_loop(); @@ -769,6 +808,7 @@ fn check_block( module_name: &str, type_params: &HashSet, in_loop: bool, + allow_defer: bool, ) -> Result<(), TypeError> { scopes.push_scope(); for stmt in &block.stmts { @@ -785,6 +825,7 @@ fn check_block( module_name, type_params, in_loop, + allow_defer, )?; } ensure_linear_scope_consumed(scopes, struct_map, enum_map, block.span)?; @@ -2042,6 +2083,7 @@ fn check_match_stmt( module_name, type_params, in_loop, + false, )?; arm_scope.pop_scope(); arm_scopes.push(arm_scope); @@ -2178,6 +2220,7 @@ fn check_match_arm_value( module_name, type_params, in_loop, + false, )?; } match last { diff --git a/capc/src/typeck/lower.rs b/capc/src/typeck/lower.rs index 3fedbb5..b8c5587 100644 --- a/capc/src/typeck/lower.rs +++ b/capc/src/typeck/lower.rs @@ -35,6 +35,7 @@ struct LoweringCtx<'a> { local_types: HashMap, local_counter: usize, type_params: HashSet, + defers: Vec, } impl<'a> LoweringCtx<'a> { @@ -62,6 +63,7 @@ impl<'a> LoweringCtx<'a> { local_types: HashMap::new(), local_counter: 0, type_params: HashSet::new(), + defers: Vec::new(), } } @@ -232,6 +234,7 @@ fn lower_function(func: &Function, ctx: &mut LoweringCtx) -> Result Result Result Result { ctx.push_scope(); - let stmts: Result, TypeError> = block - .stmts - .iter() - .map(|stmt| lower_stmt(stmt, ctx, ret_ty)) - .collect(); + let mut stmts = Vec::new(); + for stmt in &block.stmts { + let lowered = lower_stmt(stmt, ctx, ret_ty)?; + stmts.extend(lowered); + } ctx.pop_scope(); - Ok(HirBlock { stmts: stmts? }) + Ok(HirBlock { stmts }) } /// Lower a statement into HIR. -fn lower_stmt(stmt: &Stmt, ctx: &mut LoweringCtx, ret_ty: &Ty) -> Result { +fn lower_stmt(stmt: &Stmt, ctx: &mut LoweringCtx, ret_ty: &Ty) -> Result, TypeError> { match stmt { Stmt::Let(let_stmt) => { let expr = lower_expr(&let_stmt.expr, ctx, ret_ty)?; let ty = expr.ty().clone(); let local_id = ctx.fresh_local(let_stmt.name.item.clone(), ty.ty.clone()); - Ok(HirStmt::Let(HirLetStmt { + Ok(vec![HirStmt::Let(HirLetStmt { local_id, ty, expr, span: let_stmt.span, - })) + })]) } Stmt::Assign(assign) => { let expr = lower_expr(&assign.expr, ctx, ret_ty)?; @@ -306,28 +311,29 @@ fn lower_stmt(stmt: &Stmt, ctx: &mut LoweringCtx, ret_ty: &Ty) -> Result lower_defer_stmt(defer_stmt, ctx, ret_ty), Stmt::Return(ret) => { let expr = ret.expr.as_ref().map(|e| lower_expr(e, ctx, ret_ty)).transpose()?; - Ok(HirStmt::Return(HirReturnStmt { + Ok(vec![HirStmt::Return(HirReturnStmt { expr, span: ret.span, - })) + })]) } Stmt::Break(break_stmt) => { - Ok(HirStmt::Break(HirBreakStmt { + Ok(vec![HirStmt::Break(HirBreakStmt { span: break_stmt.span, - })) + })]) } Stmt::Continue(continue_stmt) => { - Ok(HirStmt::Continue(HirContinueStmt { + Ok(vec![HirStmt::Continue(HirContinueStmt { span: continue_stmt.span, - })) + })]) } Stmt::If(if_stmt) => { let cond = lower_expr(&if_stmt.cond, ctx, ret_ty)?; @@ -338,22 +344,22 @@ fn lower_stmt(stmt: &Stmt, ctx: &mut LoweringCtx, ret_ty: &Ty) -> Result { let cond = lower_expr(&while_stmt.cond, ctx, ret_ty)?; let body = lower_block(&while_stmt.body, ctx, ret_ty)?; - Ok(HirStmt::While(HirWhileStmt { + Ok(vec![HirStmt::While(HirWhileStmt { cond, body, span: while_stmt.span, - })) + })]) } Stmt::For(for_stmt) => { let start = lower_expr(&for_stmt.start, ctx, ret_ty)?; @@ -367,42 +373,213 @@ fn lower_stmt(stmt: &Stmt, ctx: &mut LoweringCtx, ret_ty: &Ty) -> Result { if let Expr::Match(match_expr) = &expr_stmt.expr { match lower_expr(&expr_stmt.expr, ctx, ret_ty) { Ok(expr) => { - return Ok(HirStmt::Expr(HirExprStmt { + return Ok(vec![HirStmt::Expr(HirExprStmt { expr, span: expr_stmt.span, - })); + })]); } Err(_) => { let expr = lower_match_stmt(match_expr, ctx, ret_ty)?; - return Ok(HirStmt::Expr(HirExprStmt { + return Ok(vec![HirStmt::Expr(HirExprStmt { expr, span: expr_stmt.span, - })); + })]); } } } let expr = lower_expr(&expr_stmt.expr, ctx, ret_ty)?; - Ok(HirStmt::Expr(HirExprStmt { + Ok(vec![HirStmt::Expr(HirExprStmt { expr, span: expr_stmt.span, - })) + })]) } } } +fn lower_defer_stmt( + defer_stmt: &DeferStmt, + ctx: &mut LoweringCtx, + ret_ty: &Ty, +) -> Result, TypeError> { + let mut stmts = Vec::new(); + let expr_ty = type_of_ast_expr(&defer_stmt.expr, ctx, ret_ty)?; + let hir_ret_ty = hir_type_for(expr_ty, ctx, defer_stmt.expr.span())?; + + let deferred = match &defer_stmt.expr { + Expr::Call(call) => { + let mut args = Vec::with_capacity(call.args.len()); + for arg in &call.args { + args.push(capture_defer_expr(arg, ctx, ret_ty, &mut stmts)?); + } + let path = call.callee.to_path().ok_or_else(|| { + TypeError::new( + "call target must be a function path".to_string(), + call.callee.span(), + ) + })?; + lower_defer_call_from_path(&path, &call.type_args, args, hir_ret_ty, ctx)? + } + Expr::MethodCall(method_call) => { + fn get_leftmost_segment(expr: &Expr) -> Option<&str> { + match expr { + Expr::Path(path) if path.segments.len() == 1 => Some(&path.segments[0].item), + Expr::FieldAccess(fa) => get_leftmost_segment(&fa.object), + _ => None, + } + } + + let base_is_local = if let Some(base_name) = get_leftmost_segment(&method_call.receiver) { + ctx.local_types.contains_key(base_name) + } else { + true + }; + + let path_call = method_call.receiver.to_path().map(|mut path| { + path.segments.push(method_call.method.clone()); + path.span = Span::new(path.span.start, method_call.method.span.end); + path + }); + + let is_function = if let Some(path) = &path_call { + let resolved = super::resolve_path(path, ctx.use_map); + let key = resolved.join("."); + ctx.functions.contains_key(&key) + } else { + false + }; + + if !base_is_local && is_function { + let path = path_call.expect("path exists for function call"); + let mut args = Vec::with_capacity(method_call.args.len()); + for arg in &method_call.args { + args.push(capture_defer_expr(arg, ctx, ret_ty, &mut stmts)?); + } + let deferred = lower_defer_call_from_path( + &path, + &method_call.type_args, + args, + hir_ret_ty, + ctx, + )?; + ctx.defers.push(deferred); + return Ok(stmts); + } + + let receiver = capture_defer_expr(&method_call.receiver, ctx, ret_ty, &mut stmts)?; + let receiver_ty = type_of_ast_expr(&method_call.receiver, ctx, ret_ty)?; + let (method_module, type_name, _) = resolve_method_target( + &receiver_ty, + ctx.module_name, + ctx.structs, + ctx.enums, + method_call.receiver.span(), + )?; + let method_fn = format!("{type_name}__{}", method_call.method.item); + let key = format!("{method_module}.{method_fn}"); + let symbol = format!("capable_{}", key.replace('.', "_")); + + let mut args = Vec::with_capacity(method_call.args.len() + 1); + args.push(receiver); + for arg in &method_call.args { + args.push(capture_defer_expr(arg, ctx, ret_ty, &mut stmts)?); + } + + HirExpr::Call(HirCall { + callee: ResolvedCallee::Function { + module: method_module, + name: method_fn, + symbol, + }, + type_args: lower_call_type_args(&method_call.type_args, ctx)?, + args, + ret_ty: hir_ret_ty, + span: method_call.span, + }) + } + _ => { + return Err(TypeError::new( + "defer expects a function or method call".to_string(), + defer_stmt.span, + )) + } + }; + + ctx.defers.push(deferred); + Ok(stmts) +} + +fn lower_defer_call_from_path( + path: &Path, + type_args: &[Type], + args: Vec, + ret_ty: HirType, + ctx: &mut LoweringCtx, +) -> Result { + + if path.segments.len() == 1 { + let name = &path.segments[0].item; + if name == "drop" || name == "panic" || name == "Ok" || name == "Err" { + return Err(TypeError::new( + "defer expects a function or method call".to_string(), + path.span, + )); + } + } + + let mut resolved = super::resolve_path(&path, ctx.use_map); + if resolved.len() == 1 { + resolved.insert(0, ctx.module_name.to_string()); + } + let key = resolved.join("."); + let module = resolved[..resolved.len() - 1].join("."); + let name = resolved.last().unwrap().clone(); + let symbol = format!("capable_{}", key.replace('.', "_")); + + Ok(HirExpr::Call(HirCall { + callee: ResolvedCallee::Function { module, name, symbol }, + type_args: lower_call_type_args(type_args, ctx)?, + args, + ret_ty, + span: path.span, + })) +} + +fn capture_defer_expr( + expr: &Expr, + ctx: &mut LoweringCtx, + ret_ty: &Ty, + stmts: &mut Vec, +) -> Result { + let hir_expr = lower_expr(expr, ctx, ret_ty)?; + let ty = hir_expr.ty().clone(); + let local_name = format!("__defer_{}", ctx.local_counter); + let local_id = ctx.fresh_local(local_name, ty.ty.clone()); + stmts.push(HirStmt::Let(HirLetStmt { + local_id, + ty: ty.clone(), + expr: hir_expr, + span: expr.span(), + })); + Ok(HirExpr::Local(HirLocal { + local_id, + ty, + span: expr.span(), + })) +} + /// Helper to get the type of an AST expression using the existing typechecker. /// This ensures we have a single source of truth for types. fn type_of_ast_expr( diff --git a/capc/src/typeck/monomorphize.rs b/capc/src/typeck/monomorphize.rs index 42fcba8..0add147 100644 --- a/capc/src/typeck/monomorphize.rs +++ b/capc/src/typeck/monomorphize.rs @@ -280,12 +280,18 @@ impl MonoCtx { .collect(); let ret_ty = self.mono_hir_type(module, &func.ret_ty, subs)?; let body = self.mono_block(module, &func.body, subs)?; + let defers: Result, TypeError> = func + .defers + .iter() + .map(|expr| self.mono_expr(module, expr, subs)) + .collect(); Ok(HirFunction { name: new_name, type_params: Vec::new(), params: params?, ret_ty, body, + defers: defers?, }) } diff --git a/capc/tests/run.rs b/capc/tests/run.rs index e78131e..b39d8d1 100644 --- a/capc/tests/run.rs +++ b/capc/tests/run.rs @@ -880,3 +880,20 @@ fn run_result_ok_err() { assert_eq!(code, 0); assert!(stdout.contains("ok_err ok"), "stdout was: {stdout:?}"); } + +#[test] +fn run_defer() { + let out_dir = make_out_dir("defer"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/should_pass_defer.cap", + ]); + assert_eq!(code, 0); + assert!( + stdout.contains("start\nend\nsecond\nfirst\n"), + "stdout was: {stdout:?}" + ); +} diff --git a/docs/DEFER.md b/docs/DEFER.md new file mode 100644 index 0000000..9f7a07b --- /dev/null +++ b/docs/DEFER.md @@ -0,0 +1,36 @@ +# Defer + +`defer` schedules a function or method call to run when the current function +returns. Defers run in last-in, first-out order. + +```cap +fn example(c: Console) -> unit { + c.println("start") + defer c.println("first") + defer c.println("second") + c.println("end") + return () +} +``` + +Output order: + +``` +start +end +second +first +``` + +## Semantics + +- Defers run on every return path, including implicit `unit` returns. +- Arguments are evaluated at the `defer` site and captured for later use. +- Defers are executed in LIFO order. + +## Restrictions (current) + +- `defer` is only allowed at the top level of a function body (not inside `if`, + `while`, `for`, or `match` blocks). +- The deferred expression must be a function or method call. +- Defers are not run if the function traps (e.g., `panic()`). diff --git a/docs/TUTORIAL.md b/docs/TUTORIAL.md index 6e6722a..57b15ea 100644 --- a/docs/TUTORIAL.md +++ b/docs/TUTORIAL.md @@ -39,7 +39,7 @@ pub fn main() -> i32 { } ``` -- Statements: `let`, assignment, `if`, `while`, `return`, `match`. +- Statements: `let`, assignment, `if`, `while`, `return`, `match`, `defer`. - Expressions: literals, calls, binary ops, unary ops, method calls. - Modules + imports: `module ...` and `use ...` (aliases by last path segment). - If a function returns `unit`, you can omit the `-> unit` annotation. @@ -58,7 +58,25 @@ enum Color { Red, Green, Blue } Structs and enums are nominal types. Enums are currently unit variants only. -## 4) Methods +## 4) Defer + +`defer` schedules a function or method call to run when the current function +returns (LIFO order). Arguments are evaluated at the defer site. + +```cap +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + c.println("start") + defer c.println("cleanup") + c.println("end") + return 0 +} +``` + +Current restrictions: `defer` is only allowed at the top level of a function +body, and the deferred expression must be a call. + +## 5) Methods Methods are defined in `impl` blocks and lower to `Type__method` at compile time. @@ -76,7 +94,7 @@ impl Pair { Method receivers can be `self` (move) or `self: &T` (borrow‑lite, read‑only). -## 5) Results, match, and `?` +## 6) Results, match, and `?` ```cap module results @@ -123,7 +141,7 @@ match flag { } ``` -## 6) Capabilities and attenuation +## 7) Capabilities and attenuation Capabilities live in `sys.*` and are declared with the `capability` keyword (capability types are opaque). You can only get them from `RootCap`. @@ -171,7 +189,7 @@ Why this is rejected: So methods that return capabilities must take `self` by value, which consumes the old capability. -## 7) Capability, opaque, copy, affine, linear +## 8) Capability, opaque, copy, affine, linear `capability struct` is the explicit “this is an authority token” marker. Capability types are always opaque (no public fields, no user construction) and default to affine unless marked `copy` or `linear`. This exists so the capability surface is obvious in code and the compiler can enforce one‑way attenuation (methods returning capabilities must take `self` by value). @@ -198,7 +216,7 @@ In the current stdlib: - `capability` (affine): `ReadFS`, `Filesystem`, `Dir`, `Stdin` - `linear capability`: `FileRead` -## 8) Moves and use-after-move +## 9) Moves and use-after-move ```cap module moves @@ -215,7 +233,7 @@ pub fn main() -> i32 { Affine and linear values cannot be used after move. If you move in one branch, it's moved after the join. -## 9) Linear must be consumed +## 10) Linear must be consumed ```cap module linear @@ -231,7 +249,7 @@ pub fn main() -> i32 { Linear values must be consumed along every path. You can consume them with a terminal method (like `FileRead.close()` or `read_to_string()`), or with `drop(x)` as a last resort. -## 10) Borrow-lite: &T parameters +## 11) Borrow-lite: &T parameters There is a small borrow feature for read-only access in function parameters and locals. @@ -261,7 +279,7 @@ Rules: This avoids a full borrow checker while making non-consuming observers ergonomic. -## 11) Safety boundary +## 12) Safety boundary `package safe` is default. Raw pointers and extern calls require `package unsafe`. @@ -272,7 +290,7 @@ module ffi extern fn some_ffi(x: i32) -> i32 ``` -## 12) Raw pointers and unsafe +## 13) Raw pointers and unsafe Raw pointers are available as `*T`, but **only** in `package unsafe`. @@ -290,7 +308,7 @@ pub fn main(rc: RootCap) -> i32 { There is no borrow checker for pointers. Use them only inside `package unsafe`. -## 13) What exists today (quick list) +## 14) What exists today (quick list) - Methods, modules, enums, match, while, if - Opaque capability handles in `sys.*` diff --git a/tests/programs/should_pass_defer.cap b/tests/programs/should_pass_defer.cap new file mode 100644 index 0000000..5ab3f09 --- /dev/null +++ b/tests/programs/should_pass_defer.cap @@ -0,0 +1,12 @@ +module should_pass_defer +use sys::system +use sys::console + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + c.println("start") + defer c.println("first") + defer c.println("second") + c.println("end") + return 0 +} From 47631877c744836be6c84a7bc9d1751b0c45d1cd Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Thu, 1 Jan 2026 15:09:43 -0800 Subject: [PATCH 21/30] Make defer scope-based --- capc/src/codegen/emit.rs | 299 +++++++++++++++++--- capc/src/codegen/mod.rs | 12 +- capc/src/hir.rs | 9 +- capc/src/typeck/check.rs | 16 -- capc/src/typeck/lower.rs | 19 +- capc/src/typeck/monomorphize.rs | 13 +- capc/tests/run.rs | 31 ++ docs/DEFER.md | 9 +- docs/TUTORIAL.md | 7 +- tests/programs/should_pass_defer_return.cap | 14 + tests/programs/should_pass_defer_scope.cap | 21 ++ 11 files changed, 361 insertions(+), 89 deletions(-) create mode 100644 tests/programs/should_pass_defer_return.cap create mode 100644 tests/programs/should_pass_defer_scope.cap diff --git a/capc/src/codegen/emit.rs b/capc/src/codegen/emit.rs index d86a0d5..5b158eb 100644 --- a/capc/src/codegen/emit.rs +++ b/capc/src/codegen/emit.rs @@ -39,6 +39,159 @@ pub(super) struct LoopTarget { pub exit_block: ir::Block, } +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum DeferScopeKind { + Regular, + LoopBody, +} + +#[derive(Clone, Debug)] +struct DeferScope { + kind: DeferScopeKind, + defers: Vec, +} + +#[derive(Clone, Debug)] +pub(super) struct DeferStack { + scopes: Vec, +} + +impl DeferStack { + pub(super) fn new() -> Self { + Self { scopes: Vec::new() } + } + + pub(super) fn push_block_scope(&mut self) { + self.push_scope(DeferScopeKind::Regular); + } + + pub(super) fn push_loop_scope(&mut self) { + self.push_scope(DeferScopeKind::LoopBody); + } + + fn push_scope(&mut self, kind: DeferScopeKind) { + self.scopes.push(DeferScope { + kind, + defers: Vec::new(), + }); + } + + pub(super) fn pop_scope(&mut self) { + let _ = self.scopes.pop(); + } + + pub(super) fn push_defer(&mut self, expr: crate::hir::HirExpr) { + if let Some(scope) = self.scopes.last_mut() { + scope.defers.push(expr); + } + } + + fn emit_scope_defers( + &self, + scope: &DeferScope, + builder: &mut FunctionBuilder, + locals: &HashMap, + fn_map: &HashMap, + enum_index: &EnumIndex, + struct_layouts: &StructLayoutIndex, + module: &mut ObjectModule, + data_counter: &mut u32, + ) -> Result<(), CodegenError> { + for defer_expr in scope.defers.iter().rev() { + let _ = emit_hir_expr( + builder, + defer_expr, + locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + )?; + } + Ok(()) + } + + pub(super) fn emit_current_and_pop( + &mut self, + builder: &mut FunctionBuilder, + locals: &HashMap, + fn_map: &HashMap, + enum_index: &EnumIndex, + struct_layouts: &StructLayoutIndex, + module: &mut ObjectModule, + data_counter: &mut u32, + ) -> Result<(), CodegenError> { + if let Some(scope) = self.scopes.last() { + self.emit_scope_defers( + scope, + builder, + locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + )?; + } + self.pop_scope(); + Ok(()) + } + + pub(super) fn emit_all_and_clear( + &mut self, + builder: &mut FunctionBuilder, + locals: &HashMap, + fn_map: &HashMap, + enum_index: &EnumIndex, + struct_layouts: &StructLayoutIndex, + module: &mut ObjectModule, + data_counter: &mut u32, + ) -> Result<(), CodegenError> { + while let Some(scope) = self.scopes.pop() { + self.emit_scope_defers( + &scope, + builder, + locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + )?; + } + Ok(()) + } + + pub(super) fn emit_until_loop_and_pop( + &mut self, + builder: &mut FunctionBuilder, + locals: &HashMap, + fn_map: &HashMap, + enum_index: &EnumIndex, + struct_layouts: &StructLayoutIndex, + module: &mut ObjectModule, + data_counter: &mut u32, + ) -> Result<(), CodegenError> { + while let Some(scope) = self.scopes.pop() { + self.emit_scope_defers( + &scope, + builder, + locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + )?; + if scope.kind == DeferScopeKind::LoopBody { + break; + } + } + Ok(()) + } +} + /// Emit a single HIR statement. pub(super) fn emit_hir_stmt( builder: &mut FunctionBuilder, @@ -50,7 +203,7 @@ pub(super) fn emit_hir_stmt( module: &mut ObjectModule, data_counter: &mut u32, loop_target: Option, - defers: &[crate::hir::HirExpr], + defer_stack: &mut DeferStack, ) -> Result { emit_hir_stmt_inner( builder, @@ -62,7 +215,7 @@ pub(super) fn emit_hir_stmt( module, data_counter, loop_target, - defers, + defer_stack, ) .map_err(|err| err.with_span(stmt.span())) } @@ -77,7 +230,7 @@ fn emit_hir_stmt_inner( module: &mut ObjectModule, data_counter: &mut u32, loop_target: Option, - defers: &[crate::hir::HirExpr], + defer_stack: &mut DeferStack, ) -> Result { use crate::hir::HirStmt; @@ -184,6 +337,9 @@ fn emit_hir_stmt_inner( } } } + HirStmt::Defer(defer_stmt) => { + defer_stack.push_defer(defer_stmt.expr.clone()); + } HirStmt::Return(ret_stmt) => { if let Some(expr) = &ret_stmt.expr { let value = emit_hir_expr( @@ -196,9 +352,8 @@ fn emit_hir_stmt_inner( module, data_counter, )?; - emit_defer_calls( + defer_stack.emit_all_and_clear( builder, - defers, locals, fn_map, enum_index, @@ -215,9 +370,8 @@ fn emit_hir_stmt_inner( } }; } else { - emit_defer_calls( + defer_stack.emit_all_and_clear( builder, - defers, locals, fn_map, enum_index, @@ -259,7 +413,7 @@ fn emit_hir_stmt_inner( module, data_counter, loop_target, - defers, + defer_stack, )?; if diverged { return Ok(Flow::Terminated); @@ -282,6 +436,7 @@ fn emit_hir_stmt_inner( HirStmt::If(if_stmt) => { // Snapshot locals so branch-scoped lets don't leak let saved_locals = locals.clone(); + let saved_defers = defer_stack.clone(); let then_block = builder.create_block(); let merge_block = builder.create_block(); @@ -309,6 +464,8 @@ fn emit_hir_stmt_inner( // THEN branch with its own locals builder.switch_to_block(then_block); let mut then_locals = saved_locals.clone(); + let mut then_defers = saved_defers.clone(); + then_defers.push_block_scope(); let mut then_terminated = false; for stmt in &if_stmt.then_block.stmts { let flow = emit_hir_stmt( @@ -321,7 +478,7 @@ fn emit_hir_stmt_inner( module, data_counter, loop_target, - defers, + &mut then_defers, )?; if flow == Flow::Terminated { then_terminated = true; @@ -329,6 +486,15 @@ fn emit_hir_stmt_inner( } } if !then_terminated { + then_defers.emit_current_and_pop( + builder, + &then_locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + )?; builder.ins().jump(merge_block, &[]); } builder.seal_block(then_block); @@ -337,6 +503,8 @@ fn emit_hir_stmt_inner( if let Some(else_block_hir) = &if_stmt.else_block { builder.switch_to_block(else_block); let mut else_locals = saved_locals.clone(); + let mut else_defers = saved_defers.clone(); + else_defers.push_block_scope(); let mut else_terminated = false; for stmt in &else_block_hir.stmts { let flow = emit_hir_stmt( @@ -349,7 +517,7 @@ fn emit_hir_stmt_inner( module, data_counter, loop_target, - defers, + &mut else_defers, )?; if flow == Flow::Terminated { else_terminated = true; @@ -357,6 +525,15 @@ fn emit_hir_stmt_inner( } } if !else_terminated { + else_defers.emit_current_and_pop( + builder, + &else_locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + )?; builder.ins().jump(merge_block, &[]); } builder.seal_block(else_block); @@ -371,6 +548,7 @@ fn emit_hir_stmt_inner( HirStmt::While(while_stmt) => { // Snapshot locals so loop-body lets don't leak out of the loop let saved_locals = locals.clone(); + let saved_defers = defer_stack.clone(); let header_block = builder.create_block(); let body_block = builder.create_block(); @@ -405,6 +583,8 @@ fn emit_hir_stmt_inner( // Loop body gets its own locals let mut body_locals = saved_locals.clone(); + let mut body_defers = saved_defers.clone(); + body_defers.push_loop_scope(); let mut body_terminated = false; for stmt in &while_stmt.body.stmts { let flow = emit_hir_stmt( @@ -417,7 +597,7 @@ fn emit_hir_stmt_inner( module, data_counter, body_loop_target, - defers, + &mut body_defers, )?; if flow == Flow::Terminated { body_terminated = true; @@ -426,6 +606,15 @@ fn emit_hir_stmt_inner( } if !body_terminated { + body_defers.emit_current_and_pop( + builder, + &body_locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + )?; builder.ins().jump(header_block, &[]); } @@ -441,6 +630,7 @@ fn emit_hir_stmt_inner( HirStmt::For(for_stmt) => { // Snapshot locals so loop-body lets don't leak out of the loop let saved_locals = locals.clone(); + let saved_defers = defer_stack.clone(); // Create stack slot for loop variable let loop_var_slot = builder.create_sized_stack_slot(ir::StackSlotData::new( @@ -509,6 +699,8 @@ fn emit_hir_stmt_inner( for_stmt.var_id, LocalValue::Slot(loop_var_slot, ir::types::I32), ); + let mut body_defers = saved_defers.clone(); + body_defers.push_loop_scope(); let mut body_terminated = false; for stmt in &for_stmt.body.stmts { @@ -522,7 +714,7 @@ fn emit_hir_stmt_inner( module, data_counter, body_loop_target, - defers, + &mut body_defers, )?; if flow == Flow::Terminated { body_terminated = true; @@ -532,6 +724,15 @@ fn emit_hir_stmt_inner( // Fall through to increment block (if not terminated by break/continue/return) if !body_terminated { + body_defers.emit_current_and_pop( + builder, + &body_locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + )?; builder.ins().jump(increment_block, &[]); } @@ -555,11 +756,29 @@ fn emit_hir_stmt_inner( } HirStmt::Break(_) => { let target = loop_target.expect("break outside of loop (should be caught by typeck)"); + defer_stack.emit_until_loop_and_pop( + builder, + locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + )?; builder.ins().jump(target.exit_block, &[]); return Ok(Flow::Terminated); } HirStmt::Continue(_) => { let target = loop_target.expect("continue outside of loop (should be caught by typeck)"); + defer_stack.emit_until_loop_and_pop( + builder, + locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + )?; builder.ins().jump(target.continue_block, &[]); return Ok(Flow::Terminated); } @@ -567,31 +786,6 @@ fn emit_hir_stmt_inner( Ok(Flow::Continues) } -pub(super) fn emit_defer_calls( - builder: &mut FunctionBuilder, - defers: &[crate::hir::HirExpr], - locals: &HashMap, - fn_map: &HashMap, - enum_index: &EnumIndex, - struct_layouts: &StructLayoutIndex, - module: &mut ObjectModule, - data_counter: &mut u32, -) -> Result<(), CodegenError> { - for defer_expr in defers.iter().rev() { - let _ = emit_hir_expr( - builder, - defer_expr, - locals, - fn_map, - enum_index, - struct_layouts, - module, - data_counter, - )?; - } - Ok(()) -} - /// Emit a single HIR expression and return its lowered value representation. fn emit_hir_expr( builder: &mut FunctionBuilder, @@ -1145,6 +1339,7 @@ fn emit_hir_expr_inner( ) { // Note: divergence handling is done at HirStmt::Expr level. // Here we just emit the match and return Unit. + let mut temp_defers = DeferStack::new(); let _diverged = emit_hir_match_stmt( builder, match_expr, @@ -1155,7 +1350,7 @@ fn emit_hir_expr_inner( module, data_counter, None, // break/continue not supported in expression-context matches - &[], + &mut temp_defers, )?; Ok(ValueRepr::Unit) } else { @@ -2128,7 +2323,7 @@ fn emit_hir_match_stmt( module: &mut ObjectModule, data_counter: &mut u32, loop_target: Option, - defers: &[crate::hir::HirExpr], + defer_stack: &mut DeferStack, ) -> Result { // Emit the scrutinee expression let value = emit_hir_expr( @@ -2191,6 +2386,8 @@ fn emit_hir_match_stmt( builder.switch_to_block(arm_block); let mut arm_locals = locals.clone(); + let mut arm_defers = defer_stack.clone(); + arm_defers.push_block_scope(); hir_bind_match_pattern_value( builder, &arm.pattern, @@ -2212,7 +2409,7 @@ fn emit_hir_match_stmt( module, data_counter, loop_target, - defers, + &mut arm_defers, )?; if flow == Flow::Terminated { arm_terminated = true; @@ -2222,6 +2419,15 @@ fn emit_hir_match_stmt( // If the arm didn't terminate (e.g., with return), jump to merge block if !arm_terminated { + arm_defers.emit_current_and_pop( + builder, + &arm_locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + )?; builder.ins().jump(merge_block, &[]); any_arm_continues = true; } @@ -2307,6 +2513,8 @@ fn emit_hir_match_expr( builder.switch_to_block(arm_block); let mut arm_locals = locals.clone(); + let mut arm_defers = DeferStack::new(); + arm_defers.push_block_scope(); hir_bind_match_pattern_value( builder, &arm.pattern, @@ -2333,7 +2541,7 @@ fn emit_hir_match_expr( module, data_counter, None, // break/continue not allowed in value-producing match - &[], + &mut arm_defers, )?; if flow == Flow::Terminated { prefix_terminated = true; @@ -2421,6 +2629,15 @@ fn emit_hir_match_expr( for (idx, val) in values.iter().enumerate() { builder.ins().stack_store(*val, shape.slots[idx], 0); } + arm_defers.emit_current_and_pop( + builder, + &arm_locals, + fn_map, + enum_index, + struct_layouts, + module, + data_counter, + )?; builder.ins().jump(merge_block, &[]); builder.seal_block(arm_block); } diff --git a/capc/src/codegen/mod.rs b/capc/src/codegen/mod.rs index 523f3e2..81a281d 100644 --- a/capc/src/codegen/mod.rs +++ b/capc/src/codegen/mod.rs @@ -28,10 +28,7 @@ mod abi_quirks; mod intrinsics; mod layout; -use emit::{ - emit_defer_calls, emit_hir_stmt, emit_runtime_wrapper_call, flatten_value, store_local, - value_from_params, -}; +use emit::{emit_hir_stmt, emit_runtime_wrapper_call, flatten_value, store_local, value_from_params, DeferStack}; use layout::{build_enum_index, build_struct_layout_index}; #[derive(Debug, Error, Diagnostic)] @@ -335,6 +332,8 @@ pub fn build_object( let local = store_local(&mut builder, value); locals.insert(param.local_id, local); } + let mut defer_stack = DeferStack::new(); + defer_stack.push_block_scope(); let mut terminated = false; for stmt in &func.body.stmts { @@ -348,7 +347,7 @@ pub fn build_object( &mut module, &mut data_counter, None, // no loop context at function top level - &func.defers, + &mut defer_stack, )?; if flow == Flow::Terminated { terminated = true; @@ -357,9 +356,8 @@ pub fn build_object( } if info.sig.ret == AbiType::Unit && !terminated { - emit_defer_calls( + defer_stack.emit_all_and_clear( &mut builder, - &func.defers, &locals, &fn_map, &enum_index, diff --git a/capc/src/hir.rs b/capc/src/hir.rs index d05f9dd..15eb329 100644 --- a/capc/src/hir.rs +++ b/capc/src/hir.rs @@ -50,7 +50,6 @@ pub struct HirFunction { pub params: Vec, pub ret_ty: HirType, pub body: HirBlock, - pub defers: Vec, } #[derive(Debug, Clone)] @@ -107,6 +106,7 @@ pub struct HirBlock { pub enum HirStmt { Let(HirLetStmt), Assign(HirAssignStmt), + Defer(HirDeferStmt), Return(HirReturnStmt), Break(HirBreakStmt), Continue(HirContinueStmt), @@ -121,6 +121,7 @@ impl HirStmt { match self { HirStmt::Let(s) => s.span, HirStmt::Assign(s) => s.span, + HirStmt::Defer(s) => s.span, HirStmt::Return(s) => s.span, HirStmt::Break(s) => s.span, HirStmt::Continue(s) => s.span, @@ -147,6 +148,12 @@ pub struct HirAssignStmt { pub span: Span, } +#[derive(Debug, Clone)] +pub struct HirDeferStmt { + pub expr: HirExpr, + pub span: Span, +} + #[derive(Debug, Clone)] pub struct HirReturnStmt { pub expr: Option, diff --git a/capc/src/typeck/check.rs b/capc/src/typeck/check.rs index 0a92375..1f918aa 100644 --- a/capc/src/typeck/check.rs +++ b/capc/src/typeck/check.rs @@ -290,7 +290,6 @@ pub(super) fn check_function( module_name, &type_params, false, // not inside a loop at function top level - true, // allow defer only at top level )?; } @@ -329,7 +328,6 @@ fn check_stmt( module_name: &str, type_params: &HashSet, in_loop: bool, - allow_defer: bool, ) -> Result<(), TypeError> { match stmt { Stmt::Let(let_stmt) => { @@ -471,12 +469,6 @@ fn check_stmt( scopes.assign(&assign.name.item, expr_ty); } Stmt::Defer(defer_stmt) => { - if !allow_defer { - return Err(TypeError::new( - "defer statements are only allowed at the top level of a function".to_string(), - defer_stmt.span, - )); - } match &defer_stmt.expr { Expr::Call(_) | Expr::MethodCall(_) => {} _ => { @@ -597,7 +589,6 @@ fn check_stmt( module_name, type_params, in_loop, - false, )?; let mut else_scopes = scopes.clone(); if let Some(block) = &if_stmt.else_block { @@ -614,7 +605,6 @@ fn check_stmt( module_name, type_params, in_loop, - false, )?; } merge_branch_states( @@ -662,7 +652,6 @@ fn check_stmt( module_name, type_params, true, // inside loop, break/continue allowed - false, )?; body_scopes.pop_loop(); ensure_affine_states_match( @@ -740,7 +729,6 @@ fn check_stmt( module_name, type_params, true, // inside loop, break/continue allowed - false, )?; body_scopes.pop_loop(); @@ -808,7 +796,6 @@ fn check_block( module_name: &str, type_params: &HashSet, in_loop: bool, - allow_defer: bool, ) -> Result<(), TypeError> { scopes.push_scope(); for stmt in &block.stmts { @@ -825,7 +812,6 @@ fn check_block( module_name, type_params, in_loop, - allow_defer, )?; } ensure_linear_scope_consumed(scopes, struct_map, enum_map, block.span)?; @@ -2083,7 +2069,6 @@ fn check_match_stmt( module_name, type_params, in_loop, - false, )?; arm_scope.pop_scope(); arm_scopes.push(arm_scope); @@ -2220,7 +2205,6 @@ fn check_match_arm_value( module_name, type_params, in_loop, - false, )?; } match last { diff --git a/capc/src/typeck/lower.rs b/capc/src/typeck/lower.rs index b8c5587..eaa568f 100644 --- a/capc/src/typeck/lower.rs +++ b/capc/src/typeck/lower.rs @@ -4,8 +4,8 @@ use crate::ast::*; use crate::error::TypeError; use crate::abi::AbiType; use crate::hir::{ - HirAssignStmt, HirBinary, HirBlock, HirBreakStmt, HirCall, HirContinueStmt, HirEnum, - HirEnumVariant, HirEnumVariantExpr, HirExpr, HirExprStmt, HirExternFunction, HirField, + HirAssignStmt, HirBinary, HirBlock, HirBreakStmt, HirCall, HirContinueStmt, HirDeferStmt, + HirEnum, HirEnumVariant, HirEnumVariantExpr, HirExpr, HirExprStmt, HirExternFunction, HirField, HirFieldAccess, HirForStmt, HirFunction, HirIfStmt, HirLetStmt, HirLiteral, HirLocal, HirMatch, HirMatchArm, HirParam, HirPattern, HirReturnStmt, HirStmt, HirStruct, HirStructLiteral, HirStructLiteralField, HirTrap, HirType, HirUnary, HirWhileStmt, IntrinsicId, LocalId, @@ -35,7 +35,6 @@ struct LoweringCtx<'a> { local_types: HashMap, local_counter: usize, type_params: HashSet, - defers: Vec, } impl<'a> LoweringCtx<'a> { @@ -63,7 +62,6 @@ impl<'a> LoweringCtx<'a> { local_types: HashMap::new(), local_counter: 0, type_params: HashSet::new(), - defers: Vec::new(), } } @@ -234,7 +232,6 @@ fn lower_function(func: &Function, ctx: &mut LoweringCtx) -> Result Result Result, TypeError> = func - .defers - .iter() - .map(|expr| self.mono_expr(module, expr, subs)) - .collect(); Ok(HirFunction { name: new_name, type_params: Vec::new(), params: params?, ret_ty, body, - defers: defers?, }) } @@ -415,6 +409,13 @@ impl MonoCtx { span: assign.span, })) } + HirStmt::Defer(defer_stmt) => { + let expr = self.mono_expr(module, &defer_stmt.expr, subs)?; + Ok(HirStmt::Defer(HirDeferStmt { + expr, + span: defer_stmt.span, + })) + } HirStmt::Return(ret) => { let expr = ret .expr diff --git a/capc/tests/run.rs b/capc/tests/run.rs index b39d8d1..839dea2 100644 --- a/capc/tests/run.rs +++ b/capc/tests/run.rs @@ -897,3 +897,34 @@ fn run_defer() { "stdout was: {stdout:?}" ); } + +#[test] +fn run_defer_scope() { + let out_dir = make_out_dir("defer_scope"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/should_pass_defer_scope.cap", + ]); + assert_eq!(code, 0); + assert!( + stdout.contains("start\nin if\nif defer\nafter if\nin loop\nloop defer\nafter loop\n"), + "stdout was: {stdout:?}" + ); +} + +#[test] +fn run_defer_return() { + let out_dir = make_out_dir("defer_return"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/should_pass_defer_return.cap", + ]); + assert_eq!(code, 0); + assert!(stdout.contains("start\ninner\nouter\n"), "stdout was: {stdout:?}"); +} diff --git a/docs/DEFER.md b/docs/DEFER.md index 9f7a07b..2d0ed7a 100644 --- a/docs/DEFER.md +++ b/docs/DEFER.md @@ -1,7 +1,7 @@ # Defer -`defer` schedules a function or method call to run when the current function -returns. Defers run in last-in, first-out order. +`defer` schedules a function or method call to run when the current scope +exits. Defers run in last-in, first-out order. ```cap fn example(c: Console) -> unit { @@ -24,13 +24,12 @@ first ## Semantics -- Defers run on every return path, including implicit `unit` returns. +- Defers run when the current scope ends, including on `return`, `break`, + `continue`, or normal fallthrough. - Arguments are evaluated at the `defer` site and captured for later use. - Defers are executed in LIFO order. ## Restrictions (current) -- `defer` is only allowed at the top level of a function body (not inside `if`, - `while`, `for`, or `match` blocks). - The deferred expression must be a function or method call. - Defers are not run if the function traps (e.g., `panic()`). diff --git a/docs/TUTORIAL.md b/docs/TUTORIAL.md index 57b15ea..edcd607 100644 --- a/docs/TUTORIAL.md +++ b/docs/TUTORIAL.md @@ -60,8 +60,8 @@ Structs and enums are nominal types. Enums are currently unit variants only. ## 4) Defer -`defer` schedules a function or method call to run when the current function -returns (LIFO order). Arguments are evaluated at the defer site. +`defer` schedules a function or method call to run when the current scope +exits (LIFO order). Arguments are evaluated at the defer site. ```cap pub fn main(rc: RootCap) -> i32 { @@ -73,8 +73,7 @@ pub fn main(rc: RootCap) -> i32 { } ``` -Current restrictions: `defer` is only allowed at the top level of a function -body, and the deferred expression must be a call. +Current restriction: the deferred expression must be a call. ## 5) Methods diff --git a/tests/programs/should_pass_defer_return.cap b/tests/programs/should_pass_defer_return.cap new file mode 100644 index 0000000..53bd3a5 --- /dev/null +++ b/tests/programs/should_pass_defer_return.cap @@ -0,0 +1,14 @@ +module should_pass_defer_return +use sys::system +use sys::console + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + c.println("start") + defer c.println("outer") + if (true) { + defer c.println("inner") + return 0 + } + return 1 +} diff --git a/tests/programs/should_pass_defer_scope.cap b/tests/programs/should_pass_defer_scope.cap new file mode 100644 index 0000000..12ac427 --- /dev/null +++ b/tests/programs/should_pass_defer_scope.cap @@ -0,0 +1,21 @@ +module should_pass_defer_scope +use sys::system +use sys::console + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + c.println("start") + if (true) { + defer c.println("if defer") + c.println("in if") + } + c.println("after if") + let i = 0 + while (i < 1) { + defer c.println("loop defer") + c.println("in loop") + break + } + c.println("after loop") + return 0 +} From a29d59b0b52171c3d45229795cbcc4fc20bbdbde Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Thu, 1 Jan 2026 15:18:43 -0800 Subject: [PATCH 22/30] Improve net listener and string helpers --- capc/src/typeck/mod.rs | 11 +++--- examples/http_server/http_server.cap | 50 +++++++++++++++------------- stdlib/sys/net.cap | 4 +-- stdlib/sys/string.cap | 20 +++++++++++ 4 files changed, 56 insertions(+), 29 deletions(-) diff --git a/capc/src/typeck/mod.rs b/capc/src/typeck/mod.rs index 9ed2345..e402ba2 100644 --- a/capc/src/typeck/mod.rs +++ b/capc/src/typeck/mod.rs @@ -582,10 +582,13 @@ fn validate_impl_method( let ret_ty = lower_type(&method.ret, use_map, stdlib, type_params)?; if receiver_is_ref && type_contains_capability(&ret_ty, struct_map, enum_map) { - return Err(TypeError::new( - "methods returning capabilities must take `self` by value".to_string(), - method.ret.span(), - )); + let receiver_kind = type_kind(target_ty, struct_map, enum_map); + if receiver_kind != TypeKind::Unrestricted { + return Err(TypeError::new( + "methods returning capabilities must take `self` by value".to_string(), + method.ret.span(), + )); + } } Ok(params) diff --git a/examples/http_server/http_server.cap b/examples/http_server/http_server.cap index 23abef0..2b7ea7c 100644 --- a/examples/http_server/http_server.cap +++ b/examples/http_server/http_server.cap @@ -60,21 +60,19 @@ fn sanitize_path(raw_path: string) -> Result { } fn parse_request_line(line: string) -> Result { - let parts = line.trim().split(32u8) - let method_result = parts[0] - match (method_result) { - Ok(method) => { - if (method != "GET") { + let trimmed = line.trim() + match (trimmed.split_once(32u8)) { + Ok(parts) => { + if (parts.left != "GET") { return Err(()) } + match (parts.right.split_once(32u8)) { + Ok(rest) => { return sanitize_path(strip_query(rest.left)) } + Err(_) => { return Err(()) } + } } Err(_) => { return Err(()) } } - let path_result = parts[1] - match (path_result) { - Ok(raw_path) => { return sanitize_path(strip_query(raw_path)) } - Err(_) => { return Err(()) } - } } fn parse_request_path(req: string) -> Result { @@ -99,22 +97,29 @@ fn respond_bad_request(conn: &TcpConn) -> Result { return conn.write("HTTP/1.0 400 Bad Request\r\nContent-Type: text/plain\r\n\r\nbad request\n") } -fn handle_request(conn: &TcpConn, readfs: ReadFS, path: string) -> Result { - match (readfs.read_to_string(path)) { - Ok(body) => { return respond_ok(conn, body) } - Err(_) => { return respond_not_found(conn) } +fn handle_request(conn: &TcpConn, readfs: ReadFS, req: string) -> Result { + match (parse_request_path(req)) { + Ok(path) => { + match (readfs.read_to_string(path)) { + Ok(body) => { return respond_ok(conn, body) } + Err(_) => { return respond_not_found(conn) } + } + } + Err(_) => { return respond_bad_request(conn) } } } -fn serve_once(c: Console, net: Net, readfs: ReadFS) -> Result { +fn serve_forever(c: Console, net: Net, rc: RootCap, root: string) -> Result { let listener = net.listen("127.0.0.1", 8080)? - let conn = listener.accept()? - let req = conn.read(4096)? - match (parse_request_path(req)) { - Ok(path) => { handle_request(conn, readfs, path)? } - Err(_) => { respond_bad_request(conn)? } + defer listener.close() + while (true) { + let conn = listener.accept()? + defer conn.close() + let req = conn.read_to_string()? + let readfs = rc.mint_readfs(root) + handle_request(conn, readfs, req)? + defer c.println("request complete") } - conn.close() return Ok(()) } @@ -123,9 +128,8 @@ pub fn main(rc: RootCap) -> i32 { let net = rc.mint_net() let args = rc.mint_args() let root = arg_or_default(args, 1, ".") - let readfs = rc.mint_readfs(root) c.println("listening on 127.0.0.1:8080") - let result = serve_once(c, net, readfs) + let result = serve_forever(c, net, rc, root) if (result.is_err()) { c.println("server error") } diff --git a/stdlib/sys/net.cap b/stdlib/sys/net.cap index ca9cd9f..16a2cf9 100644 --- a/stdlib/sys/net.cap +++ b/stdlib/sys/net.cap @@ -2,7 +2,7 @@ package unsafe module sys::net pub copy capability struct Net -pub linear capability struct TcpListener +pub copy capability struct TcpListener pub linear capability struct TcpConn pub enum NetErr { @@ -22,7 +22,7 @@ impl Net { } impl TcpListener { - pub fn accept(self) -> Result { + pub fn accept(self: &TcpListener) -> Result { return Err(NetErr::IoError) } diff --git a/stdlib/sys/string.cap b/stdlib/sys/string.cap index c698489..b3af2be 100644 --- a/stdlib/sys/string.cap +++ b/stdlib/sys/string.cap @@ -5,6 +5,11 @@ use sys::buffer use sys::bytes use sys::vec +pub struct SplitOnce { + left: string, + right: string +} + impl string { /// Intrinsic; implemented by the runtime. pub fn len(self) -> i32 { @@ -41,6 +46,11 @@ impl string { return () } + /// Intrinsic; implemented by the runtime. + pub fn split_once(self, delim: u8) -> Result { + return Err(()) + } + /// Intrinsic; implemented by the runtime. pub fn trim(self) -> string { return "" @@ -56,6 +66,16 @@ impl string { return "" } + /// Intrinsic; implemented by the runtime. + pub fn trim_prefix(self, prefix: string) -> string { + return "" + } + + /// Intrinsic; implemented by the runtime. + pub fn trim_suffix(self, suffix: string) -> string { + return "" + } + /// split_lines() is an alias for lines(). pub fn split_lines(self) -> Vec { return self.lines() From c53e84dd1876b703ec149ee976dced5122d37915 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Thu, 1 Jan 2026 15:22:40 -0800 Subject: [PATCH 23/30] Add if let statement sugar --- capc/src/parser.rs | 69 +++++++++++++++++++++++---- capc/tests/run.rs | 14 ++++++ docs/TUTORIAL.md | 10 ++++ tests/programs/should_pass_if_let.cap | 41 ++++++++++++++++ 4 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 tests/programs/should_pass_if_let.cap diff --git a/capc/src/parser.rs b/capc/src/parser.rs index 756e0d4..ff0696c 100644 --- a/capc/src/parser.rs +++ b/capc/src/parser.rs @@ -469,7 +469,7 @@ impl Parser { Some(TokenKind::Break) => Ok(Stmt::Break(self.parse_break()?)), Some(TokenKind::Continue) => Ok(Stmt::Continue(self.parse_continue()?)), Some(TokenKind::Defer) => Ok(Stmt::Defer(self.parse_defer()?)), - Some(TokenKind::If) => Ok(Stmt::If(self.parse_if()?)), + Some(TokenKind::If) => self.parse_if_stmt(), Some(TokenKind::While) => Ok(Stmt::While(self.parse_while()?)), Some(TokenKind::For) => Ok(Stmt::For(self.parse_for()?)), Some(TokenKind::Ident) => { @@ -565,18 +565,71 @@ impl Parser { }) } - fn parse_if(&mut self) -> Result { - let start = self.expect(TokenKind::If)?.span.start; + fn parse_if_stmt(&mut self) -> Result { + let if_token = self.expect(TokenKind::If)?; + let start = if_token.span.start; + if self.peek_kind() == Some(TokenKind::Let) { + self.bump(); + let pattern = self.parse_pattern()?; + self.expect(TokenKind::Eq)?; + let expr = self.parse_expr_no_struct()?; + let then_block = self.parse_block()?; + let else_block = if self.peek_kind() == Some(TokenKind::Else) { + self.bump(); + if self.peek_kind() == Some(TokenKind::If) { + let else_if = self.parse_if_stmt()?; + let span = else_if.span(); + Some(Block { + stmts: vec![else_if], + span, + }) + } else { + Some(self.parse_block()?) + } + } else { + None + }; + let end = else_block + .as_ref() + .map_or(then_block.span.end, |b| b.span.end); + let else_body = else_block.unwrap_or(Block { + stmts: Vec::new(), + span: Span::new(end, end), + }); + let match_expr = MatchExpr { + expr: Box::new(expr), + arms: vec![ + MatchArm { + pattern, + body: then_block, + span: Span::new(start, end), + }, + MatchArm { + pattern: Pattern::Wildcard(Span::new(end, end)), + body: else_body, + span: Span::new(start, end), + }, + ], + span: Span::new(start, end), + match_span: if_token.span, + }; + return Ok(Stmt::Expr(ExprStmt { + expr: Expr::Match(match_expr), + span: Span::new(start, end), + })); + } + // Use parse_expr_no_struct because `{` after condition starts the then-block, not a struct literal let cond = self.parse_expr_no_struct()?; let then_block = self.parse_block()?; let else_block = if self.peek_kind() == Some(TokenKind::Else) { self.bump(); if self.peek_kind() == Some(TokenKind::If) { - let else_if = self.parse_if()?; + let else_if = self.parse_if_stmt()?; + let span = else_if.span(); Some(Block { - stmts: vec![Stmt::If(else_if.clone())], - span: else_if.span, + stmts: vec![else_if], + span, }) } else { Some(self.parse_block()?) @@ -587,12 +640,12 @@ impl Parser { let end = else_block .as_ref() .map_or(then_block.span.end, |b| b.span.end); - Ok(IfStmt { + Ok(Stmt::If(IfStmt { cond, then_block, else_block, span: Span::new(start, end), - }) + })) } fn parse_while(&mut self) -> Result { diff --git a/capc/tests/run.rs b/capc/tests/run.rs index 839dea2..aedc6d2 100644 --- a/capc/tests/run.rs +++ b/capc/tests/run.rs @@ -928,3 +928,17 @@ fn run_defer_return() { assert_eq!(code, 0); assert!(stdout.contains("start\ninner\nouter\n"), "stdout was: {stdout:?}"); } + +#[test] +fn run_if_let() { + let out_dir = make_out_dir("if_let"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/should_pass_if_let.cap", + ]); + assert_eq!(code, 0); + assert!(stdout.contains("ok\nerr\nif_let ok\n"), "stdout was: {stdout:?}"); +} diff --git a/docs/TUTORIAL.md b/docs/TUTORIAL.md index edcd607..c77244b 100644 --- a/docs/TUTORIAL.md +++ b/docs/TUTORIAL.md @@ -140,6 +140,16 @@ match flag { } ``` +You can also use `if let` as a single-arm `match`: + +```cap +if let Ok(x) = make() { + return x +} else { + return 0 +} +``` + ## 7) Capabilities and attenuation Capabilities live in `sys.*` and are declared with the `capability` keyword (capability types are opaque). You can only get them from `RootCap`. diff --git a/tests/programs/should_pass_if_let.cap b/tests/programs/should_pass_if_let.cap new file mode 100644 index 0000000..e07a9fc --- /dev/null +++ b/tests/programs/should_pass_if_let.cap @@ -0,0 +1,41 @@ +module should_pass_if_let +use sys::system +use sys::console + +fn make_ok() -> Result { + return Ok(7) +} + +fn make_err() -> Result { + return Err(9) +} + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + let ok = make_ok() + if let Ok(x) = ok { + if (x != 7) { + c.println("bad ok") + return 1 + } + c.println("ok") + } else { + c.println("unexpected err") + return 1 + } + + let err = make_err() + if let Err(e) = err { + if (e != 9) { + c.println("bad err") + return 2 + } + c.println("err") + } else { + c.println("unexpected ok") + return 2 + } + + c.println("if_let ok") + return 0 +} From 15276b08a2927b0ef01203654a32ace7af0577b1 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Thu, 1 Jan 2026 15:26:16 -0800 Subject: [PATCH 24/30] Add for {} infinite loop sugar --- capc/src/parser.rs | 24 +++++++++++++++++++--- capc/tests/run.rs | 14 +++++++++++++ docs/TUTORIAL.md | 3 ++- tests/programs/should_pass_for_forever.cap | 17 +++++++++++++++ 4 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 tests/programs/should_pass_for_forever.cap diff --git a/capc/src/parser.rs b/capc/src/parser.rs index ff0696c..00f5314 100644 --- a/capc/src/parser.rs +++ b/capc/src/parser.rs @@ -471,7 +471,7 @@ impl Parser { Some(TokenKind::Defer) => Ok(Stmt::Defer(self.parse_defer()?)), Some(TokenKind::If) => self.parse_if_stmt(), Some(TokenKind::While) => Ok(Stmt::While(self.parse_while()?)), - Some(TokenKind::For) => Ok(Stmt::For(self.parse_for()?)), + Some(TokenKind::For) => self.parse_for_stmt(), Some(TokenKind::Ident) => { if self.peek_token(1).is_some_and(|t| t.kind == TokenKind::Eq) { Ok(Stmt::Assign(self.parse_assign()?)) @@ -661,8 +661,7 @@ impl Parser { }) } - fn parse_for(&mut self) -> Result { - let start = self.expect(TokenKind::For)?.span.start; + fn parse_for_after(&mut self, start: usize) -> Result { let var = self.expect_ident()?; self.expect(TokenKind::In)?; let range_start = self.parse_range_bound()?; @@ -679,6 +678,25 @@ impl Parser { }) } + fn parse_for_stmt(&mut self) -> Result { + let for_token = self.expect(TokenKind::For)?; + let start = for_token.span.start; + if self.peek_kind() == Some(TokenKind::LBrace) { + let body = self.parse_block()?; + let end = body.span.end; + let cond = Expr::Literal(LiteralExpr { + value: Literal::Bool(true), + span: for_token.span, + }); + return Ok(Stmt::While(WhileStmt { + cond, + body, + span: Span::new(start, end), + })); + } + Ok(Stmt::For(self.parse_for_after(start)?)) + } + /// Parse a simple expression for range bounds (no struct literals allowed) fn parse_range_bound(&mut self) -> Result { match self.peek_kind() { diff --git a/capc/tests/run.rs b/capc/tests/run.rs index aedc6d2..2da9547 100644 --- a/capc/tests/run.rs +++ b/capc/tests/run.rs @@ -942,3 +942,17 @@ fn run_if_let() { assert_eq!(code, 0); assert!(stdout.contains("ok\nerr\nif_let ok\n"), "stdout was: {stdout:?}"); } + +#[test] +fn run_for_forever() { + let out_dir = make_out_dir("for_forever"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/should_pass_for_forever.cap", + ]); + assert_eq!(code, 0); + assert!(stdout.contains("0\n1\n2\nfor_forever ok\n"), "stdout was: {stdout:?}"); +} diff --git a/docs/TUTORIAL.md b/docs/TUTORIAL.md index c77244b..d0c71e3 100644 --- a/docs/TUTORIAL.md +++ b/docs/TUTORIAL.md @@ -39,10 +39,11 @@ pub fn main() -> i32 { } ``` -- Statements: `let`, assignment, `if`, `while`, `return`, `match`, `defer`. +- Statements: `let`, assignment, `if`, `while`, `for`, `return`, `match`, `defer`. - Expressions: literals, calls, binary ops, unary ops, method calls. - Modules + imports: `module ...` and `use ...` (aliases by last path segment). - If a function returns `unit`, you can omit the `-> unit` annotation. +- `for { ... }` is an infinite loop (Go style); `for i in a..b` is range. - Integer arithmetic traps on overflow. - Variable shadowing is not allowed. diff --git a/tests/programs/should_pass_for_forever.cap b/tests/programs/should_pass_for_forever.cap new file mode 100644 index 0000000..50018b8 --- /dev/null +++ b/tests/programs/should_pass_for_forever.cap @@ -0,0 +1,17 @@ +module should_pass_for_forever +use sys::system +use sys::console + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + let i = 0 + for { + c.println_i32(i) + if (i == 2) { + break + } + i = i + 1 + } + c.println("for_forever ok") + return 0 +} From 4ee669ec23b83b61f6a57f0048bb022a28fcae0b Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Thu, 1 Jan 2026 15:41:22 -0800 Subject: [PATCH 25/30] Add char literals and string helpers --- capc/src/lexer.rs | 2 + capc/src/parser.rs | 57 ++++++++++++++++++++++- stdlib/sys/string.cap | 76 +++++++++++++++++++++++++++++++ tests/programs/string_helpers.cap | 19 +++++++- 4 files changed, 150 insertions(+), 4 deletions(-) diff --git a/capc/src/lexer.rs b/capc/src/lexer.rs index ff2a5b9..db6b86a 100644 --- a/capc/src/lexer.rs +++ b/capc/src/lexer.rs @@ -143,6 +143,8 @@ pub enum TokenKind { Int, #[regex(r#"\"([^\"\\]|\\.)*\""#)] Str, + #[regex(r#"'([^'\\]|\\x[0-9a-fA-F]{2}|\\.)'"#)] + Char, Error, } diff --git a/capc/src/parser.rs b/capc/src/parser.rs index 00f5314..d764ec9 100644 --- a/capc/src/parser.rs +++ b/capc/src/parser.rs @@ -1049,6 +1049,16 @@ impl Parser { span: token.span, })) } + Some(TokenKind::Char) => { + let token = self.bump().unwrap(); + let value = unescape_char(&token.text).map_err(|message| { + self.error_at(token.span, format!("invalid char literal: {message}")) + })?; + Ok(Expr::Literal(LiteralExpr { + value: Literal::U8(value), + span: token.span, + })) + } Some(TokenKind::True) => { let token = self.bump().unwrap(); Ok(Expr::Literal(LiteralExpr { @@ -1105,8 +1115,8 @@ impl Parser { Ok(Expr::Path(path)) } } - Some(_other) => Err(self.error_current(format!( - "unexpected token in expression: {{other:?}}" + Some(other) => Err(self.error_current(format!( + "unexpected token in expression: {other:?}" ))), None => Err(self.error_current("unexpected end of input".to_string())), } @@ -1154,6 +1164,13 @@ impl Parser { })?; Ok(Pattern::Literal(Literal::Int(value))) } + Some(TokenKind::Char) => { + let token = self.bump().unwrap(); + let value = unescape_char(&token.text).map_err(|message| { + self.error_at(token.span, format!("invalid char literal: {message}")) + })?; + Ok(Pattern::Literal(Literal::U8(value))) + } Some(TokenKind::True) => { self.bump(); Ok(Pattern::Literal(Literal::Bool(true))) @@ -1497,6 +1514,42 @@ fn unescape_string(text: &str) -> Result { Ok(out) } +fn unescape_char(text: &str) -> Result { + let mut chars = text.chars(); + if chars.next() != Some('\'') || text.len() < 2 { + return Err("missing quotes".to_string()); + } + let Some(ch) = chars.next() else { + return Err("empty char literal".to_string()); + }; + let value = if ch == '\\' { + let Some(esc) = chars.next() else { + return Err("invalid escape".to_string()); + }; + match esc { + 'n' => b'\n', + 'r' => b'\r', + 't' => b'\t', + '\\' => b'\\', + '\'' => b'\'', + 'x' => { + let hi = chars.next().ok_or_else(|| "invalid hex escape".to_string())?; + let lo = chars.next().ok_or_else(|| "invalid hex escape".to_string())?; + let hex = format!("{hi}{lo}"); + u8::from_str_radix(&hex, 16).map_err(|_| "invalid hex escape".to_string())? + } + other => return Err(format!("unsupported escape \\{other}")), + } + } else { + let code = ch as u32; + if code > 255 { + return Err("char literal out of range".to_string()); + } + code as u8 + }; + Ok(value) +} + trait SpanExt { fn span(&self) -> Span; } diff --git a/stdlib/sys/string.cap b/stdlib/sys/string.cap index b3af2be..4b4f115 100644 --- a/stdlib/sys/string.cap +++ b/stdlib/sys/string.cap @@ -144,4 +144,80 @@ impl string { } return true } + + pub fn is_empty(self) -> bool { + return self.len() == 0 + } + + pub fn byte_at_checked(self, index: i32) -> Result { + if (index < 0) { + return Err(()) + } + if (index >= self.len()) { + return Err(()) + } + return Ok(self.byte_at(index)) + } + + pub fn index_of_byte(self, needle: u8) -> Result { + let len = self.len() + let i = 0 + while (i < len) { + if (self.byte_at(i) == needle) { + return Ok(i) + } + i = i + 1 + } + return Err(()) + } + + pub fn last_index_of_byte(self, needle: u8) -> Result { + let len = self.len() + if (len == 0) { + return Err(()) + } + let i = len - 1 + while (true) { + if (self.byte_at(i) == needle) { + return Ok(i) + } + if (i == 0) { + break + } + i = i - 1 + } + return Err(()) + } + + pub fn contains_byte(self, needle: u8) -> bool { + match (self.index_of_byte(needle)) { + Ok(_) => { return true } + Err(_) => { return false } + } + } + + pub fn count_byte(self, needle: u8) -> i32 { + let len = self.len() + let i = 0 + let count = 0 + while (i < len) { + if (self.byte_at(i) == needle) { + count = count + 1 + } + i = i + 1 + } + return count + } + + pub fn is_ascii(self) -> bool { + let len = self.len() + let i = 0 + while (i < len) { + if (self.byte_at(i) > '\x7f') { + return false + } + i = i + 1 + } + return true + } } diff --git a/tests/programs/string_helpers.cap b/tests/programs/string_helpers.cap index 34781e4..5c5bf9c 100644 --- a/tests/programs/string_helpers.cap +++ b/tests/programs/string_helpers.cap @@ -23,8 +23,23 @@ pub fn main(rc: RootCap) -> i32 { c.assert(trimmed.ends_with_byte(105u8)) c.assert(trimmed_start.starts_with("hi")) c.assert(trimmed_end.ends_with("hi")) - c.assert("abc".starts_with_byte(97u8)) - c.assert("abc".ends_with_byte(99u8)) + c.assert("abc".starts_with_byte('a')) + c.assert("abc".ends_with_byte('c')) + c.assert("".is_empty()) + c.assert(!"a".is_empty()) + c.assert("abc".contains_byte(98u8)) + c.assert(!"abc".contains_byte(120u8)) + match ("abc".index_of_byte(99u8)) { + Ok(i) => { c.assert(i == 2) } + Err(_) => { c.assert(false) } + } + match ("abca".last_index_of_byte(97u8)) { + Ok(i) => { c.assert(i == 3) } + Err(_) => { c.assert(false) } + } + c.assert("abca".count_byte(97u8) == 2) + c.assert("abc".is_ascii()) + c.assert("abc".byte_at_checked(10).is_err()) c.println("string ok") return 0 } From 48b63f00d841062e64abb04b81e76479e510377b Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Thu, 1 Jan 2026 16:30:18 -0800 Subject: [PATCH 26/30] stdlib: flesh out string/vec helpers --- capc/src/codegen/emit.rs | 41 +++-- capc/src/codegen/intrinsics.rs | 109 ++++++------ runtime/src/lib.rs | 35 ++++ stdlib/sys/buffer.cap | 19 +++ stdlib/sys/string.cap | 266 +++++++++++++++++++++++++++--- stdlib/sys/vec.cap | 82 +++++++++ tests/programs/string_helpers.cap | 33 +++- 7 files changed, 495 insertions(+), 90 deletions(-) diff --git a/capc/src/codegen/emit.rs b/capc/src/codegen/emit.rs index 5b158eb..fc48691 100644 --- a/capc/src/codegen/emit.rs +++ b/capc/src/codegen/emit.rs @@ -15,8 +15,8 @@ use crate::abi::AbiType; use crate::ast::{BinaryOp, Literal, UnaryOp}; use super::{ - CodegenError, EnumIndex, Flow, FnInfo, LocalValue, ResultKind, ResultShape, StructLayoutIndex, - TypeLayout, ValueRepr, + CodegenError, EnumIndex, Flow, FnInfo, FnSig, LocalValue, ResultKind, ResultShape, + StructLayoutIndex, TypeLayout, ValueRepr, }; use super::abi_quirks; use super::layout::{align_to, resolve_struct_layout, type_layout_for_abi}; @@ -1745,14 +1745,7 @@ fn emit_hir_struct_literal( literal.struct_ty.ty )) })?; - let ptr_ty = module.isa().pointer_type(); - let align = layout.align.max(1); - let slot_size = layout.size.max(1).saturating_add(align - 1); - let slot = builder.create_sized_stack_slot(ir::StackSlotData::new( - ir::StackSlotKind::ExplicitSlot, - slot_size, - )); - let base_ptr = aligned_stack_addr(builder, slot, align, ptr_ty); + let base_ptr = emit_heap_alloc(builder, module, layout.size.max(1) as i32)?; for field in &literal.fields { let Some(field_layout) = layout.fields.get(&field.name) else { @@ -1785,6 +1778,34 @@ fn emit_hir_struct_literal( Ok(ValueRepr::Single(base_ptr)) } +fn emit_heap_alloc( + builder: &mut FunctionBuilder, + module: &mut ObjectModule, + size: i32, +) -> Result { + let handle = builder.ins().iconst(ir::types::I64, 0); + let size_val = builder.ins().iconst(ir::types::I32, size as i64); + let sig = FnSig { + params: vec![AbiType::Handle, AbiType::I32], + ret: AbiType::Ptr, + }; + let sig = sig_to_clif( + &sig, + module.isa().pointer_type(), + module.isa().default_call_conv(), + ); + let func_id = module + .declare_function("capable_rt_malloc", Linkage::Import, &sig) + .map_err(|err| CodegenError::Codegen(err.to_string()))?; + let local = module.declare_func_in_func(func_id, builder.func); + let call_inst = builder.ins().call(local, &[handle, size_val]); + let results = builder.inst_results(call_inst); + results + .get(0) + .copied() + .ok_or_else(|| CodegenError::Codegen("missing malloc result".to_string())) +} + /// Emit field access by computing the field address/offset. fn emit_hir_field_access( builder: &mut FunctionBuilder, diff --git a/capc/src/codegen/intrinsics.rs b/capc/src/codegen/intrinsics.rs index 3958155..17ec238 100644 --- a/capc/src/codegen/intrinsics.rs +++ b/capc/src/codegen/intrinsics.rs @@ -279,6 +279,17 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { ], ret: AbiType::ResultOut(Box::new(AbiType::Handle), Box::new(AbiType::I32)), }; + let mem_buffer_new_default = FnSig { + params: vec![AbiType::I32], + ret: AbiType::Result(Box::new(AbiType::Handle), Box::new(AbiType::I32)), + }; + let mem_buffer_new_default_abi = FnSig { + params: vec![ + AbiType::I32, + AbiType::ResultOut(Box::new(AbiType::Handle), Box::new(AbiType::I32)), + ], + ret: AbiType::ResultOut(Box::new(AbiType::Handle), Box::new(AbiType::I32)), + }; let mem_buffer_len = FnSig { params: vec![AbiType::Handle], ret: AbiType::I32, @@ -327,6 +338,10 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { params: vec![AbiType::Handle], ret: AbiType::Handle, }; + let vec_new_default = FnSig { + params: vec![], + ret: AbiType::Handle, + }; let vec_u8_get = FnSig { params: vec![AbiType::Handle, AbiType::I32], ret: AbiType::Result(Box::new(AbiType::U8), Box::new(AbiType::I32)), @@ -549,6 +564,14 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { params: vec![AbiType::String], ret: AbiType::Handle, }; + let string_from_bytes = FnSig { + params: vec![AbiType::Handle], + ret: AbiType::Result(Box::new(AbiType::String), Box::new(AbiType::I32)), + }; + let string_from_bytes_abi = FnSig { + params: vec![AbiType::Handle, AbiType::ResultString], + ret: AbiType::ResultString, + }; let string_split = FnSig { params: vec![AbiType::String], ret: AbiType::Handle, @@ -557,22 +580,6 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { params: vec![AbiType::String], ret: AbiType::Handle, }; - let string_split_delim = FnSig { - params: vec![AbiType::String, AbiType::U8], - ret: AbiType::Handle, - }; - let string_trim = FnSig { - params: vec![AbiType::String], - ret: AbiType::String, - }; - let string_trim_start = FnSig { - params: vec![AbiType::String], - ret: AbiType::String, - }; - let string_trim_end = FnSig { - params: vec![AbiType::String], - ret: AbiType::String, - }; // Vec lengths. let vec_u8_len = FnSig { params: vec![AbiType::Handle], @@ -1083,6 +1090,16 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { }, ); // === Buffer + slices === + map.insert( + "sys.buffer.new".to_string(), + FnInfo { + sig: mem_buffer_new_default, + abi_sig: Some(mem_buffer_new_default_abi), + symbol: "capable_rt_buffer_new_default".to_string(), + runtime_symbol: None, + is_runtime: true, + }, + ); map.insert( "sys.buffer.Alloc__buffer_new".to_string(), FnInfo { @@ -1304,6 +1321,16 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { is_runtime: true, }, ); + map.insert( + "sys.buffer.vec_string_new".to_string(), + FnInfo { + sig: vec_new_default, + abi_sig: None, + symbol: "capable_rt_vec_string_new_default".to_string(), + runtime_symbol: None, + is_runtime: true, + }, + ); map.insert( "sys.buffer.Alloc__vec_string_free".to_string(), FnInfo { @@ -1575,6 +1602,16 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { is_runtime: true, }, ); + map.insert( + "sys.string.from_bytes".to_string(), + FnInfo { + sig: string_from_bytes, + abi_sig: Some(string_from_bytes_abi), + symbol: "capable_rt_string_from_bytes".to_string(), + runtime_symbol: None, + is_runtime: true, + }, + ); map.insert( "sys.string.string__bytes".to_string(), FnInfo { @@ -1606,46 +1643,6 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { is_runtime: true, }, ); - map.insert( - "sys.string.string__split".to_string(), - FnInfo { - sig: string_split_delim, - abi_sig: None, - symbol: "capable_rt_string_split".to_string(), - runtime_symbol: None, - is_runtime: true, - }, - ); - map.insert( - "sys.string.string__trim".to_string(), - FnInfo { - sig: string_trim, - abi_sig: None, - symbol: "capable_rt_string_trim".to_string(), - runtime_symbol: None, - is_runtime: true, - }, - ); - map.insert( - "sys.string.string__trim_start".to_string(), - FnInfo { - sig: string_trim_start, - abi_sig: None, - symbol: "capable_rt_string_trim_start".to_string(), - runtime_symbol: None, - is_runtime: true, - }, - ); - map.insert( - "sys.string.string__trim_end".to_string(), - FnInfo { - sig: string_trim_end, - abi_sig: None, - symbol: "capable_rt_string_trim_end".to_string(), - runtime_symbol: None, - is_runtime: true, - }, - ); // === Bytes === map.insert( "sys.bytes.u8__is_whitespace".to_string(), diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index e6bd5a1..29d4bfe 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1089,6 +1089,15 @@ pub extern "C" fn capable_rt_buffer_new( 0 } +#[no_mangle] +pub extern "C" fn capable_rt_buffer_new_default( + initial_len: i32, + out_ok: *mut Handle, + out_err: *mut i32, +) -> u8 { + capable_rt_buffer_new(0, initial_len, out_ok, out_err) +} + #[no_mangle] pub extern "C" fn capable_rt_buffer_len(buffer: Handle) -> i32 { with_table(&BUFFERS, "buffer table", |table| { @@ -1782,6 +1791,11 @@ pub extern "C" fn capable_rt_vec_string_new(_alloc: Handle) -> Handle { handle } +#[no_mangle] +pub extern "C" fn capable_rt_vec_string_new_default() -> Handle { + capable_rt_vec_string_new(0) +} + #[no_mangle] pub extern "C" fn capable_rt_vec_string_len(vec: Handle) -> i32 { with_table(&VECS_STRING, "vec string table", |table| { @@ -2144,6 +2158,27 @@ pub extern "C" fn capable_rt_string_as_slice(ptr: *const u8, len: usize) -> Hand handle } +#[no_mangle] +pub extern "C" fn capable_rt_string_from_bytes( + slice: Handle, + out_ptr: *mut *const u8, + out_len: *mut u64, + out_err: *mut i32, +) -> u8 { + let (ptr, len) = with_table(&SLICES, "slice table", |table| { + table.get(&slice).map(|state| (state.ptr, state.len)) + }) + .unwrap_or((0, 0)); + + if ptr == 0 || len == 0 { + return write_string_result(out_ptr, out_len, out_err, Ok(String::new())); + } + + let bytes = unsafe { std::slice::from_raw_parts(ptr as *const u8, len) }; + let value = String::from_utf8_lossy(bytes).into_owned(); + write_string_result(out_ptr, out_len, out_err, Ok(value)) +} + #[no_mangle] pub extern "C" fn capable_rt_bytes_is_whitespace(value: u8) -> u8 { match value { diff --git a/stdlib/sys/buffer.cap b/stdlib/sys/buffer.cap index 37891d9..6cff58d 100644 --- a/stdlib/sys/buffer.cap +++ b/stdlib/sys/buffer.cap @@ -1,6 +1,7 @@ package safe module sys::buffer use sys::vec +use sys::string pub copy opaque struct Alloc pub copy opaque struct Buffer @@ -11,6 +12,16 @@ pub enum AllocErr { Oom } +/// Intrinsic; implemented by the runtime. +pub fn new(initial_len: i32) -> Result { + return Err(AllocErr::Oom) +} + +/// Intrinsic; implemented by the runtime. +pub fn vec_string_new() -> vec::Vec { + return () +} + impl Alloc { pub fn malloc(self, size: i32) -> *u8 { return () @@ -82,6 +93,10 @@ impl Buffer { return Err(AllocErr::Oom) } + pub fn push_str(self, s: string) -> Result { + return self.extend(s.as_slice()) + } + pub fn is_empty(self) -> bool { return false } @@ -93,6 +108,10 @@ impl Buffer { pub fn as_mut_slice(self) -> MutSlice { return () } + + pub fn to_string(self) -> Result { + return string::from_bytes(self.as_slice()) + } } impl Slice { diff --git a/stdlib/sys/string.cap b/stdlib/sys/string.cap index 4b4f115..2ac21a2 100644 --- a/stdlib/sys/string.cap +++ b/stdlib/sys/string.cap @@ -10,6 +10,99 @@ pub struct SplitOnce { right: string } +/// Intrinsic; implemented by the runtime. +pub fn from_bytes(bytes: Slice) -> Result { + return Err(buffer::AllocErr::Oom) +} + +fn build_range(s: string, start: i32, end: i32) -> string { + if (end <= start) { + return "" + } + let buf_result = buffer::new(0) + match (buf_result) { + Ok(buf) => { + let i = start + while (i < end) { + match (buf.push(s.byte_at(i))) { + Ok(_) => { } + Err(_) => { panic() } + } + i = i + 1 + } + match (buf.to_string()) { + Ok(out) => { return out } + Err(_) => { panic() } + } + } + Err(_) => { panic() } + } +} + +fn lower_ascii_byte(b: u8) -> u8 { + match (b) { + 'A' => { return 'a' } + 'B' => { return 'b' } + 'C' => { return 'c' } + 'D' => { return 'd' } + 'E' => { return 'e' } + 'F' => { return 'f' } + 'G' => { return 'g' } + 'H' => { return 'h' } + 'I' => { return 'i' } + 'J' => { return 'j' } + 'K' => { return 'k' } + 'L' => { return 'l' } + 'M' => { return 'm' } + 'N' => { return 'n' } + 'O' => { return 'o' } + 'P' => { return 'p' } + 'Q' => { return 'q' } + 'R' => { return 'r' } + 'S' => { return 's' } + 'T' => { return 't' } + 'U' => { return 'u' } + 'V' => { return 'v' } + 'W' => { return 'w' } + 'X' => { return 'x' } + 'Y' => { return 'y' } + 'Z' => { return 'z' } + _ => { return b } + } +} + +fn upper_ascii_byte(b: u8) -> u8 { + match (b) { + 'a' => { return 'A' } + 'b' => { return 'B' } + 'c' => { return 'C' } + 'd' => { return 'D' } + 'e' => { return 'E' } + 'f' => { return 'F' } + 'g' => { return 'G' } + 'h' => { return 'H' } + 'i' => { return 'I' } + 'j' => { return 'J' } + 'k' => { return 'K' } + 'l' => { return 'L' } + 'm' => { return 'M' } + 'n' => { return 'N' } + 'o' => { return 'O' } + 'p' => { return 'P' } + 'q' => { return 'Q' } + 'r' => { return 'R' } + 's' => { return 'S' } + 't' => { return 'T' } + 'u' => { return 'U' } + 'v' => { return 'V' } + 'w' => { return 'W' } + 'x' => { return 'X' } + 'y' => { return 'Y' } + 'z' => { return 'Z' } + _ => { return b } + } +} + impl string { /// Intrinsic; implemented by the runtime. pub fn len(self) -> i32 { @@ -41,39 +134,104 @@ impl string { return () } - /// Intrinsic; implemented by the runtime. pub fn split(self, delim: u8) -> Vec { - return () + let out = buffer::vec_string_new() + let bytes = self.as_slice() + let len = bytes.len() + let start = 0 + let i = 0 + while (i < len) { + if (bytes.at(i) == delim) { + let part = build_range(self, start, i) + match (out.push(part)) { + Ok(_) => { } + Err(_) => { panic() } + } + start = i + 1 + } + i = i + 1 + } + let part = build_range(self, start, len) + match (out.push(part)) { + Ok(_) => { } + Err(_) => { panic() } + } + return out } - /// Intrinsic; implemented by the runtime. pub fn split_once(self, delim: u8) -> Result { + let bytes = self.as_slice() + let len = bytes.len() + let i = 0 + while (i < len) { + if (bytes.at(i) == delim) { + let left = build_range(self, 0, i) + let right = build_range(self, i + 1, len) + return Ok(SplitOnce { + left: left, + right: right + }) + } + i = i + 1 + } return Err(()) } - /// Intrinsic; implemented by the runtime. pub fn trim(self) -> string { - return "" + let start_trimmed = self.trim_start() + return start_trimmed.trim_end() } - /// Intrinsic; implemented by the runtime. pub fn trim_start(self) -> string { - return "" + let bytes = self.as_slice() + let len = bytes.len() + let i = 0 + while (i < len) { + if (!bytes.at(i).is_whitespace()) { + break + } + i = i + 1 + } + if (i == 0) { + return self + } + return build_range(self, i, len) } - /// Intrinsic; implemented by the runtime. pub fn trim_end(self) -> string { - return "" + let bytes = self.as_slice() + let len = bytes.len() + if (len == 0) { + return self + } + let i = len + while (i > 0) { + if (!bytes.at(i - 1).is_whitespace()) { + break + } + i = i - 1 + } + if (i == len) { + return self + } + if (i == 0) { + return "" + } + return build_range(self, 0, i) } - /// Intrinsic; implemented by the runtime. pub fn trim_prefix(self, prefix: string) -> string { - return "" + if (self.starts_with(prefix)) { + return build_range(self, prefix.len(), self.len()) + } + return self } - /// Intrinsic; implemented by the runtime. pub fn trim_suffix(self, suffix: string) -> string { - return "" + if (self.ends_with(suffix)) { + return build_range(self, 0, self.len() - suffix.len()) + } + return self } /// split_lines() is an alias for lines(). @@ -160,10 +318,11 @@ impl string { } pub fn index_of_byte(self, needle: u8) -> Result { - let len = self.len() + let bytes = self.as_slice() + let len = bytes.len() let i = 0 while (i < len) { - if (self.byte_at(i) == needle) { + if (bytes.at(i) == needle) { return Ok(i) } i = i + 1 @@ -172,13 +331,14 @@ impl string { } pub fn last_index_of_byte(self, needle: u8) -> Result { - let len = self.len() + let bytes = self.as_slice() + let len = bytes.len() if (len == 0) { return Err(()) } let i = len - 1 while (true) { - if (self.byte_at(i) == needle) { + if (bytes.at(i) == needle) { return Ok(i) } if (i == 0) { @@ -197,11 +357,12 @@ impl string { } pub fn count_byte(self, needle: u8) -> i32 { - let len = self.len() + let bytes = self.as_slice() + let len = bytes.len() let i = 0 let count = 0 while (i < len) { - if (self.byte_at(i) == needle) { + if (bytes.at(i) == needle) { count = count + 1 } i = i + 1 @@ -210,14 +371,77 @@ impl string { } pub fn is_ascii(self) -> bool { - let len = self.len() + let bytes = self.as_slice() + let len = bytes.len() let i = 0 while (i < len) { - if (self.byte_at(i) > '\x7f') { + if (bytes.at(i) > '\x7f') { return false } i = i + 1 } return true } + + pub fn to_lower_ascii(self) -> string { + let bytes = self.as_slice() + let len = bytes.len() + let buf_result = buffer::new(0) + match (buf_result) { + Ok(buf) => { + let i = 0 + while (i < len) { + let b = bytes.at(i) + let lower = lower_ascii_byte(b) + match (buf.push(lower)) { + Ok(_) => { } + Err(_) => { panic() } + } + i = i + 1 + } + match (buf.to_string()) { + Ok(out) => { return out } + Err(_) => { panic() } + } + } + Err(_) => { panic() } + } + } + + pub fn to_upper_ascii(self) -> string { + let bytes = self.as_slice() + let len = bytes.len() + let buf_result = buffer::new(0) + match (buf_result) { + Ok(buf) => { + let i = 0 + while (i < len) { + let b = bytes.at(i) + let upper = upper_ascii_byte(b) + match (buf.push(upper)) { + Ok(_) => { } + Err(_) => { panic() } + } + i = i + 1 + } + match (buf.to_string()) { + Ok(out) => { return out } + Err(_) => { panic() } + } + } + Err(_) => { panic() } + } + } + + pub fn trim_ascii(self) -> string { + return self.trim() + } + + pub fn find_byte(self, needle: u8) -> Result { + return self.index_of_byte(needle) + } + + pub fn rfind_byte(self, needle: u8) -> Result { + return self.last_index_of_byte(needle) + } } diff --git a/stdlib/sys/vec.cap b/stdlib/sys/vec.cap index b2484ed..c083760 100644 --- a/stdlib/sys/vec.cap +++ b/stdlib/sys/vec.cap @@ -18,6 +18,10 @@ impl VecU8 { return 0 } + pub fn is_empty(self) -> bool { + return self.len() == 0 + } + pub fn get(self, i: i32) -> Result { return Err(VecErr::OutOfRange) } @@ -34,6 +38,20 @@ impl VecU8 { return Err(buffer::AllocErr::Oom) } + pub fn extend_slice(self, data: Slice) -> Result { + let len = data.len() + let i = 0 + while (i < len) { + self.push(data.at(i))? + i = i + 1 + } + return Ok(()) + } + + pub fn push_all(self, other: VecU8) -> Result { + return self.extend(other) + } + pub fn filter(self, value: u8) -> VecU8 { return () } @@ -53,6 +71,16 @@ impl VecU8 { pub fn as_slice(self) -> Slice { return () } + + pub fn clear(self) -> unit { + while (true) { + match (self.pop()) { + Ok(_) => { } + Err(_) => { break } + } + } + return () + } } impl VecI32 { @@ -60,6 +88,10 @@ impl VecI32 { return 0 } + pub fn is_empty(self) -> bool { + return self.len() == 0 + } + pub fn get(self, i: i32) -> Result { return Err(VecErr::OutOfRange) } @@ -76,6 +108,10 @@ impl VecI32 { return Err(buffer::AllocErr::Oom) } + pub fn push_all(self, other: VecI32) -> Result { + return self.extend(other) + } + pub fn filter(self, value: i32) -> VecI32 { return () } @@ -87,6 +123,16 @@ impl VecI32 { pub fn pop(self) -> Result { return Err(VecErr::Empty) } + + pub fn clear(self) -> unit { + while (true) { + match (self.pop()) { + Ok(_) => { } + Err(_) => { break } + } + } + return () + } } impl VecString { @@ -94,6 +140,10 @@ impl VecString { return 0 } + pub fn is_empty(self) -> bool { + return self.len() == 0 + } + pub fn get(self, i: i32) -> Result { return Err(VecErr::OutOfRange) } @@ -106,7 +156,39 @@ impl VecString { return Err(buffer::AllocErr::Oom) } + pub fn push_all(self, other: VecString) -> Result { + return self.extend(other) + } + pub fn pop(self) -> Result { return Err(VecErr::Empty) } + + pub fn clear(self) -> unit { + while (true) { + match (self.pop()) { + Ok(_) => { } + Err(_) => { break } + } + } + return () + } + + pub fn join(self, sep: string) -> Result { + let len = self.len() + let buf = buffer::new(0)? + let i = 0 + while (i < len) { + let part = match (self.get(i)) { + Ok(v) => { v } + Err(_) => { panic() } + } + if (i > 0) { + buf.push_str(sep)? + } + buf.push_str(part)? + i = i + 1 + } + return buf.to_string() + } } diff --git a/tests/programs/string_helpers.cap b/tests/programs/string_helpers.cap index 5c5bf9c..0824dba 100644 --- a/tests/programs/string_helpers.cap +++ b/tests/programs/string_helpers.cap @@ -13,6 +13,9 @@ pub fn main(rc: RootCap) -> i32 { let trimmed = " hi \n".trim() let trimmed_start = " hi ".trim_start() let trimmed_end = " hi ".trim_end() + let trimmed_ascii = " \tHi\n".trim_ascii() + let lower = "AbC".to_lower_ascii() + let upper = "AbC".to_upper_ascii() let lines = "a\nb\n".split_lines() let alloc = rc.mint_alloc_default() alloc.vec_string_free(words) @@ -23,23 +26,47 @@ pub fn main(rc: RootCap) -> i32 { c.assert(trimmed.ends_with_byte(105u8)) c.assert(trimmed_start.starts_with("hi")) c.assert(trimmed_end.ends_with("hi")) + c.assert(trimmed_ascii.eq("Hi")) + c.assert(lower.eq("abc")) + c.assert(upper.eq("ABC")) c.assert("abc".starts_with_byte('a')) c.assert("abc".ends_with_byte('c')) c.assert("".is_empty()) c.assert(!"a".is_empty()) c.assert("abc".contains_byte(98u8)) c.assert(!"abc".contains_byte(120u8)) - match ("abc".index_of_byte(99u8)) { + match ("abc".index_of_byte('c')) { Ok(i) => { c.assert(i == 2) } Err(_) => { c.assert(false) } } - match ("abca".last_index_of_byte(97u8)) { + match ("abca".last_index_of_byte('a')) { Ok(i) => { c.assert(i == 3) } Err(_) => { c.assert(false) } } - c.assert("abca".count_byte(97u8) == 2) + match ("abca".find_byte('a')) { + Ok(i) => { c.assert(i == 0) } + Err(_) => { c.assert(false) } + } + match ("abca".rfind_byte('a')) { + Ok(i) => { c.assert(i == 3) } + Err(_) => { c.assert(false) } + } + c.assert("abca".count_byte('a') == 2) c.assert("abc".is_ascii()) c.assert("abc".byte_at_checked(10).is_err()) + match ("a,b,c".split_once(',')) { + Ok(parts) => { + c.assert(parts.left.eq("a")) + c.assert(parts.right.eq("b,c")) + } + Err(_) => { c.assert(false) } + } + let pieces = "a,b,c".split(',') + c.assert(pieces.len() == 3) + match (pieces.join(",")) { + Ok(joined) => { c.assert(joined.eq("a,b,c")) } + Err(_) => { c.assert(false) } + } c.println("string ok") return 0 } From 1df869930254219d1fffe6d86ef77a4584342bd1 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Thu, 1 Jan 2026 16:57:37 -0800 Subject: [PATCH 27/30] codegen: lower struct returns via sret/result-out Only applies to non-opaque structs and Result payloads; no inline struct ABI yet. --- capc/src/codegen/emit.rs | 468 ++++++++++++++++++++++++++++++++------- capc/src/codegen/mod.rs | 142 +++++++++++- 2 files changed, 519 insertions(+), 91 deletions(-) diff --git a/capc/src/codegen/emit.rs b/capc/src/codegen/emit.rs index fc48691..5cc023c 100644 --- a/capc/src/codegen/emit.rs +++ b/capc/src/codegen/emit.rs @@ -15,7 +15,7 @@ use crate::abi::AbiType; use crate::ast::{BinaryOp, Literal, UnaryOp}; use super::{ - CodegenError, EnumIndex, Flow, FnInfo, FnSig, LocalValue, ResultKind, ResultShape, + CodegenError, EnumIndex, Flow, FnInfo, LocalValue, ResultKind, ResultShape, StructLayoutIndex, TypeLayout, ValueRepr, }; use super::abi_quirks; @@ -39,6 +39,21 @@ pub(super) struct LoopTarget { pub exit_block: ir::Block, } +#[derive(Clone, Debug)] +pub(super) enum ReturnLowering { + Direct, + SRet { + out_ptr: ir::Value, + ret_ty: crate::hir::HirType, + }, + ResultOut { + out_ok: Option, + out_err: Option, + ok_ty: crate::hir::HirType, + err_ty: crate::hir::HirType, + }, +} + #[derive(Copy, Clone, Debug, PartialEq, Eq)] enum DeferScopeKind { Regular, @@ -94,6 +109,7 @@ impl DeferStack { fn_map: &HashMap, enum_index: &EnumIndex, struct_layouts: &StructLayoutIndex, + return_lowering: &ReturnLowering, module: &mut ObjectModule, data_counter: &mut u32, ) -> Result<(), CodegenError> { @@ -105,6 +121,7 @@ impl DeferStack { fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -119,6 +136,7 @@ impl DeferStack { fn_map: &HashMap, enum_index: &EnumIndex, struct_layouts: &StructLayoutIndex, + return_lowering: &ReturnLowering, module: &mut ObjectModule, data_counter: &mut u32, ) -> Result<(), CodegenError> { @@ -130,6 +148,7 @@ impl DeferStack { fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -145,6 +164,7 @@ impl DeferStack { fn_map: &HashMap, enum_index: &EnumIndex, struct_layouts: &StructLayoutIndex, + return_lowering: &ReturnLowering, module: &mut ObjectModule, data_counter: &mut u32, ) -> Result<(), CodegenError> { @@ -156,6 +176,7 @@ impl DeferStack { fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -170,6 +191,7 @@ impl DeferStack { fn_map: &HashMap, enum_index: &EnumIndex, struct_layouts: &StructLayoutIndex, + return_lowering: &ReturnLowering, module: &mut ObjectModule, data_counter: &mut u32, ) -> Result<(), CodegenError> { @@ -181,6 +203,7 @@ impl DeferStack { fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -203,6 +226,7 @@ pub(super) fn emit_hir_stmt( module: &mut ObjectModule, data_counter: &mut u32, loop_target: Option, + return_lowering: &ReturnLowering, defer_stack: &mut DeferStack, ) -> Result { emit_hir_stmt_inner( @@ -215,6 +239,7 @@ pub(super) fn emit_hir_stmt( module, data_counter, loop_target, + return_lowering, defer_stack, ) .map_err(|err| err.with_span(stmt.span())) @@ -230,6 +255,7 @@ fn emit_hir_stmt_inner( module: &mut ObjectModule, data_counter: &mut u32, loop_target: Option, + return_lowering: &ReturnLowering, defer_stack: &mut DeferStack, ) -> Result { use crate::hir::HirStmt; @@ -255,6 +281,7 @@ fn emit_hir_stmt_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -299,6 +326,7 @@ fn emit_hir_stmt_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -349,26 +377,96 @@ fn emit_hir_stmt_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; - defer_stack.emit_all_and_clear( - builder, - locals, - fn_map, - enum_index, - struct_layouts, - module, - data_counter, - )?; - match value { - ValueRepr::Unit => builder.ins().return_(&[]), - ValueRepr::Single(val) => builder.ins().return_(&[val]), - ValueRepr::Pair(_, _) | ValueRepr::Result { .. } => { - let values = flatten_value(&value); - builder.ins().return_(&values) + match return_lowering { + ReturnLowering::Direct => { + defer_stack.emit_all_and_clear( + builder, + locals, + fn_map, + enum_index, + struct_layouts, + return_lowering, + module, + data_counter, + )?; + match value { + ValueRepr::Unit => builder.ins().return_(&[]), + ValueRepr::Single(val) => builder.ins().return_(&[val]), + ValueRepr::Pair(_, _) | ValueRepr::Result { .. } => { + let values = flatten_value(&value); + builder.ins().return_(&values) + } + }; } - }; + ReturnLowering::SRet { out_ptr, ret_ty } => { + store_out_value( + builder, + *out_ptr, + ret_ty, + value, + struct_layouts, + module, + )?; + defer_stack.emit_all_and_clear( + builder, + locals, + fn_map, + enum_index, + struct_layouts, + return_lowering, + module, + data_counter, + )?; + builder.ins().return_(&[]); + } + ReturnLowering::ResultOut { + out_ok, + out_err, + ok_ty, + err_ty, + } => { + let ValueRepr::Result { tag, ok, err } = value else { + return Err(CodegenError::Unsupported( + "return expects Result value".to_string(), + )); + }; + if let Some(out_ok) = out_ok { + store_out_value( + builder, + *out_ok, + ok_ty, + *ok, + struct_layouts, + module, + )?; + } + if let Some(out_err) = out_err { + store_out_value( + builder, + *out_err, + err_ty, + *err, + struct_layouts, + module, + )?; + } + defer_stack.emit_all_and_clear( + builder, + locals, + fn_map, + enum_index, + struct_layouts, + return_lowering, + module, + data_counter, + )?; + builder.ins().return_(&[tag]); + } + } } else { defer_stack.emit_all_and_clear( builder, @@ -376,6 +474,7 @@ fn emit_hir_stmt_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -392,6 +491,7 @@ fn emit_hir_stmt_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -413,6 +513,7 @@ fn emit_hir_stmt_inner( module, data_counter, loop_target, + return_lowering, defer_stack, )?; if diverged { @@ -429,6 +530,7 @@ fn emit_hir_stmt_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -453,6 +555,7 @@ fn emit_hir_stmt_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -478,6 +581,7 @@ fn emit_hir_stmt_inner( module, data_counter, loop_target, + return_lowering, &mut then_defers, )?; if flow == Flow::Terminated { @@ -492,6 +596,7 @@ fn emit_hir_stmt_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -517,6 +622,7 @@ fn emit_hir_stmt_inner( module, data_counter, loop_target, + return_lowering, &mut else_defers, )?; if flow == Flow::Terminated { @@ -531,6 +637,7 @@ fn emit_hir_stmt_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -564,6 +671,7 @@ fn emit_hir_stmt_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -597,6 +705,7 @@ fn emit_hir_stmt_inner( module, data_counter, body_loop_target, + return_lowering, &mut body_defers, )?; if flow == Flow::Terminated { @@ -612,6 +721,7 @@ fn emit_hir_stmt_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -646,6 +756,7 @@ fn emit_hir_stmt_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -663,6 +774,7 @@ fn emit_hir_stmt_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -714,6 +826,7 @@ fn emit_hir_stmt_inner( module, data_counter, body_loop_target, + return_lowering, &mut body_defers, )?; if flow == Flow::Terminated { @@ -730,6 +843,7 @@ fn emit_hir_stmt_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -762,6 +876,7 @@ fn emit_hir_stmt_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -776,6 +891,7 @@ fn emit_hir_stmt_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -794,6 +910,7 @@ fn emit_hir_expr( fn_map: &HashMap, enum_index: &EnumIndex, struct_layouts: &StructLayoutIndex, + return_lowering: &ReturnLowering, module: &mut ObjectModule, data_counter: &mut u32, ) -> Result { @@ -804,6 +921,7 @@ fn emit_hir_expr( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, ) @@ -817,6 +935,7 @@ fn emit_hir_expr_inner( fn_map: &HashMap, enum_index: &EnumIndex, struct_layouts: &StructLayoutIndex, + return_lowering: &ReturnLowering, module: &mut ObjectModule, data_counter: &mut u32, ) -> Result { @@ -876,6 +995,7 @@ fn emit_hir_expr_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )? @@ -897,6 +1017,7 @@ fn emit_hir_expr_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )? @@ -949,6 +1070,7 @@ fn emit_hir_expr_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -1023,6 +1145,7 @@ fn emit_hir_expr_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -1038,9 +1161,30 @@ fn emit_hir_expr_inner( .ok_or_else(|| CodegenError::UnknownFunction(key.clone()))? .clone(); ensure_abi_sig_handled(&info)?; + let abi_sig = info.abi_sig.as_ref().unwrap_or(&info.sig); // Emit arguments let mut args = Vec::new(); + let mut sret_ptr = None; + if info.sig.ret == AbiType::Ptr + && abi_sig.ret == AbiType::Unit + && is_non_opaque_struct_type(&call.ret_ty, struct_layouts) + { + let layout = + resolve_struct_layout(&call.ret_ty.ty, "", &struct_layouts.layouts).ok_or_else( + || CodegenError::Unsupported("struct layout missing".to_string()), + )?; + let ptr_ty = module.isa().pointer_type(); + let align = layout.align.max(1); + let slot_size = layout.size.max(1).saturating_add(align - 1); + let slot = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + slot_size, + )); + let base_ptr = aligned_stack_addr(builder, slot, align, ptr_ty); + args.push(base_ptr); + sret_ptr = Some(base_ptr); + } for arg in &call.args { let value = emit_hir_expr( builder, @@ -1049,6 +1193,7 @@ fn emit_hir_expr_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -1058,7 +1203,26 @@ fn emit_hir_expr_inner( // Handle result out-parameters (same logic as AST version) let mut out_slots: Option = None; let mut result_out = None; - let abi_sig = info.abi_sig.as_ref().unwrap_or(&info.sig); + let result_payloads = match &call.ret_ty.ty { + crate::typeck::Ty::Path(name, args) + if name == "sys.result.Result" && args.len() == 2 => + { + match &call.ret_ty.abi { + AbiType::Result(ok_abi, err_abi) => Some(( + crate::hir::HirType { + ty: args[0].clone(), + abi: (**ok_abi).clone(), + }, + crate::hir::HirType { + ty: args[1].clone(), + abi: (**err_abi).clone(), + }, + )), + _ => None, + } + } + _ => None, + }; if abi_quirks::is_result_string(&abi_sig.ret) { let ptr_ty = module.isa().pointer_type(); @@ -1067,35 +1231,106 @@ fn emit_hir_expr_inner( out_slots = Some(slots); } + enum ResultOutSlot { + Scalar(ir::StackSlot, ir::Type, u32), + Struct(ir::Value), + } + if let AbiType::ResultOut(ok_ty, err_ty) = &abi_sig.ret { let ptr_ty = module.isa().pointer_type(); let ok_slot = if **ok_ty == AbiType::Unit { None } else { - let ty = value_type_for_result_out(ok_ty, ptr_ty)?; - let align = ty.bytes().max(1) as u32; - debug_assert!(align.is_power_of_two()); - let slot = builder.create_sized_stack_slot(ir::StackSlotData::new( - ir::StackSlotKind::ExplicitSlot, - aligned_slot_size(ty.bytes().max(1) as u32, align), - )); - let addr = aligned_stack_addr(builder, slot, align, ptr_ty); - args.push(addr); - Some((slot, ty, align)) + if let Some((ok_hir, _)) = &result_payloads { + if is_non_opaque_struct_type(ok_hir, struct_layouts) { + let layout = resolve_struct_layout( + &ok_hir.ty, + "", + &struct_layouts.layouts, + ) + .ok_or_else(|| { + CodegenError::Unsupported("struct layout missing".to_string()) + })?; + let align = layout.align.max(1); + let slot_size = layout.size.max(1).saturating_add(align - 1); + let slot = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + slot_size, + )); + let addr = aligned_stack_addr(builder, slot, align, ptr_ty); + args.push(addr); + Some(ResultOutSlot::Struct(addr)) + } else { + let ty = value_type_for_result_out(ok_ty, ptr_ty)?; + let align = ty.bytes().max(1) as u32; + debug_assert!(align.is_power_of_two()); + let slot = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + aligned_slot_size(ty.bytes().max(1) as u32, align), + )); + let addr = aligned_stack_addr(builder, slot, align, ptr_ty); + args.push(addr); + Some(ResultOutSlot::Scalar(slot, ty, align)) + } + } else { + let ty = value_type_for_result_out(ok_ty, ptr_ty)?; + let align = ty.bytes().max(1) as u32; + debug_assert!(align.is_power_of_two()); + let slot = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + aligned_slot_size(ty.bytes().max(1) as u32, align), + )); + let addr = aligned_stack_addr(builder, slot, align, ptr_ty); + args.push(addr); + Some(ResultOutSlot::Scalar(slot, ty, align)) + } }; let err_slot = if **err_ty == AbiType::Unit { None } else { - let ty = value_type_for_result_out(err_ty, ptr_ty)?; - let align = ty.bytes().max(1) as u32; - debug_assert!(align.is_power_of_two()); - let slot = builder.create_sized_stack_slot(ir::StackSlotData::new( - ir::StackSlotKind::ExplicitSlot, - aligned_slot_size(ty.bytes().max(1) as u32, align), - )); - let addr = aligned_stack_addr(builder, slot, align, ptr_ty); - args.push(addr); - Some((slot, ty, align)) + if let Some((_, err_hir)) = &result_payloads { + if is_non_opaque_struct_type(err_hir, struct_layouts) { + let layout = resolve_struct_layout( + &err_hir.ty, + "", + &struct_layouts.layouts, + ) + .ok_or_else(|| { + CodegenError::Unsupported("struct layout missing".to_string()) + })?; + let align = layout.align.max(1); + let slot_size = layout.size.max(1).saturating_add(align - 1); + let slot = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + slot_size, + )); + let addr = aligned_stack_addr(builder, slot, align, ptr_ty); + args.push(addr); + Some(ResultOutSlot::Struct(addr)) + } else { + let ty = value_type_for_result_out(err_ty, ptr_ty)?; + let align = ty.bytes().max(1) as u32; + debug_assert!(align.is_power_of_two()); + let slot = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + aligned_slot_size(ty.bytes().max(1) as u32, align), + )); + let addr = aligned_stack_addr(builder, slot, align, ptr_ty); + args.push(addr); + Some(ResultOutSlot::Scalar(slot, ty, align)) + } + } else { + let ty = value_type_for_result_out(err_ty, ptr_ty)?; + let align = ty.bytes().max(1) as u32; + debug_assert!(align.is_power_of_two()); + let slot = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + aligned_slot_size(ty.bytes().max(1) as u32, align), + )); + let addr = aligned_stack_addr(builder, slot, align, ptr_ty); + args.push(addr); + Some(ResultOutSlot::Scalar(slot, ty, align)) + } }; result_out = Some((ok_slot, err_slot, ok_ty.clone(), err_ty.clone())); } @@ -1154,27 +1389,37 @@ fn emit_hir_expr_inner( .ok_or_else(|| CodegenError::Codegen("missing result tag".to_string()))?; let (ok_slot, err_slot, ok_ty, err_ty) = result_out .ok_or_else(|| CodegenError::Codegen("missing result slots".to_string()))?; - let ok_val = if let Some((slot, ty, align)) = ok_slot { - let addr = aligned_stack_addr( - builder, - slot, - align, - module.isa().pointer_type(), - ); - let val = builder.ins().load(ty, MemFlags::new(), addr, 0); - ValueRepr::Single(val) + let ok_val = if let Some(slot) = ok_slot { + match slot { + ResultOutSlot::Scalar(slot, ty, align) => { + let addr = aligned_stack_addr( + builder, + slot, + align, + module.isa().pointer_type(), + ); + let val = builder.ins().load(ty, MemFlags::new(), addr, 0); + ValueRepr::Single(val) + } + ResultOutSlot::Struct(ptr) => ValueRepr::Single(ptr), + } } else { ValueRepr::Unit }; - let err_val = if let Some((slot, ty, align)) = err_slot { - let addr = aligned_stack_addr( - builder, - slot, - align, - module.isa().pointer_type(), - ); - let val = builder.ins().load(ty, MemFlags::new(), addr, 0); - ValueRepr::Single(val) + let err_val = if let Some(slot) = err_slot { + match slot { + ResultOutSlot::Scalar(slot, ty, align) => { + let addr = aligned_stack_addr( + builder, + slot, + align, + module.isa().pointer_type(), + ); + let val = builder.ins().load(ty, MemFlags::new(), addr, 0); + ValueRepr::Single(val) + } + ResultOutSlot::Struct(ptr) => ValueRepr::Single(ptr), + } } else { ValueRepr::Unit }; @@ -1188,6 +1433,8 @@ fn emit_hir_expr_inner( "result out params for {ok_ty:?}/{err_ty:?}" ))), } + } else if let Some(ptr) = sret_ptr { + Ok(ValueRepr::Single(ptr)) } else { let mut index = 0; value_from_results(builder, &info.sig.ret, &results, &mut index) @@ -1201,6 +1448,7 @@ fn emit_hir_expr_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -1226,6 +1474,7 @@ fn emit_hir_expr_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, ); @@ -1238,6 +1487,7 @@ fn emit_hir_expr_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -1317,6 +1567,7 @@ fn emit_hir_expr_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -1350,6 +1601,7 @@ fn emit_hir_expr_inner( module, data_counter, None, // break/continue not supported in expression-context matches + return_lowering, &mut temp_defers, )?; Ok(ValueRepr::Unit) @@ -1361,6 +1613,7 @@ fn emit_hir_expr_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, ) @@ -1373,6 +1626,7 @@ fn emit_hir_expr_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, ), @@ -1383,6 +1637,7 @@ fn emit_hir_expr_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, ), @@ -1394,6 +1649,7 @@ fn emit_hir_expr_inner( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, ) @@ -1524,6 +1780,7 @@ fn emit_hir_index( fn_map: &HashMap, enum_index: &EnumIndex, struct_layouts: &StructLayoutIndex, + return_lowering: &ReturnLowering, module: &mut ObjectModule, data_counter: &mut u32, ) -> Result { @@ -1535,6 +1792,7 @@ fn emit_hir_index( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -1547,6 +1805,7 @@ fn emit_hir_index( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -1735,6 +1994,7 @@ fn emit_hir_struct_literal( fn_map: &HashMap, enum_index: &EnumIndex, struct_layouts: &StructLayoutIndex, + return_lowering: &ReturnLowering, module: &mut ObjectModule, data_counter: &mut u32, ) -> Result { @@ -1745,7 +2005,14 @@ fn emit_hir_struct_literal( literal.struct_ty.ty )) })?; - let base_ptr = emit_heap_alloc(builder, module, layout.size.max(1) as i32)?; + let ptr_ty = module.isa().pointer_type(); + let align = layout.align.max(1); + let slot_size = layout.size.max(1).saturating_add(align - 1); + let slot = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + slot_size, + )); + let base_ptr = aligned_stack_addr(builder, slot, align, ptr_ty); for field in &literal.fields { let Some(field_layout) = layout.fields.get(&field.name) else { @@ -1761,6 +2028,7 @@ fn emit_hir_struct_literal( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -1778,34 +2046,6 @@ fn emit_hir_struct_literal( Ok(ValueRepr::Single(base_ptr)) } -fn emit_heap_alloc( - builder: &mut FunctionBuilder, - module: &mut ObjectModule, - size: i32, -) -> Result { - let handle = builder.ins().iconst(ir::types::I64, 0); - let size_val = builder.ins().iconst(ir::types::I32, size as i64); - let sig = FnSig { - params: vec![AbiType::Handle, AbiType::I32], - ret: AbiType::Ptr, - }; - let sig = sig_to_clif( - &sig, - module.isa().pointer_type(), - module.isa().default_call_conv(), - ); - let func_id = module - .declare_function("capable_rt_malloc", Linkage::Import, &sig) - .map_err(|err| CodegenError::Codegen(err.to_string()))?; - let local = module.declare_func_in_func(func_id, builder.func); - let call_inst = builder.ins().call(local, &[handle, size_val]); - let results = builder.inst_results(call_inst); - results - .get(0) - .copied() - .ok_or_else(|| CodegenError::Codegen("missing malloc result".to_string())) -} - /// Emit field access by computing the field address/offset. fn emit_hir_field_access( builder: &mut FunctionBuilder, @@ -1814,6 +2054,7 @@ fn emit_hir_field_access( fn_map: &HashMap, enum_index: &EnumIndex, struct_layouts: &StructLayoutIndex, + return_lowering: &ReturnLowering, module: &mut ObjectModule, data_counter: &mut u32, ) -> Result { @@ -1839,6 +2080,7 @@ fn emit_hir_field_access( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -1861,6 +2103,51 @@ fn emit_hir_field_access( ) } +fn is_non_opaque_struct_type( + ty: &crate::hir::HirType, + struct_layouts: &StructLayoutIndex, +) -> bool { + resolve_struct_layout(&ty.ty, "", &struct_layouts.layouts).is_some() +} + +fn store_out_value( + builder: &mut FunctionBuilder, + out_ptr: ir::Value, + ty: &crate::hir::HirType, + value: ValueRepr, + struct_layouts: &StructLayoutIndex, + module: &mut ObjectModule, +) -> Result<(), CodegenError> { + if is_non_opaque_struct_type(ty, struct_layouts) { + return store_value_by_ty( + builder, + out_ptr, + 0, + ty, + value, + struct_layouts, + module, + ); + } + match ty.abi { + AbiType::I32 + | AbiType::U32 + | AbiType::U8 + | AbiType::Bool + | AbiType::Handle + | AbiType::Ptr => store_value_by_tykind( + builder, + out_ptr, + &ty.abi, + value, + module.isa().pointer_type(), + ), + _ => Err(CodegenError::Unsupported( + "return out param type".to_string(), + )), + } +} + /// Store a lowered value into memory using a typeck::Ty layout. fn store_value_by_ty( builder: &mut FunctionBuilder, @@ -2290,6 +2577,7 @@ fn emit_hir_short_circuit_expr( fn_map: &HashMap, enum_index: &EnumIndex, struct_layouts: &StructLayoutIndex, + return_lowering: &ReturnLowering, module: &mut ObjectModule, data_counter: &mut u32, ) -> Result { @@ -2317,6 +2605,7 @@ fn emit_hir_short_circuit_expr( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -2344,6 +2633,7 @@ fn emit_hir_match_stmt( module: &mut ObjectModule, data_counter: &mut u32, loop_target: Option, + return_lowering: &ReturnLowering, defer_stack: &mut DeferStack, ) -> Result { // Emit the scrutinee expression @@ -2354,6 +2644,7 @@ fn emit_hir_match_stmt( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -2430,6 +2721,7 @@ fn emit_hir_match_stmt( module, data_counter, loop_target, + return_lowering, &mut arm_defers, )?; if flow == Flow::Terminated { @@ -2446,6 +2738,7 @@ fn emit_hir_match_stmt( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -2478,6 +2771,7 @@ fn emit_hir_match_expr( fn_map: &HashMap, enum_index: &EnumIndex, struct_layouts: &StructLayoutIndex, + return_lowering: &ReturnLowering, module: &mut ObjectModule, data_counter: &mut u32, ) -> Result { @@ -2491,6 +2785,7 @@ fn emit_hir_match_expr( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -2562,6 +2857,7 @@ fn emit_hir_match_expr( module, data_counter, None, // break/continue not allowed in value-producing match + return_lowering, &mut arm_defers, )?; if flow == Flow::Terminated { @@ -2589,6 +2885,7 @@ fn emit_hir_match_expr( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; @@ -2656,6 +2953,7 @@ fn emit_hir_match_expr( fn_map, enum_index, struct_layouts, + return_lowering, module, data_counter, )?; diff --git a/capc/src/codegen/mod.rs b/capc/src/codegen/mod.rs index 81a281d..b54e6d3 100644 --- a/capc/src/codegen/mod.rs +++ b/capc/src/codegen/mod.rs @@ -28,8 +28,11 @@ mod abi_quirks; mod intrinsics; mod layout; -use emit::{emit_hir_stmt, emit_runtime_wrapper_call, flatten_value, store_local, value_from_params, DeferStack}; -use layout::{build_enum_index, build_struct_layout_index}; +use emit::{ + emit_hir_stmt, emit_runtime_wrapper_call, flatten_value, store_local, value_from_params, + DeferStack, ReturnLowering, +}; +use layout::{build_enum_index, build_struct_layout_index, resolve_struct_layout}; #[derive(Debug, Error, Diagnostic)] #[allow(unused_assignments)] @@ -224,6 +227,7 @@ pub fn build_object( &enum_index, &mut fn_map, &runtime_intrinsics, + &struct_layouts, module.isa().pointer_type(), true, )?; @@ -236,6 +240,7 @@ pub fn build_object( &enum_index, &mut fn_map, &runtime_intrinsics, + &struct_layouts, module.isa().pointer_type(), false, )?; @@ -246,6 +251,7 @@ pub fn build_object( &enum_index, &mut fn_map, &runtime_intrinsics, + &struct_layouts, module.isa().pointer_type(), false, )?; @@ -257,7 +263,7 @@ pub fn build_object( .collect::>(); for module_ref in &all_modules { - register_extern_functions_from_hir(module_ref, &mut fn_map)?; + register_extern_functions_from_hir(module_ref, &mut fn_map, &struct_layouts)?; } let mut data_counter = 0u32; @@ -272,12 +278,18 @@ pub fn build_object( if info.is_runtime { continue; } + let abi_sig = info.abi_sig.as_ref().unwrap_or(&info.sig); + let sig_for_codegen = if info.runtime_symbol.is_some() { + &info.sig + } else { + abi_sig + }; let func_id = module .declare_function( &info.symbol, Linkage::Export, &sig_to_clif( - &info.sig, + sig_for_codegen, module.isa().pointer_type(), module.isa().default_call_conv(), ), @@ -287,7 +299,7 @@ pub fn build_object( ctx.func = Function::with_name_signature( ir::UserFuncName::user(0, func_id.as_u32()), sig_to_clif( - &info.sig, + sig_for_codegen, module.isa().pointer_type(), module.isa().default_call_conv(), ), @@ -326,12 +338,81 @@ pub fn build_object( let mut locals: HashMap = HashMap::new(); let params = builder.block_params(block).to_vec(); let mut param_index = 0; + let mut return_lowering = ReturnLowering::Direct; + + if info.sig.ret == AbiType::Ptr + && abi_sig.ret == AbiType::Unit + && resolve_struct_layout(&func.ret_ty.ty, "", &struct_layouts.layouts).is_some() + { + let out_ptr = params + .get(0) + .copied() + .ok_or_else(|| CodegenError::Codegen("missing sret param".to_string()))?; + return_lowering = ReturnLowering::SRet { + out_ptr, + ret_ty: func.ret_ty.clone(), + }; + param_index = 1; + } else if let AbiType::ResultOut(ok_abi, err_abi) = &abi_sig.ret { + let (ok_ty, err_ty) = match &func.ret_ty.ty { + crate::typeck::Ty::Path(name, args) + if name == "sys.result.Result" && args.len() == 2 => + { + ( + crate::hir::HirType { + ty: args[0].clone(), + abi: (**ok_abi).clone(), + }, + crate::hir::HirType { + ty: args[1].clone(), + abi: (**err_abi).clone(), + }, + ) + } + _ => { + return Err(CodegenError::Codegen( + "result out params missing result type".to_string(), + )) + } + }; + return_lowering = ReturnLowering::ResultOut { + out_ok: None, + out_err: None, + ok_ty, + err_ty, + }; + } for param in &func.params { let value = value_from_params(&mut builder, ¶m.ty.abi, ¶ms, &mut param_index)?; let local = store_local(&mut builder, value); locals.insert(param.local_id, local); } + if let ReturnLowering::ResultOut { + out_ok, + out_err, + ok_ty, + err_ty, + } = &mut return_lowering + { + if ok_ty.abi != AbiType::Unit { + *out_ok = Some( + params + .get(param_index) + .copied() + .ok_or_else(|| CodegenError::Codegen("missing ok out param".to_string()))?, + ); + param_index += 1; + } + if err_ty.abi != AbiType::Unit { + *out_err = Some( + params + .get(param_index) + .copied() + .ok_or_else(|| CodegenError::Codegen("missing err out param".to_string()))?, + ); + } + } let mut defer_stack = DeferStack::new(); defer_stack.push_block_scope(); @@ -347,6 +428,7 @@ pub fn build_object( &mut module, &mut data_counter, None, // no loop context at function top level + &return_lowering, &mut defer_stack, )?; if flow == Flow::Terminated { @@ -362,6 +444,7 @@ pub fn build_object( &fn_map, &enum_index, &struct_layouts, + &return_lowering, &mut module, &mut data_counter, )?; @@ -469,6 +552,49 @@ fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { intrinsics::register_runtime_intrinsics(ptr_ty) } +fn is_non_opaque_struct_type( + ty: &crate::typeck::Ty, + struct_layouts: &StructLayoutIndex, +) -> bool { + resolve_struct_layout(ty, "", &struct_layouts.layouts).is_some() +} + +fn lowered_abi_sig_for_return( + sig: &FnSig, + ret_ty: &crate::hir::HirType, + struct_layouts: &StructLayoutIndex, +) -> Option { + if let crate::typeck::Ty::Path(name, args) = &ret_ty.ty { + if name == "sys.result.Result" && args.len() == 2 { + let ok_is_struct = is_non_opaque_struct_type(&args[0], struct_layouts); + let err_is_struct = is_non_opaque_struct_type(&args[1], struct_layouts); + if ok_is_struct || err_is_struct { + let AbiType::Result(ok_abi, err_abi) = &ret_ty.abi else { + return None; + }; + let mut params = sig.params.clone(); + params.push(AbiType::ResultOut(ok_abi.clone(), err_abi.clone())); + return Some(FnSig { + params, + ret: AbiType::ResultOut(ok_abi.clone(), err_abi.clone()), + }); + } + } + } + + if is_non_opaque_struct_type(&ret_ty.ty, struct_layouts) { + let mut params = Vec::with_capacity(sig.params.len() + 1); + params.push(AbiType::Ptr); + params.extend(sig.params.iter().cloned()); + return Some(FnSig { + params, + ret: AbiType::Unit, + }); + } + + None +} + /// Register Capable-defined functions (stdlib or user) into the codegen map. fn register_user_functions( module: &crate::hir::HirModule, @@ -476,6 +602,7 @@ fn register_user_functions( _enum_index: &EnumIndex, map: &mut HashMap, runtime_intrinsics: &HashMap, + struct_layouts: &StructLayoutIndex, _ptr_ty: Type, is_stdlib: bool, ) -> Result<(), CodegenError> { @@ -508,6 +635,7 @@ fn register_user_functions( } else { (None, None) }; + let abi_sig = abi_sig.or_else(|| lowered_abi_sig_for_return(&sig, &func.ret_ty, struct_layouts)); map.insert( key, FnInfo { @@ -526,6 +654,7 @@ fn register_user_functions( fn register_extern_functions_from_hir( module: &crate::hir::HirModule, map: &mut HashMap, + struct_layouts: &StructLayoutIndex, ) -> Result<(), CodegenError> { let module_name = &module.name; for func in &module.extern_functions { @@ -538,11 +667,12 @@ fn register_extern_functions_from_hir( ret: func.ret_ty.abi.clone(), }; let key = format!("{}.{}", module_name, func.name); + let abi_sig = lowered_abi_sig_for_return(&sig, &func.ret_ty, struct_layouts); map.insert( key, FnInfo { sig, - abi_sig: None, + abi_sig, symbol: func.name.clone(), runtime_symbol: None, is_runtime: true, From 29effacb211514a95c94348143375095ae33a9d5 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Thu, 1 Jan 2026 17:00:40 -0800 Subject: [PATCH 28/30] docs: describe ABI lowering for struct returns --- docs/ABI.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/ABI.md diff --git a/docs/ABI.md b/docs/ABI.md new file mode 100644 index 0000000..af9c34b --- /dev/null +++ b/docs/ABI.md @@ -0,0 +1,38 @@ +# ABI Notes + +This doc captures current ABI/lowering rules used by the compiler and runtime. +These rules are intentionally simple and may evolve. + +## Struct returns (sret) + +Capable does not currently return non-opaque structs in registers. Instead it +uses an explicit "structure return" (sret) out-parameter: + +- A function that returns a non-opaque struct is lowered to: + - an extra first parameter: `out: *T` + - return type: `unit` +- The callee writes the struct into `out`. +- Callers allocate stack space and pass `&out`. + +This is a common ABI strategy used by many toolchains for larger values. + +## Result out-params for struct payloads + +When a `Result` payload is a non-opaque struct, the return is lowered to +out-params: + +- The function takes extra `ok_out` / `err_out` pointer params for the + struct payloads. +- The function returns only the Result tag (`Ok`/`Err` discriminator). +- Scalar payloads still use the normal in-register `Result` layout. + +## Runtime wrappers + +Runtime-backed intrinsics keep their original ABI (no sret) and are wrapped by +compiler-generated stubs when needed. + +## Status + +- Inline-by-value struct returns are not implemented yet. +- These rules apply to non-opaque structs only. Opaque/capability types remain + handles and return directly. From 09a908cfbf8829c66de72e90d0ce0ffda3386035 Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Thu, 1 Jan 2026 18:29:20 -0800 Subject: [PATCH 29/30] Fix ABI lowering and reserved type test --- FIXME.md | 50 +- capc/src/codegen/abi_quirks.rs | 36 +- capc/src/codegen/emit.rs | 599 +++++++++--------- capc/src/codegen/mod.rs | 29 +- capc/src/typeck/mod.rs | 31 +- capc/tests/typecheck.rs | 2 +- .../should_fail_reserved_type_name.cap | 2 +- 7 files changed, 366 insertions(+), 383 deletions(-) diff --git a/FIXME.md b/FIXME.md index 02a5383..00eb975 100644 --- a/FIXME.md +++ b/FIXME.md @@ -1,11 +1,39 @@ -FIXME -===== - -Resolved (current branch) -------------------------- -- break/continue linear-consumption checks now only validate scopes exited by - the loop, not all scopes. -- Slice/MutSlice indexing now requires to match runtime behavior. -- Direct runtime calls now use the target's default calling convention. -- Generic-vs-comparison parsing uses a lookahead for `>` followed by `(` or `{` - to avoid false-positive generic parsing. +# FIXME + +Commits on this branch (main..HEAD), in order: + +- 57fc54c Add break and continue statements +- a22c056 Fix nested match codegen panic +- 978f14b Use break in sort example +- d9e0aa3 Add for loop support with range syntax +- a6fe98f Add string comparison operators == and != +- 9b746d3 Add index syntax [] and change generics to <> +- aa09a93 check tests properly +- 7555204 Change default target name from capable +- f64de33 Remove outdated plan file +- 4bd4f97 remove outdated progress file +- 529b9ed move docs +- 4c6e29e Simple ok+err handlers +- f0b3345 Add Vec indexing syntax and fix for-loop range parsing +- c167d11 Move Result enum from compiler builtin to stdlib +- 2f28e94 [WIP] Move Result methods from compiler special-case to stdlib +- 993e805 Fix some stuff +- 7416ad8 Fix loop linearity, indexing constraints, and call conv +- 3f2aca3 Add docs and TODO list +- fc93b87 Add never type and Vec specializations +- d925d5b Add top-level defer statement +- 4763187 Make defer scope-based +- a29d59b Improve net listener and string helpers +- c53e84d Add if let statement sugar +- 15276b0 Add for {} infinite loop sugar +- 4ee669e Add char literals and string helpers +- 48b63f0 stdlib: flesh out string/vec helpers +- 1df8699 codegen: lower struct returns via sret/result-out +- 29effac docs: describe ABI lowering for struct returns + +Fixes applied from review: + +- Allow user-defined `string` type names now that `string` is userland; update reserved-name test to `i32`. +- Support sret lowering in runtime wrapper emission to avoid ABI mismatches. +- Use return lowering in `try` error path to avoid returning wrong signature. +- Initialize zero values for non-opaque structs to avoid null deref when building Result payloads. diff --git a/capc/src/codegen/abi_quirks.rs b/capc/src/codegen/abi_quirks.rs index ecddc0b..9b8b18c 100644 --- a/capc/src/codegen/abi_quirks.rs +++ b/capc/src/codegen/abi_quirks.rs @@ -1,22 +1,12 @@ //! ABI quirks and lowering helpers. //! //! These helpers are the single source of truth for the special-case ABI -//! shapes we use to lower `Result` values (ResultString and ResultOut). +//! shapes we use to lower `Result` values (ResultOut). use crate::abi::AbiType; use super::FnSig; -/// ResultString ABI uses a u64 length slot across targets. -pub fn result_string_len_bytes() -> u32 { - 8 -} - -/// Return true if the ABI type is lowered as ResultString. -pub fn is_result_string(ty: &AbiType) -> bool { - matches!(ty, AbiType::ResultString) -} - /// Return true if the ABI type is lowered using ResultOut parameters. pub fn is_result_out(ty: &AbiType) -> bool { matches!(ty, AbiType::ResultOut(_, _)) @@ -24,12 +14,25 @@ pub fn is_result_out(ty: &AbiType) -> bool { /// Return true if the ABI type uses any Result lowering. pub fn is_result_lowering(ty: &AbiType) -> bool { - is_result_string(ty) || is_result_out(ty) + is_result_out(ty) } -/// Return true if a signature mismatch is explained by Result lowering. +fn is_sret_lowering(abi_sig: &FnSig, sig: &FnSig) -> bool { + if sig.ret != AbiType::Ptr || abi_sig.ret != AbiType::Unit { + return false; + } + if abi_sig.params.len() != sig.params.len() + 1 { + return false; + } + if abi_sig.params.first() != Some(&AbiType::Ptr) { + return false; + } + abi_sig.params[1..] == sig.params +} + +/// Return true if a signature mismatch is explained by Result or sret lowering. pub fn abi_sig_requires_lowering(abi_sig: &FnSig, sig: &FnSig) -> bool { - abi_sig != sig && is_result_lowering(&abi_sig.ret) + abi_sig != sig && (is_result_lowering(&abi_sig.ret) || is_sret_lowering(abi_sig, sig)) } /// Error message used when a layout is requested for a lowered Result ABI. @@ -46,8 +49,3 @@ pub fn result_abi_mismatch_error() -> &'static str { pub fn result_out_params_error() -> &'static str { "result out params" } - -/// Error message used when ResultString lowering is requested but unsupported. -pub fn result_string_params_error() -> &'static str { - "result abi" -} diff --git a/capc/src/codegen/emit.rs b/capc/src/codegen/emit.rs index 5cc023c..57c9d49 100644 --- a/capc/src/codegen/emit.rs +++ b/capc/src/codegen/emit.rs @@ -22,16 +22,6 @@ use super::abi_quirks; use super::layout::{align_to, resolve_struct_layout, type_layout_for_abi}; use super::sig_to_clif; -#[derive(Copy, Clone, Debug)] -struct ResultStringSlots { - slot_ptr: ir::StackSlot, - slot_len: ir::StackSlot, - slot_err: ir::StackSlot, - ptr_align: u32, - len_align: u32, - err_align: u32, -} - /// Target blocks for break/continue inside a loop. #[derive(Copy, Clone, Debug)] pub(super) struct LoopTarget { @@ -396,7 +386,7 @@ fn emit_hir_stmt_inner( match value { ValueRepr::Unit => builder.ins().return_(&[]), ValueRepr::Single(val) => builder.ins().return_(&[val]), - ValueRepr::Pair(_, _) | ValueRepr::Result { .. } => { + ValueRepr::Result { .. } => { let values = flatten_value(&value); builder.ins().return_(&values) } @@ -952,7 +942,9 @@ fn emit_hir_expr_inner( Literal::Bool(value) => Ok(ValueRepr::Single( builder.ins().iconst(ir::types::I8, *value as i64), )), - Literal::String(value) => emit_string(builder, value, module, data_counter), + Literal::String(value) => { + emit_string(builder, value, module, data_counter, struct_layouts) + } Literal::Unit => Ok(ValueRepr::Unit), }, HirExpr::Local(local) => { @@ -1000,15 +992,31 @@ fn emit_hir_expr_inner( data_counter, )? } else { - zero_value_for_ty(builder, &ok_ty, ptr_ty, Some(struct_layouts))? + zero_value_for_ty( + builder, + &ok_ty, + ptr_ty, + Some(struct_layouts), + module, + )? }; - let err_zero = - zero_value_for_ty(builder, &err_ty, ptr_ty, Some(struct_layouts))?; + let err_zero = zero_value_for_ty( + builder, + &err_ty, + ptr_ty, + Some(struct_layouts), + module, + )?; (payload, err_zero) } "Err" => { - let ok_zero = - zero_value_for_ty(builder, &ok_ty, ptr_ty, Some(struct_layouts))?; + let ok_zero = zero_value_for_ty( + builder, + &ok_ty, + ptr_ty, + Some(struct_layouts), + module, + )?; let payload = if let Some(payload_expr) = &variant.payload { emit_hir_expr( builder, @@ -1022,7 +1030,13 @@ fn emit_hir_expr_inner( data_counter, )? } else { - zero_value_for_ty(builder, &err_ty, ptr_ty, Some(struct_layouts))? + zero_value_for_ty( + builder, + &err_ty, + ptr_ty, + Some(struct_layouts), + module, + )? }; (ok_zero, payload) } @@ -1099,8 +1113,13 @@ fn emit_hir_expr_inner( abi: (**ok_abi).clone(), }; let ptr_ty = module.isa().pointer_type(); - let ok_zero = - zero_value_for_ty(builder, &ok_ty, ptr_ty, Some(struct_layouts))?; + let ok_zero = zero_value_for_ty( + builder, + &ok_ty, + ptr_ty, + Some(struct_layouts), + module, + )?; let tag_val = builder.ins().iconst(ir::types::I8, 1); ValueRepr::Result { tag: tag_val, @@ -1114,8 +1133,56 @@ fn emit_hir_expr_inner( )) } }; - let ret_values = flatten_value(&ret_value); - builder.ins().return_(&ret_values); + match return_lowering { + ReturnLowering::Direct => { + let ret_values = flatten_value(&ret_value); + builder.ins().return_(&ret_values); + } + ReturnLowering::SRet { out_ptr, ret_ty } => { + store_out_value( + builder, + *out_ptr, + ret_ty, + ret_value, + struct_layouts, + module, + )?; + builder.ins().return_(&[]); + } + ReturnLowering::ResultOut { + out_ok, + out_err, + ok_ty, + err_ty, + } => { + let ValueRepr::Result { tag, ok, err } = ret_value else { + return Err(CodegenError::Unsupported( + "try expects a Result value".to_string(), + )); + }; + if let Some(out_ok) = out_ok { + store_out_value( + builder, + *out_ok, + ok_ty, + *ok, + struct_layouts, + module, + )?; + } + if let Some(out_err) = out_err { + store_out_value( + builder, + *out_err, + err_ty, + *err, + struct_layouts, + module, + )?; + } + builder.ins().return_(&[tag]); + } + } builder.seal_block(err_block); builder.switch_to_block(ok_block); @@ -1201,7 +1268,6 @@ fn emit_hir_expr_inner( } // Handle result out-parameters (same logic as AST version) - let mut out_slots: Option = None; let mut result_out = None; let result_payloads = match &call.ret_ty.ty { crate::typeck::Ty::Path(name, args) @@ -1224,13 +1290,6 @@ fn emit_hir_expr_inner( _ => None, }; - if abi_quirks::is_result_string(&abi_sig.ret) { - let ptr_ty = module.isa().pointer_type(); - let slots = result_string_slots(builder, ptr_ty); - push_result_string_out_params(builder, ptr_ty, &slots, &mut args); - out_slots = Some(slots); - } - enum ResultOutSlot { Scalar(ir::StackSlot, ir::Type, u32), Struct(ir::Value), @@ -1358,32 +1417,7 @@ fn emit_hir_expr_inner( let results = builder.inst_results(call_inst).to_vec(); // Handle result unpacking (same logic as AST version) - if abi_quirks::is_result_string(&abi_sig.ret) { - let tag = results - .get(0) - .ok_or_else(|| CodegenError::Codegen("missing result tag".to_string()))?; - let slots = - out_slots.ok_or_else(|| CodegenError::Codegen("missing slots".to_string()))?; - let ptr_ty = module.isa().pointer_type(); - let (ptr, len, err) = read_result_string_slots(builder, ptr_ty, &slots); - match &info.sig.ret { - AbiType::Result(ok_ty, err_ty) => { - if **ok_ty != AbiType::String || **err_ty != AbiType::I32 { - return Err(CodegenError::Unsupported( - abi_quirks::result_out_params_error().to_string(), - )); - } - Ok(ValueRepr::Result { - tag: *tag, - ok: Box::new(ValueRepr::Pair(ptr, len)), - err: Box::new(ValueRepr::Single(err)), - }) - } - _ => Err(CodegenError::Unsupported( - abi_quirks::result_out_params_error().to_string(), - )), - } - } else if abi_quirks::is_result_out(&abi_sig.ret) { + if abi_quirks::is_result_out(&abi_sig.ret) { let tag = results .get(0) .ok_or_else(|| CodegenError::Codegen("missing result tag".to_string()))?; @@ -1459,9 +1493,9 @@ fn emit_hir_expr_inner( ValueRepr::Unit => { return Err(CodegenError::Unsupported("boolean op on unit".to_string())) } - ValueRepr::Pair(_, _) | ValueRepr::Result { .. } => { + ValueRepr::Result { .. } => { return Err(CodegenError::Unsupported( - "boolean op on string".to_string(), + "boolean op on result".to_string(), )) } }; @@ -1505,6 +1539,21 @@ fn emit_hir_expr_inner( (BinaryOp::Div, ValueRepr::Single(a), ValueRepr::Single(b)) => { Ok(ValueRepr::Single(emit_checked_div(builder, a, b, &binary.ty)?)) } + (BinaryOp::Eq, ValueRepr::Single(a), ValueRepr::Single(b)) + if is_string_type(&binary.left.ty().ty) => + { + let result = emit_string_eq(builder, module, a, b)?; + Ok(ValueRepr::Single(result)) + } + (BinaryOp::Neq, ValueRepr::Single(a), ValueRepr::Single(b)) + if is_string_type(&binary.left.ty().ty) => + { + let eq_result = emit_string_eq(builder, module, a, b)?; + // Invert the result: 1 becomes 0, 0 becomes 1 + let one = builder.ins().iconst(ir::types::I8, 1); + let neq_result = builder.ins().bxor(eq_result, one); + Ok(ValueRepr::Single(neq_result)) + } (BinaryOp::Eq, ValueRepr::Single(a), ValueRepr::Single(b)) => { let cmp = builder.ins().icmp(IntCC::Equal, a, b); Ok(ValueRepr::Single(bool_to_i8(builder, cmp))) @@ -1513,17 +1562,6 @@ fn emit_hir_expr_inner( let cmp = builder.ins().icmp(IntCC::NotEqual, a, b); Ok(ValueRepr::Single(bool_to_i8(builder, cmp))) } - (BinaryOp::Eq, ValueRepr::Pair(ptr1, len1), ValueRepr::Pair(ptr2, len2)) => { - let result = emit_string_eq(builder, module, ptr1, len1, ptr2, len2)?; - Ok(ValueRepr::Single(result)) - } - (BinaryOp::Neq, ValueRepr::Pair(ptr1, len1), ValueRepr::Pair(ptr2, len2)) => { - let eq_result = emit_string_eq(builder, module, ptr1, len1, ptr2, len2)?; - // Invert the result: 1 becomes 0, 0 becomes 1 - let one = builder.ins().iconst(ir::types::I8, 1); - let neq_result = builder.ins().bxor(eq_result, one); - Ok(ValueRepr::Single(neq_result)) - } (BinaryOp::Lt, ValueRepr::Single(a), ValueRepr::Single(b)) => { let cmp = builder.ins().icmp(cmp_cc(&binary.left, IntCC::SignedLessThan, IntCC::UnsignedLessThan), a, b); Ok(ValueRepr::Single(bool_to_i8(builder, cmp))) @@ -1743,21 +1781,17 @@ fn trap_on_overflow(builder: &mut FunctionBuilder, overflow: Value) { fn emit_string_eq( builder: &mut FunctionBuilder, module: &mut ObjectModule, - ptr1: Value, - len1: Value, - ptr2: Value, - len2: Value, + lhs: Value, + rhs: Value, ) -> Result { use cranelift_codegen::ir::{AbiParam, Signature}; let ptr_ty = module.isa().pointer_type(); - // Build signature: (ptr, i64, ptr, i64) -> i8 + // Build signature: (ptr, ptr) -> i8 let mut sig = Signature::new(module.isa().default_call_conv()); sig.params.push(AbiParam::new(ptr_ty)); - sig.params.push(AbiParam::new(ir::types::I64)); sig.params.push(AbiParam::new(ptr_ty)); - sig.params.push(AbiParam::new(ir::types::I64)); sig.returns.push(AbiParam::new(ir::types::I8)); // Declare and import the runtime function @@ -1767,11 +1801,15 @@ fn emit_string_eq( let local_func = module.declare_func_in_func(func_id, builder.func); // Call the function - let call_inst = builder.ins().call(local_func, &[ptr1, len1, ptr2, len2]); + let call_inst = builder.ins().call(local_func, &[lhs, rhs]); let results = builder.inst_results(call_inst); Ok(results[0]) } +fn is_string_type(ty: &crate::typeck::Ty) -> bool { + matches!(ty, crate::typeck::Ty::Path(name, _) if name == "sys.string.string" || name == "string") +} + /// Emit an index expression, calling the appropriate runtime function. fn emit_hir_index( builder: &mut FunctionBuilder, @@ -1823,18 +1861,27 @@ fn emit_hir_index( let object_ty = &index_expr.object.ty().ty; match object_ty { - crate::typeck::Ty::Builtin(crate::typeck::BuiltinType::String) => { - // For strings, call capable_rt_string_byte_at(ptr, len, index) -> u8 - let (ptr, len) = match object { - ValueRepr::Pair(p, l) => (p, l), + ty if is_string_type(ty) => { + // For strings, index into the backing Slice. + let base_ptr = match object { + ValueRepr::Single(ptr) => ptr, _ => { return Err(CodegenError::Codegen( - "expected string to be a pointer-length pair".to_string(), + "expected string to be a struct pointer".to_string(), )) } }; - - let result = emit_string_byte_at(builder, module, ptr, len, index_val)?; + let layout = resolve_struct_layout(object_ty, "", &struct_layouts.layouts).ok_or_else( + || CodegenError::Unsupported("string layout missing".to_string()), + )?; + let field = layout.fields.get("bytes").ok_or_else(|| { + CodegenError::Unsupported("string.bytes field missing".to_string()) + })?; + let addr = ptr_add(builder, base_ptr, field.offset); + let handle = builder + .ins() + .load(ir::types::I64, MemFlags::new(), addr, 0); + let result = emit_slice_at(builder, module, handle, index_val)?; Ok(ValueRepr::Single(result)) } crate::typeck::Ty::Path(name, _) if name == "Slice" || name == "sys.buffer.Slice" => { @@ -1872,64 +1919,60 @@ fn emit_hir_index( } } -/// Emit a call to the runtime string byte_at function. +/// Emit a call to the runtime slice at function. /// Returns a u8 value at the given index. -fn emit_string_byte_at( +fn emit_slice_at( builder: &mut FunctionBuilder, module: &mut ObjectModule, - ptr: Value, - len: Value, + handle: Value, index: Value, ) -> Result { use cranelift_codegen::ir::{AbiParam, Signature}; let ptr_ty = module.isa().pointer_type(); - // Build signature: (ptr, i64, i32) -> u8 + // Build signature: (handle, i32) -> u8 let mut sig = Signature::new(module.isa().default_call_conv()); - sig.params.push(AbiParam::new(ptr_ty)); - sig.params.push(AbiParam::new(ir::types::I64)); + sig.params.push(AbiParam::new(ptr_ty)); // Handle is a usize sig.params.push(AbiParam::new(ir::types::I32)); sig.returns.push(AbiParam::new(ir::types::I8)); // Declare and import the runtime function let func_id = module - .declare_function("capable_rt_string_byte_at", Linkage::Import, &sig) + .declare_function("capable_rt_slice_at", Linkage::Import, &sig) .map_err(|err| CodegenError::Codegen(err.to_string()))?; let local_func = module.declare_func_in_func(func_id, builder.func); // Call the function - let call_inst = builder.ins().call(local_func, &[ptr, len, index]); + let call_inst = builder.ins().call(local_func, &[handle, index]); let results = builder.inst_results(call_inst); Ok(results[0]) } -/// Emit a call to the runtime slice at function. -/// Returns a u8 value at the given index. -fn emit_slice_at( +/// Emit a call to the runtime slice_from_ptr helper. +fn emit_slice_from_ptr( builder: &mut FunctionBuilder, module: &mut ObjectModule, - handle: Value, - index: Value, + ptr: Value, + len: Value, ) -> Result { use cranelift_codegen::ir::{AbiParam, Signature}; let ptr_ty = module.isa().pointer_type(); - // Build signature: (handle, i32) -> u8 + // Build signature: (handle, ptr, i32) -> handle let mut sig = Signature::new(module.isa().default_call_conv()); - sig.params.push(AbiParam::new(ptr_ty)); // Handle is a usize + sig.params.push(AbiParam::new(ir::types::I64)); + sig.params.push(AbiParam::new(ptr_ty)); sig.params.push(AbiParam::new(ir::types::I32)); - sig.returns.push(AbiParam::new(ir::types::I8)); + sig.returns.push(AbiParam::new(ir::types::I64)); - // Declare and import the runtime function let func_id = module - .declare_function("capable_rt_slice_at", Linkage::Import, &sig) + .declare_function("capable_rt_slice_from_ptr", Linkage::Import, &sig) .map_err(|err| CodegenError::Codegen(err.to_string()))?; let local_func = module.declare_func_in_func(func_id, builder.func); - - // Call the function - let call_inst = builder.ins().call(local_func, &[handle, index]); + let default_alloc = builder.ins().iconst(ir::types::I64, 0); + let call_inst = builder.ins().call(local_func, &[default_alloc, ptr, len]); let results = builder.inst_results(call_inst); Ok(results[0]) } @@ -2179,17 +2222,6 @@ fn store_value_by_ty( builder.ins().store(MemFlags::new(), val, addr, 0); Ok(()) } - BuiltinType::String => { - let ValueRepr::Pair(ptr, len) = value else { - return Err(CodegenError::Unsupported("store string".to_string())); - }; - let (ptr_off, len_off) = string_offsets(ptr_ty); - let ptr_addr = ptr_add(builder, base_ptr, offset + ptr_off); - let len_addr = ptr_add(builder, base_ptr, offset + len_off); - builder.ins().store(MemFlags::new(), ptr, ptr_addr, 0); - builder.ins().store(MemFlags::new(), len, len_addr, 0); - Ok(()) - } BuiltinType::I64 => Err(CodegenError::Unsupported("i64 not yet supported".to_string())), }, Ty::Ptr(_) => { @@ -2338,18 +2370,6 @@ fn load_value_by_ty( BuiltinType::U8 | BuiltinType::Bool => Ok(ValueRepr::Single( builder.ins().load(ir::types::I8, MemFlags::new(), addr, 0), )), - BuiltinType::String => { - let (ptr_off, len_off) = string_offsets(ptr_ty); - let ptr_addr = ptr_add(builder, base_ptr, offset + ptr_off); - let len_addr = ptr_add(builder, base_ptr, offset + len_off); - let ptr = builder - .ins() - .load(ptr_ty, MemFlags::new(), ptr_addr, 0); - let len = builder - .ins() - .load(ir::types::I64, MemFlags::new(), len_addr, 0); - Ok(ValueRepr::Pair(ptr, len)) - } BuiltinType::I64 => Err(CodegenError::Unsupported("i64 not yet supported".to_string())), }, Ty::Ptr(_) => Ok(ValueRepr::Single( @@ -2478,72 +2498,6 @@ fn aligned_slot_size(size: u32, align: u32) -> u32 { size.max(1).saturating_add(align.saturating_sub(1)) } -fn result_string_slots(builder: &mut FunctionBuilder, ptr_ty: Type) -> ResultStringSlots { - let ptr_align = ptr_ty.bytes() as u32; - let len_bytes = abi_quirks::result_string_len_bytes(); - let len_align = len_bytes; - let err_align = 4u32; - let slot_ptr = builder.create_sized_stack_slot(ir::StackSlotData::new( - ir::StackSlotKind::ExplicitSlot, - aligned_slot_size(ptr_ty.bytes() as u32, ptr_align), - )); - let slot_len = builder.create_sized_stack_slot(ir::StackSlotData::new( - ir::StackSlotKind::ExplicitSlot, - aligned_slot_size(len_bytes, len_align), - )); - let slot_err = builder.create_sized_stack_slot(ir::StackSlotData::new( - ir::StackSlotKind::ExplicitSlot, - aligned_slot_size(4, err_align), - )); - ResultStringSlots { - slot_ptr, - slot_len, - slot_err, - ptr_align, - len_align, - err_align, - } -} - -fn push_result_string_out_params( - builder: &mut FunctionBuilder, - ptr_ty: Type, - slots: &ResultStringSlots, - args: &mut Vec, -) { - let ptr_ptr = aligned_stack_addr(builder, slots.slot_ptr, slots.ptr_align, ptr_ty); - let len_ptr = aligned_stack_addr(builder, slots.slot_len, slots.len_align, ptr_ty); - let err_ptr = aligned_stack_addr(builder, slots.slot_err, slots.err_align, ptr_ty); - args.push(ptr_ptr); - args.push(len_ptr); - args.push(err_ptr); -} - -fn read_result_string_slots( - builder: &mut FunctionBuilder, - ptr_ty: Type, - slots: &ResultStringSlots, -) -> (Value, Value, Value) { - let ptr_addr = aligned_stack_addr(builder, slots.slot_ptr, slots.ptr_align, ptr_ty); - let len_addr = aligned_stack_addr(builder, slots.slot_len, slots.len_align, ptr_ty); - let err_addr = aligned_stack_addr(builder, slots.slot_err, slots.err_align, ptr_ty); - let ptr = builder.ins().load(ptr_ty, MemFlags::new(), ptr_addr, 0); - let len = builder - .ins() - .load(ir::types::I64, MemFlags::new(), len_addr, 0); - let err = builder - .ins() - .load(ir::types::I32, MemFlags::new(), err_addr, 0); - (ptr, len, err) -} - -/// Compute pointer/len offsets for the string layout. -fn string_offsets(ptr_ty: Type) -> (u32, u32) { - let ptr_size = ptr_ty.bytes() as u32; - let len_offset = align_to(ptr_size, 8); - (0, len_offset) -} - /// Compute offsets for Result layout (tag, ok, err). fn result_offsets(ok: TypeLayout, err: TypeLayout) -> (u32, u32, u32) { let tag_offset = 0u32; @@ -2653,9 +2607,6 @@ fn emit_hir_match_stmt( ValueRepr::Single(v) => (v, None), ValueRepr::Result { tag, ok, err } => (tag, Some((*ok, *err))), ValueRepr::Unit => (builder.ins().iconst(ir::types::I32, 0), None), - ValueRepr::Pair(_, _) => { - return Err(CodegenError::Unsupported("match on string".to_string())) - } }; let merge_block = builder.create_block(); @@ -2794,9 +2745,6 @@ fn emit_hir_match_expr( ValueRepr::Single(v) => (v, None), ValueRepr::Result { tag, ok, err } => (tag, Some((*ok, *err))), ValueRepr::Unit => (builder.ins().iconst(ir::types::I32, 0), None), - ValueRepr::Pair(_, _) => { - return Err(CodegenError::Unsupported("match on string".to_string())) - } }; let merge_block = builder.create_block(); @@ -2904,7 +2852,6 @@ fn emit_hir_match_expr( } else { let values = match &arm_value { ValueRepr::Single(val) => vec![*val], - ValueRepr::Pair(a, b) => vec![*a, *b], ValueRepr::Unit => vec![], ValueRepr::Result { .. } => { return Err(CodegenError::Unsupported("match result value".to_string())) @@ -2929,7 +2876,6 @@ fn emit_hir_match_expr( kind: match &arm_value { ValueRepr::Unit => ResultKind::Unit, ValueRepr::Single(_) => ResultKind::Single, - ValueRepr::Pair(_, _) => ResultKind::Pair, _ => ResultKind::Single, }, slots, @@ -2983,7 +2929,6 @@ fn emit_hir_match_expr( let result = match shape.kind { ResultKind::Unit => ValueRepr::Unit, ResultKind::Single => ValueRepr::Single(loaded[0]), - ResultKind::Pair => ValueRepr::Pair(loaded[0], loaded[1]), }; Ok(result) @@ -3111,9 +3056,7 @@ fn to_b1(builder: &mut FunctionBuilder, value: ValueRepr) -> Result Ok(builder.ins().icmp_imm(IntCC::NotEqual, val, 0)), ValueRepr::Unit => Err(CodegenError::Unsupported("unit condition".to_string())), - ValueRepr::Pair(_, _) | ValueRepr::Result { .. } => { - Err(CodegenError::Unsupported("string condition".to_string())) - } + ValueRepr::Result { .. } => Err(CodegenError::Unsupported("result condition".to_string())), } } @@ -3133,6 +3076,7 @@ fn emit_string( value: &str, module: &mut ObjectModule, data_counter: &mut u32, + struct_layouts: &StructLayoutIndex, ) -> Result { let name = format!("__str_{}", data_counter); *data_counter += 1; @@ -3148,8 +3092,29 @@ fn emit_string( let ptr = builder .ins() .global_value(module.isa().pointer_type(), global); - let len = builder.ins().iconst(ir::types::I64, value.len() as i64); - Ok(ValueRepr::Pair(ptr, len)) + let len = builder.ins().iconst(ir::types::I32, value.len() as i64); + let slice_handle = emit_slice_from_ptr(builder, module, ptr, len)?; + + let string_ty = crate::typeck::Ty::Path("sys.string.string".to_string(), Vec::new()); + let layout = resolve_struct_layout(&string_ty, "", &struct_layouts.layouts).ok_or_else(|| { + CodegenError::Unsupported("string layout missing".to_string()) + })?; + let field = layout.fields.get("bytes").ok_or_else(|| { + CodegenError::Unsupported("string.bytes field missing".to_string()) + })?; + let ptr_ty = module.isa().pointer_type(); + let align = layout.align.max(1); + let slot_size = layout.size.max(1).saturating_add(align - 1); + let slot = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + slot_size, + )); + let base_ptr = aligned_stack_addr(builder, slot, align, ptr_ty); + let addr = ptr_add(builder, base_ptr, field.offset); + builder + .ins() + .store(MemFlags::new(), slice_handle, addr, 0); + Ok(ValueRepr::Single(base_ptr)) } /// Flatten a ValueRepr into ABI-ready Cranelift values. @@ -3157,7 +3122,6 @@ pub(super) fn flatten_value(value: &ValueRepr) -> Vec { match value { ValueRepr::Unit => vec![], ValueRepr::Single(val) => vec![*val], - ValueRepr::Pair(a, b) => vec![*a, *b], ValueRepr::Result { tag, ok, err } => { let mut out = vec![*tag]; out.extend(flatten_value(ok)); @@ -3222,11 +3186,6 @@ fn zero_value_for_tykind( AbiType::U8 | AbiType::Bool => Ok(ValueRepr::Single(builder.ins().iconst(ir::types::I8, 0))), AbiType::Handle => Ok(ValueRepr::Single(builder.ins().iconst(ir::types::I64, 0))), AbiType::Ptr => Ok(ValueRepr::Single(builder.ins().iconst(ptr_ty, 0))), - AbiType::String => { - let ptr = builder.ins().iconst(ptr_ty, 0); - let len = builder.ins().iconst(ir::types::I64, 0); - Ok(ValueRepr::Pair(ptr, len)) - } AbiType::Result(ok, err) => { let tag = builder.ins().iconst(ir::types::I8, 0); let ok_val = zero_value_for_tykind(builder, ok, ptr_ty)?; @@ -3240,9 +3199,6 @@ fn zero_value_for_tykind( AbiType::ResultOut(_, _) => Err(CodegenError::Unsupported( abi_quirks::result_out_params_error().to_string(), )), - AbiType::ResultString => Err(CodegenError::Unsupported( - abi_quirks::result_string_params_error().to_string(), - )), } } @@ -3251,7 +3207,8 @@ fn zero_value_for_ty( builder: &mut FunctionBuilder, ty: &crate::hir::HirType, ptr_ty: Type, - _struct_layouts: Option<&StructLayoutIndex>, + struct_layouts: Option<&StructLayoutIndex>, + module: &mut ObjectModule, ) -> Result { use crate::typeck::Ty; @@ -3262,7 +3219,7 @@ fn zero_value_for_ty( ty: *inner.clone(), abi: ty.abi.clone(), }; - zero_value_for_ty(builder, &inner_ty, ptr_ty, _struct_layouts) + zero_value_for_ty(builder, &inner_ty, ptr_ty, struct_layouts, module) } Ty::Param(_) => Err(CodegenError::Unsupported( "generic type parameters must be monomorphized before codegen".to_string(), @@ -3283,14 +3240,42 @@ fn zero_value_for_ty( abi: (**err_abi).clone(), }; let tag = builder.ins().iconst(ir::types::I8, 0); - let ok_val = zero_value_for_ty(builder, &ok_ty, ptr_ty, _struct_layouts)?; - let err_val = zero_value_for_ty(builder, &err_ty, ptr_ty, _struct_layouts)?; + let ok_val = zero_value_for_ty(builder, &ok_ty, ptr_ty, struct_layouts, module)?; + let err_val = zero_value_for_ty(builder, &err_ty, ptr_ty, struct_layouts, module)?; return Ok(ValueRepr::Result { tag, ok: Box::new(ok_val), err: Box::new(err_val), }); } + if let Some(struct_layouts) = struct_layouts { + if let Some(layout) = resolve_struct_layout(&ty.ty, "", &struct_layouts.layouts) { + let align = layout.align.max(1); + let slot_size = layout.size.max(1).saturating_add(align - 1); + let slot = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + slot_size, + )); + let base_ptr = aligned_stack_addr(builder, slot, align, ptr_ty); + for name in &layout.field_order { + let Some(field) = layout.fields.get(name) else { + continue; + }; + let field_zero = + zero_value_for_ty(builder, &field.ty, ptr_ty, Some(struct_layouts), module)?; + store_value_by_ty( + builder, + base_ptr, + field.offset, + &field.ty, + field_zero, + struct_layouts, + module, + )?; + } + return Ok(ValueRepr::Single(base_ptr)); + } + } zero_value_for_tykind(builder, &ty.abi, ptr_ty) } } @@ -3310,12 +3295,6 @@ pub(super) fn value_from_params( *idx += 1; Ok(ValueRepr::Single(val)) } - AbiType::String => { - let ptr = params[*idx]; - let len = params[*idx + 1]; - *idx += 2; - Ok(ValueRepr::Pair(ptr, len)) - } AbiType::Result(ok, err) => { let tag = params[*idx]; *idx += 1; @@ -3327,18 +3306,13 @@ pub(super) fn value_from_params( err: Box::new(err_val), }) } - // ResultOut and ResultString are ABI-level return types, not input types. + // ResultOut is an ABI-level return type, not an input type. // They should never appear as function parameters. AbiType::ResultOut(ok, err) => { Err(CodegenError::Codegen(format!( "ResultOut<{ok:?}, {err:?}> cannot be a parameter type (ABI return type only)" ))) } - AbiType::ResultString => { - Err(CodegenError::Codegen( - "ResultString cannot be a parameter type (ABI return type only)".to_string() - )) - } } } @@ -3358,15 +3332,6 @@ fn value_from_results( *idx += 1; Ok(ValueRepr::Single(*val)) } - AbiType::String => { - if results.len() < *idx + 2 { - return Err(CodegenError::Codegen("string return count".to_string())); - } - let ptr = results[*idx]; - let len = results[*idx + 1]; - *idx += 2; - Ok(ValueRepr::Pair(ptr, len)) - } AbiType::Result(ok, err) => { let tag = results .get(*idx) @@ -3383,9 +3348,6 @@ fn value_from_results( AbiType::ResultOut(_, _) => Err(CodegenError::Unsupported( abi_quirks::result_out_params_error().to_string(), )), - AbiType::ResultString => Err(CodegenError::Unsupported( - abi_quirks::result_string_params_error().to_string(), - )), } } @@ -3395,24 +3357,53 @@ pub(super) fn emit_runtime_wrapper_call( builder: &mut FunctionBuilder, module: &mut ObjectModule, info: &FnInfo, - mut args: Vec, + args: Vec, + ret_ty: &crate::hir::HirType, + struct_layouts: &StructLayoutIndex, ) -> Result { ensure_abi_sig_handled(info)?; let abi_sig = info.abi_sig.as_ref().unwrap_or(&info.sig); - let mut out_slots: Option = None; let mut result_out = None; + let mut sret_ptr = None; + let mut call_args = args; - if abi_quirks::is_result_string(&abi_sig.ret) { + enum ResultOutSlot { + Scalar(ir::StackSlot, ir::Type, u32), + Struct(ir::Value), + } + + if info.sig.ret == AbiType::Ptr + && abi_sig.ret == AbiType::Unit + && is_non_opaque_struct_type(ret_ty, struct_layouts) + { + let layout = resolve_struct_layout(&ret_ty.ty, "", &struct_layouts.layouts).ok_or_else( + || CodegenError::Unsupported("struct layout missing".to_string()), + )?; let ptr_ty = module.isa().pointer_type(); - let slots = result_string_slots(builder, ptr_ty); - push_result_string_out_params(builder, ptr_ty, &slots, &mut args); - out_slots = Some(slots); + let align = layout.align.max(1); + let slot_size = layout.size.max(1).saturating_add(align - 1); + let slot = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + slot_size, + )); + let base_ptr = aligned_stack_addr(builder, slot, align, ptr_ty); + call_args.insert(0, base_ptr); + sret_ptr = Some(base_ptr); } if let AbiType::ResultOut(ok_ty, err_ty) = &abi_sig.ret { let ptr_ty = module.isa().pointer_type(); let ok_slot = if **ok_ty == AbiType::Unit { None + } else if **ok_ty == AbiType::Ptr { + let align = ptr_ty.bytes().max(1) as u32; + let slot = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + aligned_slot_size(ptr_ty.bytes() as u32, align), + )); + let addr = aligned_stack_addr(builder, slot, align, ptr_ty); + call_args.push(addr); + Some(ResultOutSlot::Struct(addr)) } else { let ty = value_type_for_result_out(ok_ty, ptr_ty)?; let align = ty.bytes().max(1) as u32; @@ -3422,11 +3413,20 @@ pub(super) fn emit_runtime_wrapper_call( aligned_slot_size(ty.bytes().max(1) as u32, align), )); let addr = aligned_stack_addr(builder, slot, align, ptr_ty); - args.push(addr); - Some((slot, ty, align)) + call_args.push(addr); + Some(ResultOutSlot::Scalar(slot, ty, align)) }; let err_slot = if **err_ty == AbiType::Unit { None + } else if **err_ty == AbiType::Ptr { + let align = ptr_ty.bytes().max(1) as u32; + let slot = builder.create_sized_stack_slot(ir::StackSlotData::new( + ir::StackSlotKind::ExplicitSlot, + aligned_slot_size(ptr_ty.bytes() as u32, align), + )); + let addr = aligned_stack_addr(builder, slot, align, ptr_ty); + call_args.push(addr); + Some(ResultOutSlot::Struct(addr)) } else { let ty = value_type_for_result_out(err_ty, ptr_ty)?; let align = ty.bytes().max(1) as u32; @@ -3436,8 +3436,8 @@ pub(super) fn emit_runtime_wrapper_call( aligned_slot_size(ty.bytes().max(1) as u32, align), )); let addr = aligned_stack_addr(builder, slot, align, ptr_ty); - args.push(addr); - Some((slot, ty, align)) + call_args.push(addr); + Some(ResultOutSlot::Scalar(slot, ty, align)) }; result_out = Some((ok_slot, err_slot, ok_ty.clone(), err_ty.clone())); } @@ -3455,52 +3455,38 @@ pub(super) fn emit_runtime_wrapper_call( .declare_function(call_symbol, Linkage::Import, &sig) .map_err(|err| CodegenError::Codegen(err.to_string()))?; let local = module.declare_func_in_func(func_id, builder.func); - let call_inst = builder.ins().call(local, &args); + let call_inst = builder.ins().call(local, &call_args); let results = builder.inst_results(call_inst).to_vec(); - if abi_quirks::is_result_string(&abi_sig.ret) { - let tag = results - .get(0) - .ok_or_else(|| CodegenError::Codegen("missing result tag".to_string()))?; - let slots = out_slots.ok_or_else(|| CodegenError::Codegen("missing slots".to_string()))?; - let ptr_ty = module.isa().pointer_type(); - let (ptr, len, err) = read_result_string_slots(builder, ptr_ty, &slots); - match &info.sig.ret { - AbiType::Result(ok_ty, err_ty) => { - if **ok_ty != AbiType::String || **err_ty != AbiType::I32 { - return Err(CodegenError::Unsupported( - abi_quirks::result_out_params_error().to_string(), - )); - } - return Ok(ValueRepr::Result { - tag: *tag, - ok: Box::new(ValueRepr::Pair(ptr, len)), - err: Box::new(ValueRepr::Single(err)), - }); - } - _ => return Err(CodegenError::Unsupported( - abi_quirks::result_out_params_error().to_string(), - )), - } - } - if abi_quirks::is_result_out(&abi_sig.ret) { let tag = results .get(0) .ok_or_else(|| CodegenError::Codegen("missing result tag".to_string()))?; let (ok_slot, err_slot, ok_ty, err_ty) = result_out .ok_or_else(|| CodegenError::Codegen("missing result slots".to_string()))?; - let ok_val = if let Some((slot, ty, align)) = ok_slot { - let addr = aligned_stack_addr(builder, slot, align, module.isa().pointer_type()); - let val = builder.ins().load(ty, MemFlags::new(), addr, 0); - ValueRepr::Single(val) + let ok_val = if let Some(slot) = ok_slot { + match slot { + ResultOutSlot::Scalar(slot, ty, align) => { + let addr = + aligned_stack_addr(builder, slot, align, module.isa().pointer_type()); + let val = builder.ins().load(ty, MemFlags::new(), addr, 0); + ValueRepr::Single(val) + } + ResultOutSlot::Struct(addr) => ValueRepr::Single(addr), + } } else { ValueRepr::Unit }; - let err_val = if let Some((slot, ty, align)) = err_slot { - let addr = aligned_stack_addr(builder, slot, align, module.isa().pointer_type()); - let val = builder.ins().load(ty, MemFlags::new(), addr, 0); - ValueRepr::Single(val) + let err_val = if let Some(slot) = err_slot { + match slot { + ResultOutSlot::Scalar(slot, ty, align) => { + let addr = + aligned_stack_addr(builder, slot, align, module.isa().pointer_type()); + let val = builder.ins().load(ty, MemFlags::new(), addr, 0); + ValueRepr::Single(val) + } + ResultOutSlot::Struct(addr) => ValueRepr::Single(addr), + } } else { ValueRepr::Unit }; @@ -3520,6 +3506,10 @@ pub(super) fn emit_runtime_wrapper_call( } } + if let Some(ptr) = sret_ptr { + return Ok(ValueRepr::Single(ptr)); + } + let mut idx = 0; value_from_results(builder, &info.sig.ret, &results, &mut idx) } @@ -3535,7 +3525,7 @@ fn ensure_abi_sig_handled(info: &FnInfo) -> Result<(), CodegenError> { Ok(()) } else { Err(CodegenError::Codegen(format!( - "abi signature mismatch for {} without ResultString/ResultOut lowering", + "abi signature mismatch for {} without ResultOut lowering", info.symbol ))) } @@ -3552,31 +3542,6 @@ mod tests { assert_eq!(aligned_slot_size(8, 8), 15); } - #[test] - fn result_string_len_is_u64() { - assert_eq!(abi_quirks::result_string_len_bytes(), 8); - } - - #[test] - fn ensure_abi_sig_allows_result_string_lowering() { - let sig = FnSig { - params: Vec::new(), - ret: AbiType::Result(Box::new(AbiType::String), Box::new(AbiType::I32)), - }; - let abi_sig = FnSig { - params: Vec::new(), - ret: AbiType::ResultString, - }; - let info = FnInfo { - sig, - abi_sig: Some(abi_sig), - symbol: "test".to_string(), - runtime_symbol: None, - is_runtime: false, - }; - assert!(ensure_abi_sig_handled(&info).is_ok()); - } - #[test] fn ensure_abi_sig_rejects_unhandled_mismatch() { let sig = FnSig { diff --git a/capc/src/codegen/mod.rs b/capc/src/codegen/mod.rs index b54e6d3..b63f3e9 100644 --- a/capc/src/codegen/mod.rs +++ b/capc/src/codegen/mod.rs @@ -159,7 +159,6 @@ struct TypeLayout { enum ValueRepr { Unit, Single(ir::Value), - Pair(ir::Value, ir::Value), Result { tag: ir::Value, ok: Box, @@ -188,7 +187,6 @@ struct ResultShape { enum ResultKind { Unit, Single, - Pair, } /// Build and write the object file for a fully-checked HIR program. @@ -314,11 +312,18 @@ pub fn build_object( if info.runtime_symbol.is_some() { let args = builder.block_params(block).to_vec(); - let value = emit_runtime_wrapper_call(&mut builder, &mut module, &info, args)?; + let value = emit_runtime_wrapper_call( + &mut builder, + &mut module, + &info, + args, + &func.ret_ty, + &struct_layouts, + )?; match value { ValueRepr::Unit => builder.ins().return_(&[]), ValueRepr::Single(val) => builder.ins().return_(&[val]), - ValueRepr::Pair(_, _) | ValueRepr::Result { .. } => { + ValueRepr::Result { .. } => { let values = flatten_value(&value); builder.ins().return_(&values) } @@ -488,10 +493,6 @@ fn sig_to_clif(sig: &FnSig, ptr_ty: Type, call_conv: CallConv) -> Signature { fn append_ty_params(signature: &mut Signature, ty: &AbiType, ptr_ty: Type) { match ty { AbiType::Unit => {} - AbiType::String => { - signature.params.push(AbiParam::new(ptr_ty)); - signature.params.push(AbiParam::new(ir::types::I64)); - } AbiType::Handle => signature.params.push(AbiParam::new(ir::types::I64)), AbiType::Ptr => signature.params.push(AbiParam::new(ptr_ty)), AbiType::I32 => signature.params.push(AbiParam::new(ir::types::I32)), @@ -511,11 +512,6 @@ fn append_ty_params(signature: &mut Signature, ty: &AbiType, ptr_ty: Type) { signature.params.push(AbiParam::new(ptr_ty)); } } - AbiType::ResultString => { - signature.params.push(AbiParam::new(ptr_ty)); - signature.params.push(AbiParam::new(ptr_ty)); - signature.params.push(AbiParam::new(ptr_ty)); - } } } @@ -529,10 +525,6 @@ fn append_ty_returns(signature: &mut Signature, ty: &AbiType, ptr_ty: Type) { AbiType::Bool => signature.returns.push(AbiParam::new(ir::types::I8)), AbiType::Handle => signature.returns.push(AbiParam::new(ir::types::I64)), AbiType::Ptr => signature.returns.push(AbiParam::new(ptr_ty)), - AbiType::String => { - signature.returns.push(AbiParam::new(ptr_ty)); - signature.returns.push(AbiParam::new(ir::types::I64)); - } AbiType::Result(ok, err) => { signature.returns.push(AbiParam::new(ir::types::I8)); append_ty_returns(signature, ok, ptr_ty); @@ -541,9 +533,6 @@ fn append_ty_returns(signature: &mut Signature, ty: &AbiType, ptr_ty: Type) { AbiType::ResultOut(_, _) => { signature.returns.push(AbiParam::new(ir::types::I8)); } - AbiType::ResultString => { - signature.returns.push(AbiParam::new(ir::types::I8)); - } } } diff --git a/capc/src/typeck/mod.rs b/capc/src/typeck/mod.rs index e402ba2..44a65b6 100644 --- a/capc/src/typeck/mod.rs +++ b/capc/src/typeck/mod.rs @@ -19,8 +19,8 @@ use crate::ast::*; use crate::error::TypeError; use crate::hir::HirModule; -pub(super) const RESERVED_TYPE_PARAMS: [&str; 8] = [ - "i32", "i64", "u32", "u8", "bool", "string", "unit", "never", +pub(super) const RESERVED_TYPE_PARAMS: [&str; 7] = [ + "i32", "i64", "u32", "u8", "bool", "unit", "never", ]; /// Resolved type used after lowering. No spans, fully qualified paths. @@ -88,7 +88,6 @@ pub enum BuiltinType { U32, U8, Bool, - String, Unit, Never, } @@ -382,13 +381,6 @@ fn resolve_method_target( }; let (receiver_name, receiver_args) = match base_ty { Ty::Path(name, args) => (name.as_str(), args), - Ty::Builtin(BuiltinType::String) => { - return Ok(( - "sys.string".to_string(), - "string".to_string(), - Vec::new(), - )); - } Ty::Builtin(BuiltinType::U8) => { return Ok(( "sys.bytes".to_string(), @@ -499,7 +491,6 @@ fn resolve_impl_target( )); } } - Ty::Builtin(BuiltinType::String) => ("sys.string".to_string(), "string".to_string()), Ty::Builtin(BuiltinType::U8) => ("sys.bytes".to_string(), "u8".to_string()), _ => { return Err(TypeError::new( @@ -713,7 +704,6 @@ fn lower_type( "u32" => Some(BuiltinType::U32), "u8" => Some(BuiltinType::U8), "bool" => Some(BuiltinType::Bool), - "string" => Some(BuiltinType::String), "unit" => Some(BuiltinType::Unit), "never" => Some(BuiltinType::Never), _ => None, @@ -739,7 +729,7 @@ fn lower_type( let vec_name = match elem { Ty::Builtin(BuiltinType::U8) => "sys.vec.VecU8", Ty::Builtin(BuiltinType::I32) => "sys.vec.VecI32", - Ty::Builtin(BuiltinType::String) => "sys.vec.VecString", + _ if is_string_ty(elem) => "sys.vec.VecString", _ => { return Err(TypeError::new( "Vec only supports u8, i32, and string element types".to_string(), @@ -763,7 +753,7 @@ fn lower_type( let vec_name = match elem { Ty::Builtin(BuiltinType::U8) => "sys.vec.VecU8", Ty::Builtin(BuiltinType::I32) => "sys.vec.VecI32", - Ty::Builtin(BuiltinType::String) => "sys.vec.VecString", + _ if is_string_ty(elem) => "sys.vec.VecString", _ => { return Err(TypeError::new( "Vec only supports u8, i32, and string element types".to_string(), @@ -822,6 +812,19 @@ fn resolve_type_name(path: &Path, use_map: &UseMap, stdlib: &StdlibIndex) -> Str resolved.join(".") } +pub(super) fn is_string_ty(ty: &Ty) -> bool { + matches!(ty, Ty::Path(name, _) if name == "sys.string.string" || name == "string") +} + +fn stdlib_string_ty(stdlib: &StdlibIndex) -> Ty { + let name = stdlib + .types + .get("string") + .cloned() + .unwrap_or_else(|| "sys.string.string".to_string()); + Ty::Path(name, Vec::new()) +} + fn type_contains_ref(ty: &Type) -> Option { match ty { Type::Ref { span, .. } => Some(*span), diff --git a/capc/tests/typecheck.rs b/capc/tests/typecheck.rs index 97a1057..905ba93 100644 --- a/capc/tests/typecheck.rs +++ b/capc/tests/typecheck.rs @@ -256,7 +256,7 @@ fn typecheck_reserved_type_name_fails() { let module = parse_module(&source).expect("parse module"); let stdlib = load_stdlib().expect("load stdlib"); let err = type_check_program(&module, &stdlib, &[]).expect_err("expected type error"); - assert!(err.to_string().contains("type name `string` is reserved")); + assert!(err.to_string().contains("type name `i32` is reserved")); } #[test] diff --git a/tests/programs/should_fail_reserved_type_name.cap b/tests/programs/should_fail_reserved_type_name.cap index adbbcd0..05d53a7 100644 --- a/tests/programs/should_fail_reserved_type_name.cap +++ b/tests/programs/should_fail_reserved_type_name.cap @@ -1,6 +1,6 @@ module should_fail_reserved_type_name -struct string { } +struct i32 { } pub fn main(rc: RootCap) -> i32 { return 0 From 72e9a9f11fa19df7ffb2337968d5ad46f435811a Mon Sep 17 00:00:00 2001 From: Jordan Mecom Date: Thu, 1 Jan 2026 18:29:52 -0800 Subject: [PATCH 30/30] Make string userland and update ABI/runtime --- capc/src/abi.rs | 3 - capc/src/codegen/intrinsics.rs | 201 ++++--------- capc/src/codegen/layout.rs | 10 +- capc/src/typeck/check.rs | 15 +- capc/src/typeck/lower.rs | 1 - capc/src/typeck/monomorphize.rs | 5 +- runtime/src/lib.rs | 482 ++++++++++---------------------- stdlib/sys/string.cap | 74 ++++- 8 files changed, 280 insertions(+), 511 deletions(-) diff --git a/capc/src/abi.rs b/capc/src/abi.rs index 2cc796e..7495cc9 100644 --- a/capc/src/abi.rs +++ b/capc/src/abi.rs @@ -10,10 +10,7 @@ pub enum AbiType { Bool, Handle, Ptr, - String, Result(Box, Box), - /// ABI-only return lowering for `Result`. - ResultString, /// ABI-only return lowering for `Result` where out params are used. ResultOut(Box, Box), } diff --git a/capc/src/codegen/intrinsics.rs b/capc/src/codegen/intrinsics.rs index 17ec238..12c1c5b 100644 --- a/capc/src/codegen/intrinsics.rs +++ b/capc/src/codegen/intrinsics.rs @@ -21,11 +21,11 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { ret: AbiType::Handle, }; let system_fs_read = FnSig { - params: vec![AbiType::Handle, AbiType::String], + params: vec![AbiType::Handle, AbiType::Ptr], ret: AbiType::Handle, }; let system_filesystem = FnSig { - params: vec![AbiType::Handle, AbiType::String], + params: vec![AbiType::Handle, AbiType::Ptr], ret: AbiType::Handle, }; // Filesystem. @@ -34,47 +34,47 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { ret: AbiType::Handle, }; let fs_subdir = FnSig { - params: vec![AbiType::Handle, AbiType::String], + params: vec![AbiType::Handle, AbiType::Ptr], ret: AbiType::Handle, }; let fs_open_read = FnSig { - params: vec![AbiType::Handle, AbiType::String], + params: vec![AbiType::Handle, AbiType::Ptr], ret: AbiType::Handle, }; let fs_read_to_string = FnSig { - params: vec![AbiType::Handle, AbiType::String], - ret: AbiType::Result(Box::new(AbiType::String), Box::new(AbiType::I32)), + params: vec![AbiType::Handle, AbiType::Ptr], + ret: AbiType::Result(Box::new(AbiType::Ptr), Box::new(AbiType::I32)), }; let fs_read_to_string_abi = FnSig { - params: vec![AbiType::Handle, AbiType::String, AbiType::ResultString], - ret: AbiType::ResultString, + params: vec![AbiType::Handle, AbiType::Ptr, AbiType::ResultOut(Box::new(AbiType::Ptr), Box::new(AbiType::I32))], + ret: AbiType::ResultOut(Box::new(AbiType::Ptr), Box::new(AbiType::I32)), }; let fs_read_bytes = FnSig { - params: vec![AbiType::Handle, AbiType::String], + params: vec![AbiType::Handle, AbiType::Ptr], ret: AbiType::Result(Box::new(AbiType::Handle), Box::new(AbiType::I32)), }; let fs_read_bytes_abi = FnSig { params: vec![ AbiType::Handle, - AbiType::String, + AbiType::Ptr, AbiType::ResultOut(Box::new(AbiType::Handle), Box::new(AbiType::I32)), ], ret: AbiType::ResultOut(Box::new(AbiType::Handle), Box::new(AbiType::I32)), }; let fs_list_dir = FnSig { - params: vec![AbiType::Handle, AbiType::String], + params: vec![AbiType::Handle, AbiType::Ptr], ret: AbiType::Result(Box::new(AbiType::Handle), Box::new(AbiType::I32)), }; let fs_list_dir_abi = FnSig { params: vec![ AbiType::Handle, - AbiType::String, + AbiType::Ptr, AbiType::ResultOut(Box::new(AbiType::Handle), Box::new(AbiType::I32)), ], ret: AbiType::ResultOut(Box::new(AbiType::Handle), Box::new(AbiType::I32)), }; let fs_exists = FnSig { - params: vec![AbiType::Handle, AbiType::String], + params: vec![AbiType::Handle, AbiType::Ptr], ret: AbiType::Bool, }; let fs_readfs_close = FnSig { @@ -91,11 +91,11 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { }; let fs_file_read_to_string = FnSig { params: vec![AbiType::Handle], - ret: AbiType::Result(Box::new(AbiType::String), Box::new(AbiType::I32)), + ret: AbiType::Result(Box::new(AbiType::Ptr), Box::new(AbiType::I32)), }; let fs_file_read_to_string_abi = FnSig { - params: vec![AbiType::Handle, AbiType::ResultString], - ret: AbiType::ResultString, + params: vec![AbiType::Handle, AbiType::ResultOut(Box::new(AbiType::Ptr), Box::new(AbiType::I32))], + ret: AbiType::ResultOut(Box::new(AbiType::Ptr), Box::new(AbiType::I32)), }; let fs_file_read_close = FnSig { params: vec![AbiType::Handle], @@ -113,16 +113,20 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { ret: AbiType::ResultOut(Box::new(AbiType::Handle), Box::new(AbiType::I32)), }; let fs_join = FnSig { - params: vec![AbiType::String, AbiType::String], - ret: AbiType::String, + params: vec![AbiType::Ptr, AbiType::Ptr], + ret: AbiType::Ptr, + }; + let fs_join_abi = FnSig { + params: vec![AbiType::Ptr, AbiType::Ptr, AbiType::Ptr], + ret: AbiType::Unit, }; // Console. let console_println = FnSig { - params: vec![AbiType::Handle, AbiType::String], + params: vec![AbiType::Handle, AbiType::Ptr], ret: AbiType::Unit, }; let console_print = FnSig { - params: vec![AbiType::Handle, AbiType::String], + params: vec![AbiType::Handle, AbiType::Ptr], ret: AbiType::Unit, }; let console_print_i32 = FnSig { @@ -173,26 +177,26 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { }; // Net. let net_listen = FnSig { - params: vec![AbiType::Handle, AbiType::String, AbiType::I32], + params: vec![AbiType::Handle, AbiType::Ptr, AbiType::I32], ret: AbiType::Result(Box::new(AbiType::Handle), Box::new(AbiType::I32)), }; let net_listen_abi = FnSig { params: vec![ AbiType::Handle, - AbiType::String, + AbiType::Ptr, AbiType::I32, AbiType::ResultOut(Box::new(AbiType::Handle), Box::new(AbiType::I32)), ], ret: AbiType::ResultOut(Box::new(AbiType::Handle), Box::new(AbiType::I32)), }; let net_connect = FnSig { - params: vec![AbiType::Handle, AbiType::String, AbiType::I32], + params: vec![AbiType::Handle, AbiType::Ptr, AbiType::I32], ret: AbiType::Result(Box::new(AbiType::Handle), Box::new(AbiType::I32)), }; let net_connect_abi = FnSig { params: vec![ AbiType::Handle, - AbiType::String, + AbiType::Ptr, AbiType::I32, AbiType::ResultOut(Box::new(AbiType::Handle), Box::new(AbiType::I32)), ], @@ -200,28 +204,28 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { }; let net_read_to_string = FnSig { params: vec![AbiType::Handle], - ret: AbiType::Result(Box::new(AbiType::String), Box::new(AbiType::I32)), + ret: AbiType::Result(Box::new(AbiType::Ptr), Box::new(AbiType::I32)), }; let net_read_to_string_abi = FnSig { - params: vec![AbiType::Handle, AbiType::ResultString], - ret: AbiType::ResultString, + params: vec![AbiType::Handle, AbiType::ResultOut(Box::new(AbiType::Ptr), Box::new(AbiType::I32))], + ret: AbiType::ResultOut(Box::new(AbiType::Ptr), Box::new(AbiType::I32)), }; let net_read = FnSig { params: vec![AbiType::Handle, AbiType::I32], - ret: AbiType::Result(Box::new(AbiType::String), Box::new(AbiType::I32)), + ret: AbiType::Result(Box::new(AbiType::Ptr), Box::new(AbiType::I32)), }; let net_read_abi = FnSig { - params: vec![AbiType::Handle, AbiType::I32, AbiType::ResultString], - ret: AbiType::ResultString, + params: vec![AbiType::Handle, AbiType::I32, AbiType::ResultOut(Box::new(AbiType::Ptr), Box::new(AbiType::I32))], + ret: AbiType::ResultOut(Box::new(AbiType::Ptr), Box::new(AbiType::I32)), }; let net_write = FnSig { - params: vec![AbiType::Handle, AbiType::String], + params: vec![AbiType::Handle, AbiType::Ptr], ret: AbiType::Result(Box::new(AbiType::Unit), Box::new(AbiType::I32)), }; let net_write_abi = FnSig { params: vec![ AbiType::Handle, - AbiType::String, + AbiType::Ptr, AbiType::ResultOut(Box::new(AbiType::Unit), Box::new(AbiType::I32)), ], ret: AbiType::ResultOut(Box::new(AbiType::Unit), Box::new(AbiType::I32)), @@ -247,11 +251,11 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { }; let args_at = FnSig { params: vec![AbiType::Handle, AbiType::I32], - ret: AbiType::Result(Box::new(AbiType::String), Box::new(AbiType::I32)), + ret: AbiType::Result(Box::new(AbiType::Ptr), Box::new(AbiType::I32)), }; let args_at_abi = FnSig { - params: vec![AbiType::Handle, AbiType::I32, AbiType::ResultString], - ret: AbiType::ResultString, + params: vec![AbiType::Handle, AbiType::I32, AbiType::ResultOut(Box::new(AbiType::Ptr), Box::new(AbiType::I32))], + ret: AbiType::ResultOut(Box::new(AbiType::Ptr), Box::new(AbiType::I32)), }; // Buffer + slices. let mem_slice_from_ptr = FnSig { @@ -509,20 +513,20 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { }; let vec_string_get = FnSig { params: vec![AbiType::Handle, AbiType::I32], - ret: AbiType::Result(Box::new(AbiType::String), Box::new(AbiType::I32)), + ret: AbiType::Result(Box::new(AbiType::Ptr), Box::new(AbiType::I32)), }; let vec_string_get_abi = FnSig { - params: vec![AbiType::Handle, AbiType::I32, AbiType::ResultString], - ret: AbiType::ResultString, + params: vec![AbiType::Handle, AbiType::I32, AbiType::ResultOut(Box::new(AbiType::Ptr), Box::new(AbiType::I32))], + ret: AbiType::ResultOut(Box::new(AbiType::Ptr), Box::new(AbiType::I32)), }; let vec_string_push = FnSig { - params: vec![AbiType::Handle, AbiType::String], + params: vec![AbiType::Handle, AbiType::Ptr], ret: AbiType::Result(Box::new(AbiType::Unit), Box::new(AbiType::I32)), }; let vec_string_push_abi = FnSig { params: vec![ AbiType::Handle, - AbiType::String, + AbiType::Ptr, AbiType::ResultOut(Box::new(AbiType::Unit), Box::new(AbiType::I32)), ], ret: AbiType::ResultOut(Box::new(AbiType::Unit), Box::new(AbiType::I32)), @@ -541,45 +545,16 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { }; let vec_string_pop = FnSig { params: vec![AbiType::Handle], - ret: AbiType::Result(Box::new(AbiType::String), Box::new(AbiType::I32)), + ret: AbiType::Result(Box::new(AbiType::Ptr), Box::new(AbiType::I32)), }; let vec_string_pop_abi = FnSig { - params: vec![AbiType::Handle, AbiType::ResultString], - ret: AbiType::ResultString, + params: vec![AbiType::Handle, AbiType::ResultOut(Box::new(AbiType::Ptr), Box::new(AbiType::I32))], + ret: AbiType::ResultOut(Box::new(AbiType::Ptr), Box::new(AbiType::I32)), }; let vec_string_free = FnSig { params: vec![AbiType::Handle, AbiType::Handle], ret: AbiType::Unit, }; - // Strings. - let string_len = FnSig { - params: vec![AbiType::String], - ret: AbiType::I32, - }; - let string_byte_at = FnSig { - params: vec![AbiType::String, AbiType::I32], - ret: AbiType::U8, - }; - let string_as_slice = FnSig { - params: vec![AbiType::String], - ret: AbiType::Handle, - }; - let string_from_bytes = FnSig { - params: vec![AbiType::Handle], - ret: AbiType::Result(Box::new(AbiType::String), Box::new(AbiType::I32)), - }; - let string_from_bytes_abi = FnSig { - params: vec![AbiType::Handle, AbiType::ResultString], - ret: AbiType::ResultString, - }; - let string_split = FnSig { - params: vec![AbiType::String], - ret: AbiType::Handle, - }; - let string_lines = FnSig { - params: vec![AbiType::String], - ret: AbiType::Handle, - }; // Vec lengths. let vec_u8_len = FnSig { params: vec![AbiType::Handle], @@ -680,11 +655,11 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { FnInfo { sig: FnSig { params: vec![AbiType::Handle], - ret: AbiType::Result(Box::new(AbiType::String), Box::new(AbiType::I32)), + ret: AbiType::Result(Box::new(AbiType::Ptr), Box::new(AbiType::I32)), }, abi_sig: Some(FnSig { - params: vec![AbiType::Handle, AbiType::ResultString], - ret: AbiType::ResultString, + params: vec![AbiType::Handle, AbiType::ResultOut(Box::new(AbiType::Ptr), Box::new(AbiType::I32))], + ret: AbiType::ResultOut(Box::new(AbiType::Ptr), Box::new(AbiType::I32)), }), symbol: "capable_rt_read_stdin_to_string".to_string(), runtime_symbol: None, @@ -828,7 +803,7 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { "sys.console.Console__assert".to_string(), FnInfo { sig: FnSig { - params: vec![AbiType::Handle, AbiType::Bool, AbiType::String], + params: vec![AbiType::Handle, AbiType::Bool, AbiType::Ptr], ret: AbiType::Unit, }, abi_sig: None, @@ -1083,7 +1058,7 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { "sys.fs.join".to_string(), FnInfo { sig: fs_join, - abi_sig: None, + abi_sig: Some(fs_join_abi), symbol: "capable_rt_fs_join".to_string(), runtime_symbol: None, is_runtime: true, @@ -1571,78 +1546,6 @@ pub fn register_runtime_intrinsics(ptr_ty: Type) -> HashMap { is_runtime: true, }, ); - // === String === - map.insert( - "sys.string.string__len".to_string(), - FnInfo { - sig: string_len, - abi_sig: None, - symbol: "capable_rt_string_len".to_string(), - runtime_symbol: None, - is_runtime: true, - }, - ); - map.insert( - "sys.string.string__byte_at".to_string(), - FnInfo { - sig: string_byte_at, - abi_sig: None, - symbol: "capable_rt_string_byte_at".to_string(), - runtime_symbol: None, - is_runtime: true, - }, - ); - map.insert( - "sys.string.string__as_slice".to_string(), - FnInfo { - sig: string_as_slice.clone(), - abi_sig: None, - symbol: "capable_rt_string_as_slice".to_string(), - runtime_symbol: None, - is_runtime: true, - }, - ); - map.insert( - "sys.string.from_bytes".to_string(), - FnInfo { - sig: string_from_bytes, - abi_sig: Some(string_from_bytes_abi), - symbol: "capable_rt_string_from_bytes".to_string(), - runtime_symbol: None, - is_runtime: true, - }, - ); - map.insert( - "sys.string.string__bytes".to_string(), - FnInfo { - // bytes() is an alias for as_slice(); both map to the same runtime symbol. - sig: string_as_slice, - abi_sig: None, - symbol: "capable_rt_string_as_slice".to_string(), - runtime_symbol: None, - is_runtime: true, - }, - ); - map.insert( - "sys.string.string__split_whitespace".to_string(), - FnInfo { - sig: string_split, - abi_sig: None, - symbol: "capable_rt_string_split_whitespace".to_string(), - runtime_symbol: None, - is_runtime: true, - }, - ); - map.insert( - "sys.string.string__lines".to_string(), - FnInfo { - sig: string_lines, - abi_sig: None, - symbol: "capable_rt_string_split_lines".to_string(), - runtime_symbol: None, - is_runtime: true, - }, - ); // === Bytes === map.insert( "sys.bytes.u8__is_whitespace".to_string(), diff --git a/capc/src/codegen/layout.rs b/capc/src/codegen/layout.rs index 3fcc8ef..5cafc23 100644 --- a/capc/src/codegen/layout.rs +++ b/capc/src/codegen/layout.rs @@ -242,14 +242,6 @@ pub(super) fn type_layout_for_abi( size: ptr_ty.bytes() as u32, align: ptr_ty.bytes() as u32, }), - AbiType::String => { - let ptr_size = ptr_ty.bytes() as u32; - let len_offset = align_to(ptr_size, 8); - Ok(TypeLayout { - size: len_offset + 8, - align: ptr_ty.bytes().max(8) as u32, - }) - } AbiType::Result(ok, err) => { let tag = TypeLayout { size: 1, align: 1 }; let ok = type_layout_for_abi(ok, ptr_ty)?; @@ -266,7 +258,7 @@ pub(super) fn type_layout_for_abi( let size = align_to(offset, align); Ok(TypeLayout { size, align }) } - AbiType::ResultOut(_, _) | AbiType::ResultString => Err(CodegenError::Unsupported( + AbiType::ResultOut(_, _) => Err(CodegenError::Unsupported( abi_quirks::result_lowering_layout_error().to_string(), )), } diff --git a/capc/src/typeck/check.rs b/capc/src/typeck/check.rs index 1f918aa..2ee65b0 100644 --- a/capc/src/typeck/check.rs +++ b/capc/src/typeck/check.rs @@ -4,10 +4,11 @@ use crate::ast::*; use crate::error::TypeError; use super::{ - build_type_params, is_affine_type, is_numeric_type, is_orderable_type, lower_type, - resolve_enum_variant, resolve_method_target, resolve_path, resolve_type_name, type_contains_ref, - type_kind, validate_type_args, BuiltinType, EnumInfo, FunctionSig, MoveState, Scopes, - SpanExt, StdlibIndex, StructInfo, Ty, TypeKind, TypeTable, UseMap, UseMode, + build_type_params, is_affine_type, is_numeric_type, is_orderable_type, is_string_ty, + lower_type, resolve_enum_variant, resolve_method_target, resolve_path, resolve_type_name, + stdlib_string_ty, type_contains_ref, type_kind, validate_type_args, BuiltinType, EnumInfo, + FunctionSig, MoveState, Scopes, SpanExt, StdlibIndex, StructInfo, Ty, TypeKind, TypeTable, + UseMap, UseMode, }; /// Optional recorder for expression types during checking. @@ -1040,7 +1041,7 @@ pub(super) fn check_expr( Expr::Literal(lit) => match &lit.value { Literal::Int(_) => Ok(Ty::Builtin(BuiltinType::I32)), Literal::U8(_) => Ok(Ty::Builtin(BuiltinType::U8)), - Literal::String(_) => Ok(Ty::Builtin(BuiltinType::String)), + Literal::String(_) => Ok(stdlib_string_ty(stdlib)), Literal::Bool(_) => Ok(Ty::Builtin(BuiltinType::Bool)), Literal::Unit => Ok(Ty::Builtin(BuiltinType::Unit)), }, @@ -1951,7 +1952,7 @@ pub(super) fn check_expr( // Determine element type based on object type match &object_ty { - Ty::Builtin(BuiltinType::String) => Ok(Ty::Builtin(BuiltinType::U8)), + ty if is_string_ty(ty) => Ok(Ty::Builtin(BuiltinType::U8)), Ty::Path(name, args) if name == "Slice" || name == "sys.buffer.Slice" => { if args.len() != 1 { return Err(TypeError::new( @@ -1987,7 +1988,7 @@ pub(super) fn check_expr( Ok(Ty::Path( "sys.result.Result".to_string(), vec![ - Ty::Builtin(BuiltinType::String), + stdlib_string_ty(stdlib), Ty::Path("sys.vec.VecErr".to_string(), vec![]), ], )) diff --git a/capc/src/typeck/lower.rs b/capc/src/typeck/lower.rs index eaa568f..77d0b4b 100644 --- a/capc/src/typeck/lower.rs +++ b/capc/src/typeck/lower.rs @@ -640,7 +640,6 @@ fn abi_type_for(ty: &Ty, ctx: &LoweringCtx, span: Span) -> Result Ok(AbiType::U32), BuiltinType::U8 => Ok(AbiType::U8), BuiltinType::Bool => Ok(AbiType::Bool), - BuiltinType::String => Ok(AbiType::String), BuiltinType::Unit => Ok(AbiType::Unit), BuiltinType::Never => Ok(AbiType::Unit), }, diff --git a/capc/src/typeck/monomorphize.rs b/capc/src/typeck/monomorphize.rs index fdb7b34..f8d2a1a 100644 --- a/capc/src/typeck/monomorphize.rs +++ b/capc/src/typeck/monomorphize.rs @@ -859,7 +859,6 @@ impl MonoCtx { BuiltinType::U32 => Ok(AbiType::U32), BuiltinType::U8 => Ok(AbiType::U8), BuiltinType::Bool => Ok(AbiType::Bool), - BuiltinType::String => Ok(AbiType::String), BuiltinType::Unit => Ok(AbiType::Unit), BuiltinType::Never => Ok(AbiType::Unit), }, @@ -1068,7 +1067,6 @@ fn mangle_type(ty: &Ty) -> String { crate::typeck::BuiltinType::U32 => "u32".to_string(), crate::typeck::BuiltinType::U8 => "u8".to_string(), crate::typeck::BuiltinType::Bool => "bool".to_string(), - crate::typeck::BuiltinType::String => "string".to_string(), crate::typeck::BuiltinType::Unit => "unit".to_string(), crate::typeck::BuiltinType::Never => "never".to_string(), }, @@ -1076,6 +1074,9 @@ fn mangle_type(ty: &Ty) -> String { Ty::Ref(inner) => format!("ref_{}", mangle_type(inner)), Ty::Param(name) => format!("param_{name}"), Ty::Path(name, args) => { + if name == "sys.string.string" || name == "string" { + return "string".to_string(); + } let mut base = name.replace('.', "_"); if !args.is_empty() { let suffix = args diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 29d4bfe..f9c024e 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -67,9 +67,8 @@ struct FileReadState { } #[repr(C)] -pub struct RawString { - ptr: *const u8, - len: u64, +pub struct CapString { + bytes: Handle, } #[derive(Copy, Clone, Debug)] @@ -133,11 +132,21 @@ fn with_table( f(&mut table) } -fn to_raw_string(value: String) -> RawString { +fn to_slice_handle(value: String) -> Handle { let bytes = value.into_bytes().into_boxed_slice(); - let len = bytes.len() as u64; - let ptr = Box::into_raw(bytes) as *const u8; - RawString { ptr, len } + let len = bytes.len(); + let ptr = Box::into_raw(bytes) as *mut u8; + let handle = new_handle(); + with_table(&SLICES, "slice table", |table| { + table.insert( + handle, + SliceState { + ptr: ptr as usize, + len, + }, + ); + }); + handle } fn write_handle_result( @@ -249,13 +258,12 @@ pub extern "C" fn capable_rt_mint_net(_sys: Handle) -> Handle { #[no_mangle] pub extern "C" fn capable_rt_mint_readfs( _sys: Handle, - root_ptr: *const u8, - root_len: usize, + root: *const CapString, ) -> Handle { if !has_handle(&ROOT_CAPS, _sys, "root cap table") { return 0; } - let root = unsafe { read_str(root_ptr, root_len) }; + let root = unsafe { read_cap_string(root) }; let root_path = match root { Some(path) => normalize_root(Path::new(&path)), None => normalize_root(Path::new("")), @@ -271,13 +279,12 @@ pub extern "C" fn capable_rt_mint_readfs( #[no_mangle] pub extern "C" fn capable_rt_mint_filesystem( _sys: Handle, - root_ptr: *const u8, - root_len: usize, + root: *const CapString, ) -> Handle { if !has_handle(&ROOT_CAPS, _sys, "root cap table") { return 0; } - let root = unsafe { read_str(root_ptr, root_len) }; + let root = unsafe { read_cap_string(root) }; let root_path = match root { Some(path) => normalize_root(Path::new(&path)), None => normalize_root(Path::new("")), @@ -322,10 +329,9 @@ pub extern "C" fn capable_rt_fs_filesystem_close(fs: Handle) { #[no_mangle] pub extern "C" fn capable_rt_fs_subdir( dir: Handle, - name_ptr: *const u8, - name_len: usize, + name: *const CapString, ) -> Handle { - let name = unsafe { read_str(name_ptr, name_len) }; + let name = unsafe { read_cap_string(name) }; let state = take_handle(&DIRS, dir, "dir table"); let (Some(state), Some(name)) = (state, name) else { return 0; @@ -353,10 +359,9 @@ pub extern "C" fn capable_rt_fs_subdir( #[no_mangle] pub extern "C" fn capable_rt_fs_open_read( dir: Handle, - name_ptr: *const u8, - name_len: usize, + name: *const CapString, ) -> Handle { - let name = unsafe { read_str(name_ptr, name_len) }; + let name = unsafe { read_cap_string(name) }; let state = take_handle(&DIRS, dir, "dir table"); let (Some(state), Some(name)) = (state, name) else { return 0; @@ -389,10 +394,9 @@ pub extern "C" fn capable_rt_fs_dir_close(dir: Handle) { #[no_mangle] pub extern "C" fn capable_rt_fs_exists( fs: Handle, - path_ptr: *const u8, - path_len: usize, + path: *const CapString, ) -> u8 { - let path = unsafe { read_str(path_ptr, path_len) }; + let path = unsafe { read_cap_string(path) }; let state = take_handle(&READ_FS, fs, "readfs table"); let (Some(state), Some(path)) = (state, path) else { return 0; @@ -410,12 +414,11 @@ pub extern "C" fn capable_rt_fs_exists( #[no_mangle] pub extern "C" fn capable_rt_fs_read_bytes( fs: Handle, - path_ptr: *const u8, - path_len: usize, + path: *const CapString, out_ok: *mut Handle, out_err: *mut i32, ) -> u8 { - let path = unsafe { read_str(path_ptr, path_len) }; + let path = unsafe { read_cap_string(path) }; let state = take_handle(&READ_FS, fs, "readfs table"); let (Some(state), Some(path)) = (state, path) else { return write_handle_result(out_ok, out_err, Err(FsErr::PermissionDenied)); @@ -442,12 +445,11 @@ pub extern "C" fn capable_rt_fs_read_bytes( #[no_mangle] pub extern "C" fn capable_rt_fs_list_dir( fs: Handle, - path_ptr: *const u8, - path_len: usize, + path: *const CapString, out_ok: *mut Handle, out_err: *mut i32, ) -> u8 { - let path = unsafe { read_str(path_ptr, path_len) }; + let path = unsafe { read_cap_string(path) }; let state = take_handle(&READ_FS, fs, "readfs table"); let (Some(state), Some(path)) = (state, path) else { return write_handle_result(out_ok, out_err, Err(FsErr::PermissionDenied)); @@ -481,10 +483,9 @@ pub extern "C" fn capable_rt_fs_list_dir( #[no_mangle] pub extern "C" fn capable_rt_fs_dir_exists( dir: Handle, - name_ptr: *const u8, - name_len: usize, + name: *const CapString, ) -> u8 { - let name = unsafe { read_str(name_ptr, name_len) }; + let name = unsafe { read_cap_string(name) }; let state = take_handle(&DIRS, dir, "dir table"); let (Some(state), Some(name)) = (state, name) else { return 0; @@ -502,12 +503,11 @@ pub extern "C" fn capable_rt_fs_dir_exists( #[no_mangle] pub extern "C" fn capable_rt_fs_dir_read_bytes( dir: Handle, - name_ptr: *const u8, - name_len: usize, + name: *const CapString, out_ok: *mut Handle, out_err: *mut i32, ) -> u8 { - let name = unsafe { read_str(name_ptr, name_len) }; + let name = unsafe { read_cap_string(name) }; let state = take_handle(&DIRS, dir, "dir table"); let (Some(state), Some(name)) = (state, name) else { return write_handle_result(out_ok, out_err, Err(FsErr::PermissionDenied)); @@ -567,16 +567,25 @@ pub extern "C" fn capable_rt_fs_dir_list_dir( #[no_mangle] pub extern "C" fn capable_rt_fs_join( - a_ptr: *const u8, - a_len: usize, - b_ptr: *const u8, - b_len: usize, -) -> RawString { - let (Some(a), Some(b)) = (unsafe { read_str(a_ptr, a_len) }, unsafe { read_str(b_ptr, b_len) }) else { - return RawString { ptr: std::ptr::null(), len: 0 }; + out: *mut CapString, + a: *const CapString, + b: *const CapString, +) { + let (Some(a), Some(b)) = (unsafe { read_cap_string(a) }, unsafe { read_cap_string(b) }) else { + unsafe { + if !out.is_null() { + (*out).bytes = 0; + } + } + return; }; let joined = Path::new(&a).join(&b); - to_raw_string(joined.to_string_lossy().to_string()) + let handle = to_slice_handle(joined.to_string_lossy().to_string()); + unsafe { + if !out.is_null() { + (*out).bytes = handle; + } + } } #[no_mangle] @@ -591,19 +600,27 @@ pub extern "C" fn capable_rt_assert(_sys: Handle, cond: u8) { } #[no_mangle] -pub extern "C" fn capable_rt_console_print(_console: Handle, ptr: *const u8, len: usize) { +pub extern "C" fn capable_rt_console_print(_console: Handle, s: *const CapString) { if !has_handle(&CONSOLES, _console, "console table") { return; } - unsafe { write_bytes(ptr, len, false) }; + let handle = unsafe { if s.is_null() { 0 } else { (*s).bytes } }; + let Some(state) = slice_state(handle) else { + return; + }; + unsafe { write_bytes(state.ptr as *const u8, state.len, false) }; } #[no_mangle] -pub extern "C" fn capable_rt_console_println(_console: Handle, ptr: *const u8, len: usize) { +pub extern "C" fn capable_rt_console_println(_console: Handle, s: *const CapString) { if !has_handle(&CONSOLES, _console, "console table") { return; } - unsafe { write_bytes(ptr, len, true) }; + let handle = unsafe { if s.is_null() { 0 } else { (*s).bytes } }; + let Some(state) = slice_state(handle) else { + return; + }; + unsafe { write_bytes(state.ptr as *const u8, state.len, true) }; } #[no_mangle] @@ -674,37 +691,35 @@ pub extern "C" fn capable_rt_math_mul_wrap_u8(a: u8, b: u8) -> u8 { #[no_mangle] pub extern "C" fn capable_rt_fs_read_to_string( fs: Handle, - path_ptr: *const u8, - path_len: usize, - out_ptr: *mut *const u8, - out_len: *mut u64, + path: *const CapString, + out_ok: *mut CapString, out_err: *mut i32, ) -> u8 { - let path = unsafe { read_str(path_ptr, path_len) }; + let path = unsafe { read_cap_string(path) }; let state = take_handle(&READ_FS, fs, "readfs table"); let Some(state) = state else { - return write_result(out_ptr, out_len, out_err, Err(FsErr::PermissionDenied)); + return write_result(out_ok, out_err, Err(FsErr::PermissionDenied)); }; let Some(path) = path else { - return write_result(out_ptr, out_len, out_err, Err(FsErr::InvalidPath)); + return write_result(out_ok, out_err, Err(FsErr::InvalidPath)); }; let relative = match normalize_relative(Path::new(&path)) { Some(path) => path, - None => return write_result(out_ptr, out_len, out_err, Err(FsErr::InvalidPath)), + None => return write_result(out_ok, out_err, Err(FsErr::InvalidPath)), }; let full = state.root.join(relative); let full = match full.canonicalize() { Ok(path) => path, - Err(err) => return write_result(out_ptr, out_len, out_err, Err(map_fs_err(err))), + Err(err) => return write_result(out_ok, out_err, Err(map_fs_err(err))), }; if !full.starts_with(&state.root) { - return write_result(out_ptr, out_len, out_err, Err(FsErr::InvalidPath)); + return write_result(out_ok, out_err, Err(FsErr::InvalidPath)); } match std::fs::read_to_string(&full) { - Ok(contents) => write_result(out_ptr, out_len, out_err, Ok(contents)), - Err(err) => write_result(out_ptr, out_len, out_err, Err(map_fs_err(err))), + Ok(contents) => write_result(out_ok, out_err, Ok(contents)), + Err(err) => write_result(out_ok, out_err, Err(map_fs_err(err))), } } @@ -716,27 +731,26 @@ pub extern "C" fn capable_rt_fs_readfs_close(fs: Handle) { #[no_mangle] pub extern "C" fn capable_rt_fs_file_read_to_string( file: Handle, - out_ptr: *mut *const u8, - out_len: *mut u64, + out_ok: *mut CapString, out_err: *mut i32, ) -> u8 { let state = take_handle(&FILE_READS, file, "file read table"); let Some(state) = state else { - return write_result(out_ptr, out_len, out_err, Err(FsErr::PermissionDenied)); + return write_result(out_ok, out_err, Err(FsErr::PermissionDenied)); }; let full = state.root.join(state.rel); let full = match full.canonicalize() { Ok(path) => path, - Err(err) => return write_result(out_ptr, out_len, out_err, Err(map_fs_err(err))), + Err(err) => return write_result(out_ok, out_err, Err(map_fs_err(err))), }; if !full.starts_with(&state.root) { - return write_result(out_ptr, out_len, out_err, Err(FsErr::InvalidPath)); + return write_result(out_ok, out_err, Err(FsErr::InvalidPath)); } match std::fs::read_to_string(&full) { - Ok(contents) => write_result(out_ptr, out_len, out_err, Ok(contents)), - Err(err) => write_result(out_ptr, out_len, out_err, Err(map_fs_err(err))), + Ok(contents) => write_result(out_ok, out_err, Ok(contents)), + Err(err) => write_result(out_ok, out_err, Err(map_fs_err(err))), } } @@ -748,8 +762,7 @@ pub extern "C" fn capable_rt_fs_file_read_close(file: Handle) { #[no_mangle] pub extern "C" fn capable_rt_net_connect( net: Handle, - host_ptr: *const u8, - host_len: usize, + host: *const CapString, port: i32, out_ok: *mut Handle, out_err: *mut i32, @@ -757,7 +770,7 @@ pub extern "C" fn capable_rt_net_connect( if !has_handle(&NET_CAPS, net, "net table") { return write_handle_result_code(out_ok, out_err, Err(NetErr::IoError as i32)); } - let host = unsafe { read_str(host_ptr, host_len) }; + let host = unsafe { read_cap_string(host) }; let Some(host) = host else { return write_handle_result_code(out_ok, out_err, Err(NetErr::InvalidAddress as i32)); }; @@ -777,8 +790,7 @@ pub extern "C" fn capable_rt_net_connect( #[no_mangle] pub extern "C" fn capable_rt_net_listen( net: Handle, - host_ptr: *const u8, - host_len: usize, + host: *const CapString, port: i32, out_ok: *mut Handle, out_err: *mut i32, @@ -786,7 +798,7 @@ pub extern "C" fn capable_rt_net_listen( if !has_handle(&NET_CAPS, net, "net table") { return write_handle_result_code(out_ok, out_err, Err(NetErr::IoError as i32)); } - let host = unsafe { read_str(host_ptr, host_len) }; + let host = unsafe { read_cap_string(host) }; let Some(host) = host else { return write_handle_result_code(out_ok, out_err, Err(NetErr::InvalidAddress as i32)); }; @@ -826,8 +838,7 @@ pub extern "C" fn capable_rt_net_accept( #[no_mangle] pub extern "C" fn capable_rt_net_read_to_string( conn: Handle, - out_ptr: *mut *const u8, - out_len: *mut u64, + out_ok: *mut CapString, out_err: *mut i32, ) -> u8 { let result = with_table(&TCP_CONNS, "tcp conn table", |table| { @@ -840,20 +851,14 @@ pub extern "C" fn capable_rt_net_read_to_string( Err(err) => Err(map_net_err(err)), } }); - write_string_result( - out_ptr, - out_len, - out_err, - result.map_err(|err| err as i32), - ) + write_string_result(out_ok, out_err, result.map_err(|err| err as i32)) } #[no_mangle] pub extern "C" fn capable_rt_net_read( conn: Handle, max_size: i32, - out_ptr: *mut *const u8, - out_len: *mut u64, + out_ok: *mut CapString, out_err: *mut i32, ) -> u8 { let result = with_table(&TCP_CONNS, "tcp conn table", |table| { @@ -873,22 +878,16 @@ pub extern "C" fn capable_rt_net_read( Err(err) => Err(map_net_err(err)), } }); - write_string_result( - out_ptr, - out_len, - out_err, - result.map_err(|err| err as i32), - ) + write_string_result(out_ok, out_err, result.map_err(|err| err as i32)) } #[no_mangle] pub extern "C" fn capable_rt_net_write( conn: Handle, - data_ptr: *const u8, - data_len: usize, + data: *const CapString, out_err: *mut i32, ) -> u8 { - let data = unsafe { read_str(data_ptr, data_len) }; + let data = unsafe { read_cap_string(data) }; let Some(data) = data else { unsafe { if !out_err.is_null() { @@ -1810,35 +1809,33 @@ pub extern "C" fn capable_rt_vec_string_len(vec: Handle) -> i32 { pub extern "C" fn capable_rt_vec_string_get( vec: Handle, index: i32, - out_ptr: *mut *const u8, - out_len: *mut u64, + out_ok: *mut CapString, out_err: *mut i32, ) -> u8 { let idx = match usize::try_from(index) { Ok(idx) => idx, Err(_) => { - return write_string_result(out_ptr, out_len, out_err, Err(VecErr::OutOfRange as i32)); + return write_string_result(out_ok, out_err, Err(VecErr::OutOfRange as i32)); } }; with_table(&VECS_STRING, "vec string table", |table| { let Some(data) = table.get(&vec) else { - return write_string_result(out_ptr, out_len, out_err, Err(VecErr::OutOfRange as i32)); + return write_string_result(out_ok, out_err, Err(VecErr::OutOfRange as i32)); }; let Some(value) = data.get(idx) else { - return write_string_result(out_ptr, out_len, out_err, Err(VecErr::OutOfRange as i32)); + return write_string_result(out_ok, out_err, Err(VecErr::OutOfRange as i32)); }; - write_string_result(out_ptr, out_len, out_err, Ok(value.clone())) + write_string_result(out_ok, out_err, Ok(value.clone())) }) } #[no_mangle] pub extern "C" fn capable_rt_vec_string_push( vec: Handle, - ptr: *const u8, - len: usize, + value: *const CapString, out_err: *mut i32, ) -> u8 { - let value = unsafe { read_str(ptr, len) }; + let value = unsafe { read_cap_string(value) }; let Some(value) = value else { unsafe { if !out_err.is_null() { @@ -1908,18 +1905,17 @@ pub extern "C" fn capable_rt_vec_string_extend( #[no_mangle] pub extern "C" fn capable_rt_vec_string_pop( vec: Handle, - out_ptr: *mut *const u8, - out_len: *mut u64, + out_ok: *mut CapString, out_err: *mut i32, ) -> u8 { with_table(&VECS_STRING, "vec string table", |table| { let Some(data) = table.get_mut(&vec) else { - return write_string_result(out_ptr, out_len, out_err, Err(VecErr::Empty as i32)); + return write_string_result(out_ok, out_err, Err(VecErr::Empty as i32)); }; let Some(value) = data.pop() else { - return write_string_result(out_ptr, out_len, out_err, Err(VecErr::Empty as i32)); + return write_string_result(out_ok, out_err, Err(VecErr::Empty as i32)); }; - write_string_result(out_ptr, out_len, out_err, Ok(value)) + write_string_result(out_ok, out_err, Ok(value)) }) } @@ -1930,104 +1926,6 @@ pub extern "C" fn capable_rt_vec_string_free(_alloc: Handle, vec: Handle) { }); } -#[no_mangle] -pub extern "C" fn capable_rt_string_split_whitespace( - ptr: *const u8, - len: usize, -) -> Handle { - let value = unsafe { read_str(ptr, len) }; - let mut vec = Vec::new(); - if let Some(value) = value { - vec.extend(value.split_whitespace().map(|s| s.to_string())); - } - let handle = new_handle(); - with_table(&VECS_STRING, "vec string table", |table| { - table.insert(handle, vec); - }); - handle -} - -#[no_mangle] -pub extern "C" fn capable_rt_string_split( - ptr: *const u8, - len: usize, - delim: u8, -) -> Handle { - let value = unsafe { read_str(ptr, len) }; - let mut vec = Vec::new(); - if let Some(value) = value { - let bytes = value.as_bytes(); - let mut start = 0usize; - for (idx, byte) in bytes.iter().enumerate() { - if *byte == delim { - let part = &value[start..idx]; - vec.push(part.to_string()); - start = idx + 1; - } - } - let part = &value[start..]; - vec.push(part.to_string()); - } - let handle = new_handle(); - with_table(&VECS_STRING, "vec string table", |table| { - table.insert(handle, vec); - }); - handle -} - -#[no_mangle] -pub extern "C" fn capable_rt_string_split_lines(ptr: *const u8, len: usize) -> Handle { - let value = unsafe { read_str(ptr, len) }; - let mut vec = Vec::new(); - if let Some(value) = value { - for line in value.split('\n') { - let line = line.strip_suffix('\r').unwrap_or(line); - vec.push(line.to_string()); - } - } - let handle = new_handle(); - with_table(&VECS_STRING, "vec string table", |table| { - table.insert(handle, vec); - }); - handle -} - -#[no_mangle] -pub extern "C" fn capable_rt_string_trim(ptr: *const u8, len: usize) -> RawString { - let value = unsafe { read_str(ptr, len) }; - let trimmed = value.as_deref().unwrap_or("").trim().to_string(); - to_raw_string(trimmed) -} - -#[no_mangle] -pub extern "C" fn capable_rt_string_trim_start(ptr: *const u8, len: usize) -> RawString { - let value = unsafe { read_str(ptr, len) }; - let trimmed = value.as_deref().unwrap_or("").trim_start().to_string(); - to_raw_string(trimmed) -} - -#[no_mangle] -pub extern "C" fn capable_rt_string_trim_end(ptr: *const u8, len: usize) -> RawString { - let value = unsafe { read_str(ptr, len) }; - let trimmed = value.as_deref().unwrap_or("").trim_end().to_string(); - to_raw_string(trimmed) -} - -#[no_mangle] -pub extern "C" fn capable_rt_string_starts_with( - ptr: *const u8, - len: usize, - prefix_ptr: *const u8, - prefix_len: usize, -) -> u8 { - let value = unsafe { read_str(ptr, len) }; - let prefix = unsafe { read_str(prefix_ptr, prefix_len) }; - match (value, prefix) { - (Some(value), Some(prefix)) => u8::from(value.starts_with(&prefix)), - _ => 0, - } -} - #[no_mangle] pub extern "C" fn capable_rt_args_len(_sys: Handle) -> i32 { if !has_handle(&ARGS_CAPS, _sys, "args table") { @@ -2040,8 +1938,7 @@ pub extern "C" fn capable_rt_args_len(_sys: Handle) -> i32 { pub extern "C" fn capable_rt_args_at( _sys: Handle, index: i32, - out_ptr: *mut *const u8, - out_len: *mut u64, + out_ok: *mut CapString, out_err: *mut i32, ) -> u8 { if !has_handle(&ARGS_CAPS, _sys, "args table") { @@ -2071,112 +1968,52 @@ pub extern "C" fn capable_rt_args_at( } return 1; }; - unsafe { - if !out_ptr.is_null() { - *out_ptr = arg.as_ptr(); - } - if !out_len.is_null() { - *out_len = arg.len() as u64; - } - } - 0 + write_string_result(out_ok, out_err, Ok(arg.clone())) } #[no_mangle] pub extern "C" fn capable_rt_read_stdin_to_string( _sys: Handle, - out_ptr: *mut *const u8, - out_len: *mut u64, + out_ok: *mut CapString, out_err: *mut i32, ) -> u8 { if !has_handle(&STDIN_CAPS, _sys, "stdin table") { - return write_string_result(out_ptr, out_len, out_err, Err(0)); + return write_string_result(out_ok, out_err, Err(0)); } let mut input = String::new(); let result = io::stdin().read_to_string(&mut input); match result { - Ok(_) => write_string_result(out_ptr, out_len, out_err, Ok(input)), - Err(_) => write_string_result(out_ptr, out_len, out_err, Err(0)), + Ok(_) => write_string_result(out_ok, out_err, Ok(input)), + Err(_) => write_string_result(out_ok, out_err, Err(0)), } } #[no_mangle] pub extern "C" fn capable_rt_string_eq( - ptr1: *const u8, - len1: usize, - ptr2: *const u8, - len2: usize, + left: *const CapString, + right: *const CapString, ) -> i8 { - if len1 != len2 { - return 0; - } - if len1 == 0 { - return 1; // Both empty strings are equal - } - if ptr1.is_null() || ptr2.is_null() { + let left_handle = unsafe { if left.is_null() { 0 } else { (*left).bytes } }; + let right_handle = unsafe { if right.is_null() { 0 } else { (*right).bytes } }; + let left_state = slice_state(left_handle); + let right_state = slice_state(right_handle); + let (Some(left_state), Some(right_state)) = (left_state, right_state) else { return 0; - } - let s1 = unsafe { std::slice::from_raw_parts(ptr1, len1) }; - let s2 = unsafe { std::slice::from_raw_parts(ptr2, len2) }; - if s1 == s2 { 1 } else { 0 } -} - -#[no_mangle] -pub extern "C" fn capable_rt_string_len(ptr: *const u8, len: usize) -> i32 { - let _ = ptr; - len.min(i32::MAX as usize) as i32 -} - -#[no_mangle] -pub extern "C" fn capable_rt_string_byte_at( - ptr: *const u8, - len: usize, - index: i32, -) -> u8 { - let idx = match usize::try_from(index) { - Ok(idx) => idx, - Err(_) => return 0, }; - if ptr.is_null() || idx >= len { + if left_state.len != right_state.len { return 0; } - unsafe { *ptr.add(idx) } -} - -#[no_mangle] -pub extern "C" fn capable_rt_string_as_slice(ptr: *const u8, len: usize) -> Handle { - let handle = new_handle(); - with_table(&SLICES, "slice table", |table| { - table.insert( - handle, - SliceState { - ptr: ptr as usize, - len, - }, - ); - }); - handle -} - -#[no_mangle] -pub extern "C" fn capable_rt_string_from_bytes( - slice: Handle, - out_ptr: *mut *const u8, - out_len: *mut u64, - out_err: *mut i32, -) -> u8 { - let (ptr, len) = with_table(&SLICES, "slice table", |table| { - table.get(&slice).map(|state| (state.ptr, state.len)) - }) - .unwrap_or((0, 0)); - - if ptr == 0 || len == 0 { - return write_string_result(out_ptr, out_len, out_err, Ok(String::new())); + if left_state.len == 0 { + return 1; } - - let bytes = unsafe { std::slice::from_raw_parts(ptr as *const u8, len) }; - let value = String::from_utf8_lossy(bytes).into_owned(); - write_string_result(out_ptr, out_len, out_err, Ok(value)) + if left_state.ptr == 0 || right_state.ptr == 0 { + return 0; + } + let s1 = + unsafe { std::slice::from_raw_parts(left_state.ptr as *const u8, left_state.len) }; + let s2 = + unsafe { std::slice::from_raw_parts(right_state.ptr as *const u8, right_state.len) }; + i8::from(s1 == s2) } #[no_mangle] @@ -2245,18 +2082,10 @@ fn resolve_rooted_path(root: &Path, rel: &Path) -> Result { Ok(full) } -fn write_result( - out_ptr: *mut *const u8, - out_len: *mut u64, - out_err: *mut i32, - result: Result, -) -> u8 { +fn write_result(out_ok: *mut CapString, out_err: *mut i32, result: Result) -> u8 { unsafe { - if !out_ptr.is_null() { - *out_ptr = std::ptr::null(); - } - if !out_len.is_null() { - *out_len = 0; + if !out_ok.is_null() { + (*out_ok).bytes = 0; } if !out_err.is_null() { *out_err = 0; @@ -2264,18 +2093,10 @@ fn write_result( } match result { Ok(contents) => { - let bytes = contents.into_bytes().into_boxed_slice(); - let len = bytes.len() as u64; - let ptr = Box::into_raw(bytes) as *const u8; + let handle = to_slice_handle(contents); unsafe { - if !out_ptr.is_null() { - *out_ptr = ptr; - } - if !out_len.is_null() { - *out_len = len; - } - if !out_err.is_null() { - *out_err = 0; + if !out_ok.is_null() { + (*out_ok).bytes = handle; } } 0 @@ -2291,19 +2112,14 @@ fn write_result( } } -/// Write a Result[String, i32] payload using the ResultString ABI (u64 length). fn write_string_result( - out_ptr: *mut *const u8, - out_len: *mut u64, + out_ok: *mut CapString, out_err: *mut i32, result: Result, ) -> u8 { unsafe { - if !out_ptr.is_null() { - *out_ptr = std::ptr::null(); - } - if !out_len.is_null() { - *out_len = 0; + if !out_ok.is_null() { + (*out_ok).bytes = 0; } if !out_err.is_null() { *out_err = 0; @@ -2311,15 +2127,10 @@ fn write_string_result( } match result { Ok(s) => { - let len = s.len(); - let ptr = s.as_ptr(); - std::mem::forget(s); + let handle = to_slice_handle(s); unsafe { - if !out_ptr.is_null() { - *out_ptr = ptr; - } - if !out_len.is_null() { - *out_len = len as u64; + if !out_ok.is_null() { + (*out_ok).bytes = handle; } } 0 @@ -2335,12 +2146,27 @@ fn write_string_result( } } -unsafe fn read_str(ptr: *const u8, len: usize) -> Option { +fn slice_state(handle: Handle) -> Option { + with_table(&SLICES, "slice table", |table| table.get(&handle).copied()) +} + +fn read_slice_string(handle: Handle) -> Option { + if handle == 0 { + return Some(String::new()); + } + let state = slice_state(handle)?; + if state.ptr == 0 { + return Some(String::new()); + } + let bytes = unsafe { std::slice::from_raw_parts(state.ptr as *const u8, state.len) }; + std::str::from_utf8(bytes).ok().map(|s| s.to_string()) +} + +unsafe fn read_cap_string(ptr: *const CapString) -> Option { if ptr.is_null() { return None; } - let bytes = std::slice::from_raw_parts(ptr, len); - std::str::from_utf8(bytes).ok().map(|s| s.to_string()) + read_slice_string((*ptr).bytes) } fn normalize_root(root: &Path) -> Option { diff --git a/stdlib/sys/string.cap b/stdlib/sys/string.cap index 2ac21a2..4814525 100644 --- a/stdlib/sys/string.cap +++ b/stdlib/sys/string.cap @@ -5,14 +5,17 @@ use sys::buffer use sys::bytes use sys::vec +pub copy struct string { + bytes: Slice +} + pub struct SplitOnce { left: string, right: string } -/// Intrinsic; implemented by the runtime. pub fn from_bytes(bytes: Slice) -> Result { - return Err(buffer::AllocErr::Oom) + return Ok(string { bytes: bytes }) } fn build_range(s: string, start: i32, end: i32) -> string { @@ -104,19 +107,16 @@ fn upper_ascii_byte(b: u8) -> u8 { } impl string { - /// Intrinsic; implemented by the runtime. pub fn len(self) -> i32 { - return 0 + return self.bytes.len() } - /// Intrinsic; implemented by the runtime. pub fn byte_at(self, index: i32) -> u8 { - return 0u8 + return self.bytes.at(index) } - /// Intrinsic; implemented by the runtime. pub fn as_slice(self) -> Slice { - return () + return self.bytes } /// bytes() is an alias for as_slice(). @@ -124,14 +124,64 @@ impl string { return self.as_slice() } - /// Intrinsic; implemented by the runtime. pub fn split_whitespace(self) -> Vec { - return () + let out = buffer::vec_string_new() + let bytes = self.as_slice() + let len = bytes.len() + let i = 0 + while (i < len) { + while (i < len && bytes.at(i).is_whitespace()) { + i = i + 1 + } + if (i >= len) { + break + } + let start = i + while (i < len && !bytes.at(i).is_whitespace()) { + i = i + 1 + } + let part = build_range(self, start, i) + match (out.push(part)) { + Ok(_) => { } + Err(_) => { panic() } + } + } + return out } - /// Intrinsic; implemented by the runtime. pub fn lines(self) -> Vec { - return () + let out = buffer::vec_string_new() + let bytes = self.as_slice() + let len = bytes.len() + let start = 0 + let i = 0 + while (i < len) { + if (bytes.at(i) == '\n') { + let end = i + if (end > start && bytes.at(end - 1) == '\r') { + end = end - 1 + } + let part = build_range(self, start, end) + match (out.push(part)) { + Ok(_) => { } + Err(_) => { panic() } + } + start = i + 1 + } + i = i + 1 + } + if (start < len) { + let end = len + if (end > start && bytes.at(end - 1) == '\r') { + end = end - 1 + } + let part = build_range(self, start, end) + match (out.push(part)) { + Ok(_) => { } + Err(_) => { panic() } + } + } + return out } pub fn split(self, delim: u8) -> Vec {