diff --git a/PLAN.md b/PLAN.md index 49a270d..c1f1367 100644 --- a/PLAN.md +++ b/PLAN.md @@ -49,9 +49,9 @@ A Rust-flavored, C-targeting language - built pipeline-first. To successfully write a simple ray tracer, we need continuous math, data structures, and I/O. The following path establishes these prerequisites: -- [ ] **Floating-Point Support:** Add `f32`/`f64` types, decimal literals, and Cranelift lowering for `fadd`, `fmul`, etc. -- [ ] **FFI & Interop:** Implement `extern fn` declarations to bind C standard library functions (like `putchar` or `printf`) for `.ppm` image output. -- [ ] **Type Casting:** Add the `as` operator to convert floating-point color bounds `[0.0, 1.0]` into integer byte formats `[0, 255]`. +- [x] **Floating-Point Support:** Add `f32`/`f64` types, decimal literals, and Cranelift lowering for `fadd`, `fmul`, etc. +- [x] **FFI & Interop:** Implement `extern fn` declarations to bind C standard library functions (like `putchar` or `printf`) for `.ppm` image output. +- [x] **Type Casting:** Add the `as` operator to convert floating-point color bounds `[0.0, 1.0]` into integer byte formats `[0, 255]`. - [ ] **Pointers:** Add pointer types (`*T`), address-of (`&`), and dereference (`*`) operators. - [ ] **Structs:** Add `struct` definitions, initializers, and field access (`ray.origin.x`) to represent 3D vectors, rays, and spheres. - [ ] **Arrays:** Add fixed-size arrays (`[T; N]`) or heap allocations for the scene and framebuffers. diff --git a/runner.py b/runner.py index a9b43f3..a5ce722 100755 --- a/runner.py +++ b/runner.py @@ -12,6 +12,7 @@ def run_test(test_file: Path, compiler_bin: Path): expected_code = 0 code_lines = [] harness_lines = None + expected_output_lines = None current_section = None for line in lines: @@ -20,6 +21,8 @@ def run_test(test_file: Path, compiler_bin: Path): current_section = trimmed[1:-1] if current_section == "harness": harness_lines = [] + elif current_section == "expected_output": + expected_output_lines = [] elif current_section: if current_section == "expected_return_code": if trimmed: @@ -28,6 +31,8 @@ def run_test(test_file: Path, compiler_bin: Path): code_lines.append(line) elif current_section == "harness": harness_lines.append(line) + elif current_section == "expected_output": + expected_output_lines.append(line) code = "".join(code_lines) harness = "".join(harness_lines) if harness_lines is not None else None @@ -75,6 +80,13 @@ def run_test(test_file: Path, compiler_bin: Path): if exec_res.returncode != expected_code: raise Exception(f"Expected return code {expected_code}, but got {exec_res.returncode}") + # 4. Check the standard output if expected_output is provided + if expected_output_lines is not None: + expected_out = "".join(expected_output_lines).strip() + actual_out = exec_res.stdout.strip() + if expected_out != actual_out: + raise Exception(f"Expected output:\n{expected_out}\n\nActual output:\n{actual_out}") + def main(): GREEN = '\033[92m' RED = '\033[91m' diff --git a/src/backend/cranelift.rs b/src/backend/cranelift.rs index 1caab3f..6a8b0a0 100644 --- a/src/backend/cranelift.rs +++ b/src/backend/cranelift.rs @@ -22,6 +22,7 @@ pub struct CraneliftBackend { ctx: Context, builder_context: FunctionBuilderContext, module: ObjectModule, + func_ids: HashMap, } impl CraneliftBackend { @@ -46,6 +47,7 @@ impl CraneliftBackend { ctx: Context::new(), builder_context: FunctionBuilderContext::new(), module: ObjectModule::new(builder), + func_ids: HashMap::new(), } } @@ -55,6 +57,40 @@ impl CraneliftBackend { pub fn compile_module(mut self, module: &MirModule) -> (String, Vec) { let mut ir_output = String::new(); + for ext_func in &module.extern_functions { + let mut sig = self.module.make_signature(); + for param_ty in &ext_func.params { + sig.params.push(AbiParam::new(Self::lower_type(param_ty))); + } + if ext_func.return_type != Ty::Unit { + sig.returns + .push(AbiParam::new(Self::lower_type(&ext_func.return_type))); + } + + let func_id = self + .module + .declare_function(&ext_func.name, Linkage::Import, &sig) + .unwrap(); + self.func_ids.insert(ext_func.name.clone(), func_id); + } + + for func in &module.functions { + let mut sig = self.module.make_signature(); + for param_id in &func.params { + let param_ty = &func.locals[param_id.0].ty; + sig.params.push(AbiParam::new(Self::lower_type(param_ty))); + } + if func.return_type != Ty::Unit { + sig.returns + .push(AbiParam::new(Self::lower_type(&func.return_type))); + } + let func_id = self + .module + .declare_function(&func.name, Linkage::Export, &sig) + .unwrap(); + self.func_ids.insert(func.name.clone(), func_id); + } + for func in &module.functions { self.compile_function(func); @@ -67,12 +103,8 @@ impl CraneliftBackend { ir_output.push_str(&format!("; Function: {}\n{}", func.name, self.ctx.func)); ir_output.push('\n'); - let func_id = self - .module - .declare_function(&func.name, Linkage::Export, &self.ctx.func.signature) - .unwrap(); - - self.module.define_function(func_id, &mut self.ctx).unwrap(); + let id = self.func_ids[&func.name]; + self.module.define_function(id, &mut self.ctx).unwrap(); self.module.clear_context(&mut self.ctx); } @@ -115,6 +147,8 @@ impl CraneliftBackend { var_map, block_map, locals: &func.locals, + module: &mut self.module, + func_ids: &self.func_ids, }; if let Some(first_block) = func.blocks.first() { @@ -173,6 +207,8 @@ struct FunctionTranslator<'a> { var_map: HashMap, block_map: HashMap, locals: &'a [LocalDecl], + module: &'a mut ObjectModule, + func_ids: &'a HashMap, } impl<'a> FunctionTranslator<'a> { @@ -180,8 +216,13 @@ impl<'a> FunctionTranslator<'a> { match &stmt.kind { StatementKind::Assign(local_id, rvalue) => { let val = self.translate_rvalue(rvalue); - let var = self.var_map[local_id]; - self.builder.def_var(var, val); + if let Some(v) = val { + let var = self.var_map[local_id]; + self.builder.def_var(var, v); + } + } + StatementKind::SideEffect(rvalue) => { + self.translate_rvalue(rvalue); } } } @@ -250,9 +291,9 @@ impl<'a> FunctionTranslator<'a> { } } - fn translate_rvalue(&mut self, rvalue: &Rvalue) -> ir::Value { + fn translate_rvalue(&mut self, rvalue: &Rvalue) -> Option { match rvalue { - Rvalue::Use(op) => self.translate_operand(op), + Rvalue::Use(op) => Some(self.translate_operand(op)), Rvalue::UnaryOp(op, inner) => { let inner_val = self.translate_operand(inner); let ty = self.get_operand_type(inner); @@ -260,17 +301,19 @@ impl<'a> FunctionTranslator<'a> { match op { UnaryOp::Neg => { if ty.is_float() { - self.builder.ins().fneg(inner_val) + Some(self.builder.ins().fneg(inner_val)) } else { - self.builder.ins().ineg(inner_val) + Some(self.builder.ins().ineg(inner_val)) } } UnaryOp::Not => { let cl_ty = CraneliftBackend::lower_type(&ty); let zero = self.builder.ins().iconst(cl_ty, 0); - self.builder - .ins() - .icmp(ir::condcodes::IntCC::Equal, inner_val, zero) + Some( + self.builder + .ins() + .icmp(ir::condcodes::IntCC::Equal, inner_val, zero), + ) } } } @@ -282,7 +325,7 @@ impl<'a> FunctionTranslator<'a> { let is_signed = matches!(ty, Ty::I8 | Ty::I16 | Ty::I32 | Ty::I64); let is_float = ty.is_float(); - match op { + Some(match op { BinaryOp::Add => { if is_float { self.builder.ins().fadd(lhs_val, rhs_val) @@ -412,7 +455,7 @@ impl<'a> FunctionTranslator<'a> { self.builder.ins().icmp(cc, lhs_val, rhs_val) } } - } + }) } Rvalue::Cast(to_ty, inner) => { let inner_val = self.translate_operand(inner); @@ -420,7 +463,7 @@ impl<'a> FunctionTranslator<'a> { let cl_to_ty = CraneliftBackend::lower_type(to_ty); if from_ty == *to_ty { - inner_val + Some(inner_val) } else { match (from_ty.is_float(), to_ty.is_float()) { (false, false) => { @@ -430,43 +473,60 @@ impl<'a> FunctionTranslator<'a> { if to_width > from_width { if from_ty.is_signed() { - self.builder.ins().sextend(cl_to_ty, inner_val) + Some(self.builder.ins().sextend(cl_to_ty, inner_val)) } else { - self.builder.ins().uextend(cl_to_ty, inner_val) + Some(self.builder.ins().uextend(cl_to_ty, inner_val)) } } else if to_width < from_width { - self.builder.ins().ireduce(cl_to_ty, inner_val) + Some(self.builder.ins().ireduce(cl_to_ty, inner_val)) } else { - inner_val // e.g. bitcasting between same-sized int and uint + Some(inner_val) // e.g. bitcasting between same-sized int and uint } } (true, true) => { // Float <-> Float if to_ty.bit_width() > from_ty.bit_width() { - self.builder.ins().fpromote(cl_to_ty, inner_val) + Some(self.builder.ins().fpromote(cl_to_ty, inner_val)) } else { - self.builder.ins().fdemote(cl_to_ty, inner_val) + Some(self.builder.ins().fdemote(cl_to_ty, inner_val)) } } (false, true) => { // Integer -> Float if from_ty.is_signed() { - self.builder.ins().fcvt_from_sint(cl_to_ty, inner_val) + Some(self.builder.ins().fcvt_from_sint(cl_to_ty, inner_val)) } else { - self.builder.ins().fcvt_from_uint(cl_to_ty, inner_val) + Some(self.builder.ins().fcvt_from_uint(cl_to_ty, inner_val)) } } (true, false) => { // Float -> Integer if to_ty.is_signed() { - self.builder.ins().fcvt_to_sint_sat(cl_to_ty, inner_val) + Some(self.builder.ins().fcvt_to_sint_sat(cl_to_ty, inner_val)) } else { - self.builder.ins().fcvt_to_uint_sat(cl_to_ty, inner_val) + Some(self.builder.ins().fcvt_to_uint_sat(cl_to_ty, inner_val)) } } } } } + Rvalue::Call(name, args, ret_ty) => { + let func_id = self.func_ids[name]; + let local_callee = self.module.declare_func_in_func(func_id, self.builder.func); + + let mut arg_vals = Vec::new(); + for arg in args { + arg_vals.push(self.translate_operand(arg)); + } + + let call_inst = self.builder.ins().call(local_callee, &arg_vals); + + if *ret_ty == Ty::Unit { + None + } else { + Some(self.builder.inst_results(call_inst)[0]) + } + } } } } diff --git a/src/frontend/ast.rs b/src/frontend/ast.rs index 28789a6..8cebab6 100644 --- a/src/frontend/ast.rs +++ b/src/frontend/ast.rs @@ -57,6 +57,12 @@ pub enum DeclKind { return_type: P::ReturnType, body: Stmt

, }, + ForeignFunction { + name: String, + name_span: Span, + params: Vec, + return_type: P::ReturnType, + }, } #[derive(Debug, PartialEq, Eq)] @@ -161,6 +167,10 @@ pub enum ExprKind { expr: Box>, ty: P::CastType, }, + Call { + callee: Box>, + args: Vec>, + }, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/src/frontend/lexer.rs b/src/frontend/lexer.rs index c4f45f5..9461936 100644 --- a/src/frontend/lexer.rs +++ b/src/frontend/lexer.rs @@ -64,6 +64,7 @@ impl<'src> Lexer<'src> { match &self.source[start..self.cursor] { "fn" => TokenKind::Fn, + "foreign" => TokenKind::Foreign, "if" => TokenKind::If, "as" => TokenKind::As, "else" => TokenKind::Else, @@ -249,7 +250,7 @@ mod test { #[test] fn identifiers() { assert_eq!( - tokenize("HELLO _hello _0@ fn if else return let while break continue as"), + tokenize("HELLO _hello _0@ fn if else return let while break continue as foreign"), vec![ Token::new(TokenKind::Identifier, "HELLO", Span::new(0, 5)), Token::new(TokenKind::Identifier, "_hello", Span::new(6, 12)), @@ -264,6 +265,7 @@ mod test { Token::new(TokenKind::Break, "break", Span::new(45, 50)), Token::new(TokenKind::Continue, "continue", Span::new(51, 59)), Token::new(TokenKind::As, "as", Span::new(60, 62)), + Token::new(TokenKind::Foreign, "foreign", Span::new(63, 70)), ] ) } diff --git a/src/frontend/parser.rs b/src/frontend/parser.rs index 800df80..fd2ca4e 100644 --- a/src/frontend/parser.rs +++ b/src/frontend/parser.rs @@ -135,6 +135,7 @@ impl<'src> Parser<'src> { match peek_token.kind { TokenKind::Fn => self.parse_function_decl(), + TokenKind::Foreign => self.parse_foreign_function_decl(), _ => Err(ParseError::new( format!( @@ -185,6 +186,45 @@ impl<'src> Parser<'src> { }) } + /// Parses a foreign function declaration. + /// + /// ```ebnf + /// foreign_function_decl = "foreign" "fn" IDENTIFIER "(" function_params ")" [ "->" type ] ";" ; + /// ``` + fn parse_foreign_function_decl(&mut self) -> ParseResult { + let foreign_token = self.expect(TokenKind::Foreign)?; + self.expect(TokenKind::Fn)?; + + let (name, name_span) = { + let ident_token = self.expect(TokenKind::Identifier)?; + (ident_token.text.to_string(), ident_token.span) + }; + + self.expect(TokenKind::LParen)?; + let params = self.parse_function_params()?; + self.expect(TokenKind::RParen)?; + + let return_type = if self.is_peek(TokenKind::Arrow) { + self.advance(); + Some(self.parse_type()?) + } else { + None + }; + + let semi_token = self.expect(TokenKind::Semicolon)?; + let span = foreign_token.span.join(semi_token.span); + + Ok(Decl { + kind: DeclKind::ForeignFunction { + name, + name_span, + params, + return_type, + }, + span, + }) + } + /// Parses the function parameter list. /// /// ```ebnf @@ -581,6 +621,38 @@ impl<'src> Parser<'src> { continue; } + if peek_token.kind == TokenKind::LParen { + let left_bp = 30; // Function calls have very high precedence + if left_bp < min_bp { + break; + } + self.advance(); // consume `(` + + let mut args = Vec::new(); + if !self.is_peek(TokenKind::RParen) { + loop { + args.push(self.parse_expr()?); + if !self.is_peek(TokenKind::Comma) { + break; + } + self.advance(); // consume `,` + } + } + + let rparen_token = self.expect(TokenKind::RParen)?; + let span = lhs.span.join(rparen_token.span); + + lhs = Expr { + kind: ExprKind::Call { + callee: Box::new(lhs), + args, + }, + ty: (), + span, + }; + continue; + } + let Some((op, left_bp, right_bp)) = self.infix_operator(peek_token.kind) else { break; // Not an infix operator }; diff --git a/src/frontend/sema.rs b/src/frontend/sema.rs index 0ad9e5e..dc5bb74 100644 --- a/src/frontend/sema.rs +++ b/src/frontend/sema.rs @@ -251,6 +251,12 @@ impl Sema { params, return_type, .. + } + | DeclKind::ForeignFunction { + name, + params, + return_type, + .. } => { let param_tys: Vec = params.iter().map(|p| Ty::from(&p.ty.kind)).collect(); let ret_ty = return_type @@ -321,6 +327,31 @@ impl Sema { span: decl.span, } } + DeclKind::ForeignFunction { + name, + name_span, + params, + return_type, + } => { + let typed_params = params + .iter() + .map(|p| (p.name.clone(), Ty::from(&p.ty.kind))) + .collect(); + let expected_ret_ty = return_type + .as_ref() + .map(|t| Ty::from(&t.kind)) + .unwrap_or(Ty::Unit); + + TypedDecl { + kind: TypedDeclKind::ForeignFunction { + name: name.clone(), + name_span: *name_span, + params: typed_params, + return_type: expected_ret_ty, + }, + span: decl.span, + } + } } } @@ -663,6 +694,33 @@ impl Sema { span: expr.span, } } + ExprKind::Call { callee, args } => { + let typed_callee = self.analyze_expr(callee); + let mut typed_args = Vec::new(); + let mut arg_tys = Vec::new(); + + for arg in args { + let typed_arg = self.analyze_expr(arg); + arg_tys.push(typed_arg.ty.clone()); + typed_args.push(typed_arg); + } + + let ret_ty = self.new_var(); + let expected_callee_ty = Ty::Function(arg_tys, Box::new(ret_ty.clone())); + + if let Err(e) = self.unify(&typed_callee.ty, &expected_callee_ty) { + self.errors.push(SemanticError::new(e, callee.span)); + } + + TypedExpr { + kind: TypedExprKind::Call { + callee: Box::new(typed_callee), + args: typed_args, + }, + ty: ret_ty, + span: expr.span, + } + } } } @@ -690,6 +748,20 @@ impl Sema { body: self.apply_subst_stmt(body), } } + TypedDeclKind::ForeignFunction { + name, + name_span, + params, + return_type, + } => TypedDeclKind::ForeignFunction { + name, + name_span, + params: params + .into_iter() + .map(|(n, ty)| (n, self.apply_subst(&ty))) + .collect(), + return_type: self.apply_subst(&return_type), + }, }; TypedDecl { kind, span } @@ -775,6 +847,10 @@ impl Sema { expr: Box::new(self.apply_subst_expr(*expr)), ty: self.apply_subst(&ty), }, + TypedExprKind::Call { callee, args } => TypedExprKind::Call { + callee: Box::new(self.apply_subst_expr(*callee)), + args: args.into_iter().map(|a| self.apply_subst_expr(a)).collect(), + }, }; TypedExpr { kind, ty, span } diff --git a/src/frontend/token.rs b/src/frontend/token.rs index fe7e660..549d85f 100644 --- a/src/frontend/token.rs +++ b/src/frontend/token.rs @@ -57,6 +57,7 @@ pub enum TokenKind { // Keywords Fn, + Foreign, If, As, Else, @@ -121,6 +122,7 @@ impl Display for TokenKind { TokenKind::BooleanLit => "a boolean", TokenKind::FloatLit => "a float", TokenKind::Fn => "`fn`", + TokenKind::Foreign => "`foreign`", TokenKind::As => "`as`", TokenKind::If => "`if`", TokenKind::Else => "`else`", diff --git a/src/middle/builder.rs b/src/middle/builder.rs index 541dd56..2581c9b 100644 --- a/src/middle/builder.rs +++ b/src/middle/builder.rs @@ -10,6 +10,7 @@ pub struct MirBuilder; impl MirBuilder { /// Builds a `MirModule` from a `TypedModule`. pub fn build(module: &TypedModule) -> MirModule { + let mut extern_functions = Vec::new(); let mut functions = Vec::new(); for decl in &module.decls { @@ -53,10 +54,25 @@ impl MirBuilder { functions.push(builder.finish()); } + TypedDeclKind::ForeignFunction { + name, + params, + return_type, + .. + } => { + extern_functions.push(MirExternFunction { + name: name.clone(), + params: params.iter().map(|(_, ty)| ty.clone()).collect(), + return_type: return_type.clone(), + }); + } } } - MirModule { functions } + MirModule { + extern_functions, + functions, + } } } @@ -399,6 +415,34 @@ impl FuncBuilder { Operand::Copy(temp) } + TypedExprKind::Call { callee, args } => { + let callee_name = match &callee.kind { + TypedExprKind::Identifier { name } => name.clone(), + _ => unimplemented!("indirect function calls are not yet supported"), + }; + + let mut arg_ops = Vec::new(); + for arg in args { + arg_ops.push(self.lower_expr(arg)); + } + + let rval = Rvalue::Call(callee_name, arg_ops, expr.ty.clone()); + + if expr.ty == Ty::Unit { + self.emit_stmt(Statement { + kind: StatementKind::SideEffect(rval), + span: expr.span, + }); + Operand::Constant(ConstantValue::Boolean(false)) // Dummy value for Unit assignments + } else { + let temp = self.new_temp(expr.ty.clone()); + self.emit_stmt(Statement { + kind: StatementKind::Assign(temp, rval), + span: expr.span, + }); + Operand::Copy(temp) + } + } } } } diff --git a/src/middle/dce.rs b/src/middle/dce.rs index 82c9ad4..8034250 100644 --- a/src/middle/dce.rs +++ b/src/middle/dce.rs @@ -126,6 +126,7 @@ mod test { }, ], }], + extern_functions: vec![], }; let warnings = eliminate_dead_code(&mut module); @@ -180,6 +181,7 @@ mod test { }, ], }], + extern_functions: vec![], }; let warnings = eliminate_dead_code(&mut module); diff --git a/src/middle/fold.rs b/src/middle/fold.rs index 801fa97..8631a7b 100644 --- a/src/middle/fold.rs +++ b/src/middle/fold.rs @@ -19,19 +19,24 @@ fn optimize_function(func: &mut MirFunction) { let mut known_constants = HashMap::new(); for stmt in &mut block.statements { - let StatementKind::Assign(local, rvalue) = &mut stmt.kind; + match &mut stmt.kind { + StatementKind::Assign(local, rvalue) => { + // Propagate any known constants downwards into the rvalue + propagate_rvalue(rvalue, &known_constants); - // Propagate any known constants downwards into the rvalue - propagate_rvalue(rvalue, &known_constants); - - // Attempt to compute the rvalue - if let Some(constant) = evaluate_rvalue(rvalue) { - // Replace the complex instruction with a simple constant use - *rvalue = Rvalue::Use(Operand::Constant(constant.clone())); - known_constants.insert(*local, constant); - } else { - // Reassigned to a non-computable value; remove older cached inferences - known_constants.remove(local); + // Attempt to compute the rvalue + if let Some(constant) = evaluate_rvalue(rvalue) { + // Replace the complex instruction with a simple constant use + *rvalue = Rvalue::Use(Operand::Constant(constant.clone())); + known_constants.insert(*local, constant); + } else { + // Reassigned to a non-computable value; remove older cached inferences + known_constants.remove(local); + } + } + StatementKind::SideEffect(rvalue) => { + propagate_rvalue(rvalue, &known_constants); + } } } @@ -76,6 +81,11 @@ fn propagate_rvalue(rvalue: &mut Rvalue, known_constants: &HashMap propagate_operand(op, known_constants), + Rvalue::Call(_, args, _) => { + for arg in args { + propagate_operand(arg, known_constants); + } + } } } @@ -295,6 +305,7 @@ mod test { let mut module = MirModule { functions: vec![func], + extern_functions: vec![], }; fold_constants(&mut module); @@ -302,7 +313,9 @@ mod test { let block = &module.functions[0].blocks[0]; // The third statement (LocalId(2) = ...) should be folded to 15 - let StatementKind::Assign(_, rvalue) = &block.statements[2].kind; + let StatementKind::Assign(_, rvalue) = &block.statements[2].kind else { + panic!(); + }; assert!(matches!( rvalue, Rvalue::Use(Operand::Constant(ConstantValue::Integer(15, Ty::I32))) @@ -340,6 +353,7 @@ mod test { let mut module = MirModule { functions: vec![func], + extern_functions: vec![], }; fold_constants(&mut module); diff --git a/src/middle/mir.rs b/src/middle/mir.rs index 719d995..e07eefb 100644 --- a/src/middle/mir.rs +++ b/src/middle/mir.rs @@ -12,9 +12,17 @@ pub struct LocalId(pub usize); #[derive(Debug)] pub struct MirModule { + pub extern_functions: Vec, pub functions: Vec, } +#[derive(Debug)] +pub struct MirExternFunction { + pub name: String, + pub params: Vec, + pub return_type: Ty, +} + #[derive(Debug)] pub struct MirFunction { pub name: String, @@ -52,6 +60,8 @@ pub struct Statement { pub enum StatementKind { /// Assigns the result of an Rvalue to a local variable or temporary. Assign(LocalId, Rvalue), + /// Executes an Rvalue strictly for its side effects (e.g. FFI calling `Unit` functions) + SideEffect(Rvalue), } /// Operations that produce a value. @@ -61,6 +71,7 @@ pub enum Rvalue { UnaryOp(UnaryOp, Operand), BinaryOp(BinaryOp, Operand, Operand), Cast(Ty, Operand), + Call(String, Vec, Ty), } /// An atomic value used as inputs to instructions. diff --git a/tests/ffi_bindings.test b/tests/ffi_bindings.test new file mode 100644 index 0000000..993abcd --- /dev/null +++ b/tests/ffi_bindings.test @@ -0,0 +1,17 @@ +[code] +foreign fn putchar(c: i32) -> i32; + +fn main() -> i32 { + putchar(72); // 'H' + putchar(105); // 'i' + putchar(33); // '!' + putchar(10); // '\n' + + return 0; +} + +[expected_return_code] +0 + +[expected_output] +Hi! \ No newline at end of file