diff --git a/src/codegen.rs b/src/codegen.rs index 328649d..1844b3e 100644 --- a/src/codegen.rs +++ b/src/codegen.rs @@ -191,6 +191,12 @@ const WIN64_5TH_ARG_OFFSET: i32 = 32; /// Stack space for temporary values (must be 16-byte aligned) const STACK_TEMP_SPACE: i32 = 16; +/// Maximum expression nesting depth before warning (each level uses 16 bytes of stack) +const MAX_EXPR_DEPTH: u32 = 256; + +/// GOSUB stack size in bytes (64K entries * 8 bytes = 512KB) +const GOSUB_STACK_SIZE: i32 = 524288; + /// ASCII character codes const ASCII_TAB: i64 = 9; @@ -223,6 +229,7 @@ pub struct CodeGen { current_proc: Option, // current SUB/FUNCTION name proc_vars: HashMap, // local variables for current proc gosub_used: bool, // whether GOSUB is used (need return stack) + expr_depth: u32, // current expression nesting depth } impl CodeGen { @@ -532,7 +539,10 @@ impl CodeGen { // Initialize GOSUB return stack if needed if self.gosub_used { self.emit(" # Initialize GOSUB return stack"); - self.emit(" lea rax, [rip + _gosub_stack + 8192]"); // Point to end (stack grows down) + self.emit(&format!( + " lea rax, [rip + _gosub_stack + {}]", + GOSUB_STACK_SIZE + )); // Point to end (stack grows down) self.emit(" mov QWORD PTR [rip + _gosub_sp], rax"); } @@ -628,8 +638,10 @@ impl CodeGen { self.emit(&placeholder); // Parameters are passed in registers (per platform ABI) - // Store them in the reserved stack space + // First N params in registers, rest on stack at [rbp+16], [rbp+24], etc. + // Store them all in our local stack space let int_regs = PlatformAbi::INT_ARG_REGS; + let max_reg_args = int_regs.len(); for (i, param) in params.iter().enumerate() { self.stack_offset -= 8; let data_type = DataType::from_suffix(param); @@ -640,11 +652,24 @@ impl CodeGen { data_type, }, ); - if i < int_regs.len() { + if i < max_reg_args { + // Parameter in register - store to our local stack self.emit(&format!( " mov QWORD PTR [rbp + {}], {}", self.stack_offset, int_regs[i] )); + } else { + // Parameter on call stack - copy to our local stack + // Overflow args are at [rbp+16], [rbp+24], etc. (after saved rbp and ret addr) + let stack_arg_offset = 16 + (i - max_reg_args) * 8; + self.emit(&format!( + " mov rax, QWORD PTR [rbp + {}]", + stack_arg_offset + )); + self.emit(&format!( + " mov QWORD PTR [rbp + {}], rax", + self.stack_offset + )); } } @@ -1035,10 +1060,14 @@ impl CodeGen { GotoTarget::Label(s) => format!("_label_{}", s), }; let ret_label = self.new_label("gosub_ret"); - // Push return address to GOSUB stack (use rcx - caller-saved on both ABIs) - self.emit(&format!(" lea rax, [rip + {}]", ret_label)); + // Check for stack overflow before push self.emit(" mov rcx, QWORD PTR [rip + _gosub_sp]"); self.emit(" sub rcx, 8"); + self.emit(" lea rax, [rip + _gosub_stack]"); + self.emit(" cmp rcx, rax"); + self.emit(" jb _rt_gosub_overflow"); + // Push return address to GOSUB stack + self.emit(&format!(" lea rax, [rip + {}]", ret_label)); self.emit(" mov QWORD PTR [rcx], rax"); self.emit(" mov QWORD PTR [rip + _gosub_sp], rcx"); self.emit(&format!(" jmp {}", label)); @@ -1356,34 +1385,45 @@ impl CodeGen { /// Generate code for a binary expression fn gen_binary_expr(&mut self, op: BinaryOp, left: &Expr, right: &Expr) -> DataType { + // Track expression nesting depth and warn if too deep + self.expr_depth += 1; + if self.expr_depth == MAX_EXPR_DEPTH + 1 { + eprintln!( + "Warning: Expression nesting exceeds {} levels, stack overflow risk", + MAX_EXPR_DEPTH + ); + } + let result_type = self.promote_types(self.expr_type(left), self.expr_type(right), op); // Handle string concatenation specially if result_type == DataType::String && op == BinaryOp::Add { // Evaluate left string (ptr in rax, len in rdx) self.gen_expr(left); - // Save left string on stack (ptr, len) - self.emit(" push rdx"); // left len - self.emit(" push rax"); // left ptr + // Save left string on stack using consistent sub rsp pattern (16-byte aligned) + self.emit(&format!(" sub rsp, {}", STACK_TEMP_SPACE)); + self.emit(" mov QWORD PTR [rsp], rax"); // left ptr + self.emit(" mov QWORD PTR [rsp + 8], rdx"); // left len // Evaluate right string (ptr in rax, len in rdx) self.gen_expr(right); // Now: right ptr in rax, right len in rdx - // Stack: left ptr, left len // Call runtime string concat: rt_strcat(left_ptr, left_len, right_ptr, right_len) // Save right string temporarily self.emit(" mov r8, rax"); // right ptr self.emit(" mov r9, rdx"); // right len - // Pop left string (LIFO: ptr popped first since it was pushed last) - self.emit(" pop rax"); // left ptr from stack - self.emit(" pop rdx"); // left len from stack + // Restore left string from stack + self.emit(" mov rax, QWORD PTR [rsp]"); // left ptr + self.emit(" mov rdx, QWORD PTR [rsp + 8]"); // left len + self.emit(&format!(" add rsp, {}", STACK_TEMP_SPACE)); self.emit_arg_reg(0, "rax"); // left ptr self.emit_arg_reg(1, "rdx"); // left len self.emit_arg_reg(2, "r8"); // right ptr self.emit_arg_reg(3, "r9"); // right len self.emit(" call _rt_strcat"); // Result: ptr in rax, len in rdx + self.expr_depth -= 1; return DataType::String; } @@ -1519,6 +1559,7 @@ impl CodeGen { } } + self.expr_depth -= 1; result_type } @@ -1618,47 +1659,64 @@ impl CodeGen { } "LEFT$" => { // _rt_left(ptr, len, count) + // Use callee-saved registers to preserve string across count evaluation + self.emit(" push r12"); + self.emit(" push r13"); self.gen_expr(&args[0]); // string: rax=ptr, rdx=len - self.emit_arg_reg(0, "rax"); // ptr - self.emit_arg_reg(1, "rdx"); // len - let count_type = self.gen_expr(&args[1]); // count + self.emit(" mov r12, rax"); // save ptr + self.emit(" mov r13, rdx"); // save len + let count_type = self.gen_expr(&args[1]); // count - safe now let arg2 = Self::arg_reg(2); if count_type.is_integer() { self.emit(&format!(" movsxd {}, eax", arg2)); } else { self.emit(&format!(" cvttsd2si {}, xmm0", arg2)); } + self.emit_arg_reg(0, "r12"); // ptr + self.emit_arg_reg(1, "r13"); // len self.emit(" call _rt_left"); + self.emit(" pop r13"); + self.emit(" pop r12"); } "RIGHT$" => { // _rt_right(ptr, len, count) + // Use callee-saved registers to preserve string across count evaluation + self.emit(" push r12"); + self.emit(" push r13"); self.gen_expr(&args[0]); - self.emit_arg_reg(0, "rax"); // ptr - self.emit_arg_reg(1, "rdx"); // len - let count_type = self.gen_expr(&args[1]); + self.emit(" mov r12, rax"); // save ptr + self.emit(" mov r13, rdx"); // save len + let count_type = self.gen_expr(&args[1]); // count - safe now let arg2 = Self::arg_reg(2); if count_type.is_integer() { self.emit(&format!(" movsxd {}, eax", arg2)); } else { self.emit(&format!(" cvttsd2si {}, xmm0", arg2)); } + self.emit_arg_reg(0, "r12"); // ptr + self.emit_arg_reg(1, "r13"); // len self.emit(" call _rt_right"); + self.emit(" pop r13"); + self.emit(" pop r12"); } "MID$" => { // _rt_mid(ptr, len, start, count) + // Use callee-saved registers to preserve string across position/count evaluation + self.emit(" push r12"); + self.emit(" push r13"); + self.emit(" push r14"); self.gen_expr(&args[0]); - self.emit_arg_reg(0, "rax"); // ptr - self.emit_arg_reg(1, "rdx"); // len - let pos_type = self.gen_expr(&args[1]); - let arg2 = Self::arg_reg(2); + self.emit(" mov r12, rax"); // save ptr + self.emit(" mov r13, rdx"); // save len + let pos_type = self.gen_expr(&args[1]); // start position - safe now if pos_type.is_integer() { - self.emit(&format!(" movsxd {}, eax", arg2)); + self.emit(" movsxd r14, eax"); // save start } else { - self.emit(&format!(" cvttsd2si {}, xmm0", arg2)); + self.emit(" cvttsd2si r14, xmm0"); // save start } let arg3 = Self::arg_reg(3); if args.len() > 2 { - let len_type = self.gen_expr(&args[2]); + let len_type = self.gen_expr(&args[2]); // count - safe now if len_type.is_integer() { self.emit(&format!(" movsxd {}, eax", arg3)); } else { @@ -1667,7 +1725,13 @@ impl CodeGen { } else { self.emit(&format!(" mov {}, -1", arg3)); // rest of string } + self.emit_arg_reg(0, "r12"); // ptr + self.emit_arg_reg(1, "r13"); // len + self.emit_arg_reg(2, "r14"); // start self.emit(" call _rt_mid"); + self.emit(" pop r14"); + self.emit(" pop r13"); + self.emit(" pop r12"); } "INSTR" => { // INSTR([start,] haystack$, needle$) @@ -1798,45 +1862,181 @@ impl CodeGen { } fn gen_call(&mut self, name: &str, args: &[Expr]) { - // Push args in registers (per platform ABI) let int_regs = PlatformAbi::INT_ARG_REGS; + let max_reg_args = int_regs.len(); - // Save current xmm0 if we'll use it for args - // Use 16 bytes to maintain stack alignment during arg evaluation - if !args.is_empty() { - self.emit(&format!(" sub rsp, {}", STACK_TEMP_SPACE)); - self.emit(" movsd QWORD PTR [rsp], xmm0"); + if args.is_empty() { + self.emit(&format!(" call _proc_{}", name)); + return; } - // Pass args: numeric as doubles in integer registers, strings as ptr/len pairs - let mut reg_idx = 0; + // Phase 1: Evaluate ALL arguments to stack temporaries + // This prevents clobbering of registers when args contain nested function calls + // Each arg needs 8 bytes (numeric as double bits, string ptr only - len follows) + let mut arg_info: Vec<(DataType, i32)> = Vec::new(); // (type, stack_offset) + + // Calculate total slots needed (strings need 2 slots: ptr + len) + let mut total_slots = 0; + for arg in args.iter() { + let arg_type = self.expr_type(arg); + if arg_type == DataType::String { + total_slots += 2; // ptr + len + } else { + total_slots += 1; + } + } + + // Allocate stack space (16-byte aligned) + let stack_space = (total_slots * 8 + 15) & !15; + self.emit(&format!(" sub rsp, {}", stack_space)); + + // Evaluate each argument and save to stack + let mut slot_offset = 0i32; for arg in args.iter() { let arg_type = self.gen_expr(arg); if arg_type == DataType::String { - // String: ptr in rax, len in rdx - pass as two args - if reg_idx < int_regs.len() { - self.emit(&format!(" mov {}, rax", int_regs[reg_idx])); - reg_idx += 1; + // String: save ptr and len to consecutive slots + self.emit(&format!(" mov QWORD PTR [rsp + {}], rax", slot_offset)); + self.emit(&format!( + " mov QWORD PTR [rsp + {}], rdx", + slot_offset + 8 + )); + arg_info.push((arg_type, slot_offset)); + slot_offset += 16; + } else { + // Numeric: coerce to double and save + self.gen_coercion(arg_type, DataType::Double); + self.emit(&format!( + " movsd QWORD PTR [rsp + {}], xmm0", + slot_offset + )); + arg_info.push((arg_type, slot_offset)); + slot_offset += 8; + } + } + + // Phase 2: Count register slots used + let mut reg_slots_used: usize = 0; + for (arg_type, _) in arg_info.iter() { + if *arg_type == DataType::String { + reg_slots_used += 2; // ptr + len + } else { + reg_slots_used += 1; + } + } + + // Calculate overflow args (those that don't fit in registers) + let overflow_slots = reg_slots_used.saturating_sub(max_reg_args); + + // Phase 3: Handle overflow args (push to call stack for >6 params) + let overflow_space = if overflow_slots > 0 { + let space = ((overflow_slots * 8 + 15) & !15) as i32; + self.emit(&format!(" sub rsp, {}", space)); + + // Copy overflow args from temp stack to call stack + let mut reg_count = 0; + let mut overflow_idx = 0; + for (arg_type, temp_offset) in arg_info.iter() { + if *arg_type == DataType::String { + // String takes 2 register slots + if reg_count >= max_reg_args { + // Both ptr and len are overflow + self.emit(&format!( + " mov rax, QWORD PTR [rsp + {} + {}]", + space, temp_offset + )); + self.emit(&format!( + " mov QWORD PTR [rsp + {}], rax", + overflow_idx * 8 + )); + overflow_idx += 1; + self.emit(&format!( + " mov rax, QWORD PTR [rsp + {} + {}]", + space, + temp_offset + 8 + )); + self.emit(&format!( + " mov QWORD PTR [rsp + {}], rax", + overflow_idx * 8 + )); + overflow_idx += 1; + } else if reg_count + 1 >= max_reg_args { + // Only len is overflow (ptr fits in last register) + reg_count += 1; // ptr in register + self.emit(&format!( + " mov rax, QWORD PTR [rsp + {} + {}]", + space, + temp_offset + 8 + )); + self.emit(&format!( + " mov QWORD PTR [rsp + {}], rax", + overflow_idx * 8 + )); + overflow_idx += 1; + } + reg_count += 2; + } else { + if reg_count >= max_reg_args { + // This arg is overflow + self.emit(&format!( + " mov rax, QWORD PTR [rsp + {} + {}]", + space, temp_offset + )); + self.emit(&format!( + " mov QWORD PTR [rsp + {}], rax", + overflow_idx * 8 + )); + overflow_idx += 1; + } + reg_count += 1; } - if reg_idx < int_regs.len() { - self.emit(&format!(" mov {}, rdx", int_regs[reg_idx])); + } + space + } else { + 0 + }; + + // Phase 4: Load arguments into registers (immediately before call) + let mut reg_idx = 0; + let base_offset = overflow_space; // Offset to temp stack from current rsp + for (arg_type, temp_offset) in arg_info.iter() { + if reg_idx >= max_reg_args { + break; + } + if *arg_type == DataType::String { + // String: load ptr and len into consecutive registers + if reg_idx < max_reg_args { + self.emit(&format!( + " mov {}, QWORD PTR [rsp + {} + {}]", + int_regs[reg_idx], base_offset, temp_offset + )); reg_idx += 1; } - } else { - // Numeric: coerce to double and pass in integer register - self.gen_coercion(arg_type, DataType::Double); - if reg_idx < int_regs.len() { - self.emit(&format!(" movq {}, xmm0", int_regs[reg_idx])); + if reg_idx < max_reg_args { + self.emit(&format!( + " mov {}, QWORD PTR [rsp + {} + {}]", + int_regs[reg_idx], + base_offset, + temp_offset + 8 + )); reg_idx += 1; } + } else { + // Numeric: load as 64-bit value + self.emit(&format!( + " mov {}, QWORD PTR [rsp + {} + {}]", + int_regs[reg_idx], base_offset, temp_offset + )); + reg_idx += 1; } } + // Make the call self.emit(&format!(" call _proc_{}", name)); - if !args.is_empty() { - self.emit(&format!(" add rsp, {}", STACK_TEMP_SPACE)); - } + // Clean up: overflow space + temp stack space + let total_cleanup = overflow_space + stack_space; + self.emit(&format!(" add rsp, {}", total_cleanup)); } fn gen_dim_array(&mut self, arr: &ArrayDecl) { @@ -2057,7 +2257,10 @@ impl CodeGen { self.emit(".bss"); // GOSUB stack (if needed) if self.gosub_used { - self.emit("_gosub_stack: .skip 8192 # GOSUB return stack"); + self.emit(&format!( + "_gosub_stack: .skip {} # GOSUB return stack (64K entries)", + GOSUB_STACK_SIZE + )); } } } diff --git a/src/lexer.rs b/src/lexer.rs index 30cd5ad..ec65b69 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -444,9 +444,9 @@ mod tests { #[test] fn test_float_literal_decimal() { - let mut lexer = Lexer::new("X = 3.14159"); + let mut lexer = Lexer::new("X = 1.23456"); let tokens = lexer.tokenize().unwrap(); - assert_eq!(tokens[2], Token::Float(3.14159)); + assert_eq!(tokens[2], Token::Float(1.23456)); } #[test] @@ -783,13 +783,13 @@ mod tests { #[test] fn test_function_call() { - let mut lexer = Lexer::new("X = SIN(3.14) + COS(0)"); + let mut lexer = Lexer::new("X = SIN(1.23) + COS(0)"); let tokens = lexer.tokenize().unwrap(); assert_eq!(tokens[0], Token::Ident("X".to_string())); assert_eq!(tokens[1], Token::Eq); assert_eq!(tokens[2], Token::Ident("SIN".to_string())); assert_eq!(tokens[3], Token::LParen); - assert_eq!(tokens[4], Token::Float(3.14)); + assert_eq!(tokens[4], Token::Float(1.23)); assert_eq!(tokens[5], Token::RParen); assert_eq!(tokens[6], Token::Plus); assert_eq!(tokens[7], Token::Ident("COS".to_string())); diff --git a/src/runtime/sysv/data_defs.s b/src/runtime/sysv/data_defs.s index 3d26abb..7550110 100644 --- a/src/runtime/sysv/data_defs.s +++ b/src/runtime/sysv/data_defs.s @@ -12,3 +12,4 @@ _chr_buf: .skip 2 _str_buf: .skip 64 _rng_state: .quad 0x12345678DEADBEEF _cls_seq: .asciz "\033[2J\033[H" +_gosub_overflow_msg: .asciz "Error: GOSUB stack overflow\n" diff --git a/src/runtime/sysv/print.s b/src/runtime/sysv/print.s index f0c5951..9ab491c 100644 --- a/src/runtime/sysv/print.s +++ b/src/runtime/sysv/print.s @@ -130,3 +130,22 @@ _rt_print_float: .Lprint_float_done: leave ret + +# ------------------------------------------------------------------------------ +# _rt_gosub_overflow - Handle GOSUB stack overflow error +# ------------------------------------------------------------------------------ +# Called when the GOSUB return stack is exhausted. Prints an error message +# and terminates the program with exit code 1. +# +# Arguments: none +# Returns: never (calls exit) +# ------------------------------------------------------------------------------ +.globl _rt_gosub_overflow +_rt_gosub_overflow: + push rbp + mov rbp, rsp + lea rdi, [rip + _gosub_overflow_msg] + xor eax, eax + call {libc}printf + mov edi, 1 # exit code 1 + call {libc}exit diff --git a/src/runtime/win64-native/data_defs.s b/src/runtime/win64-native/data_defs.s index 0c4721d..d2ba146 100644 --- a/src/runtime/win64-native/data_defs.s +++ b/src/runtime/win64-native/data_defs.s @@ -15,3 +15,7 @@ _fmt_int: .asciz "%lld" _fmt_float: .asciz "%g" +# Error messages +_gosub_overflow_msg: .ascii "Error: GOSUB stack overflow\r\n" +.equ _gosub_overflow_msg_len, 30 + diff --git a/src/runtime/win64-native/print.s b/src/runtime/win64-native/print.s index 83463da..51ba71b 100644 --- a/src/runtime/win64-native/print.s +++ b/src/runtime/win64-native/print.s @@ -178,3 +178,33 @@ _rt_print_float: leave ret + +# ------------------------------------------------------------------------------ +# _rt_gosub_overflow - Handle GOSUB stack overflow error +# ------------------------------------------------------------------------------ +# Called when the GOSUB return stack is exhausted. Prints an error message +# and terminates the program with exit code 1. +# +# Arguments: none +# Returns: never (calls ExitProcess) +# ------------------------------------------------------------------------------ +.globl _rt_gosub_overflow +_rt_gosub_overflow: + push rbp + mov rbp, rsp + sub rsp, 48 + + # Get stdout handle + lea rax, [rip + _stdout_handle] + mov rcx, [rax] + + # WriteFile(handle, message, length, &bytesWritten, NULL) + lea rdx, [rip + _gosub_overflow_msg] + mov r8, _gosub_overflow_msg_len + lea r9, [rip + _bytes_written] + mov QWORD PTR [rsp + 32], 0 + call WriteFile + + # ExitProcess(1) + mov ecx, 1 + call ExitProcess diff --git a/tests/arithmetic/mod.rs b/tests/arithmetic/mod.rs index 0149deb..6c9d8f3 100644 --- a/tests/arithmetic/mod.rs +++ b/tests/arithmetic/mod.rs @@ -1,4 +1,4 @@ -//! Arithmetic and operator tests +//! Arithmetic and operator tests (consolidated) // Copyright (c) 2025-2026 Jeff Garzik // SPDX-License-Identifier: MIT @@ -6,114 +6,70 @@ use crate::common::compile_and_run; #[test] -fn test_arithmetic_add() { - let output = compile_and_run("PRINT 10 + 5").unwrap(); - assert_eq!(output.trim(), "15"); -} - -#[test] -fn test_arithmetic_sub() { - let output = compile_and_run("PRINT 10 - 3").unwrap(); - assert_eq!(output.trim(), "7"); -} - -#[test] -fn test_arithmetic_mul() { - let output = compile_and_run("PRINT 6 * 7").unwrap(); - assert_eq!(output.trim(), "42"); -} - -#[test] -fn test_expression_precedence() { - let output = compile_and_run("PRINT 2 + 3 * 4").unwrap(); - assert_eq!(output.trim(), "14"); -} - -#[test] -fn test_parentheses() { - let output = compile_and_run("PRINT (2 + 3) * 4").unwrap(); - assert_eq!(output.trim(), "20"); -} - -#[test] -fn test_negative_numbers() { - let output = compile_and_run("PRINT -5 + 10").unwrap(); - assert_eq!(output.trim(), "5"); -} - -#[test] -fn test_arithmetic_division() { - let output = compile_and_run("PRINT 10 / 4").unwrap(); - assert_eq!(output.trim(), "2.5"); -} - -#[test] -fn test_arithmetic_integer_division() { - let output = compile_and_run("PRINT 10 \\ 4").unwrap(); - assert_eq!(output.trim(), "2"); -} - -#[test] -fn test_arithmetic_mod() { - let output = compile_and_run("PRINT 10 MOD 3").unwrap(); - assert_eq!(output.trim(), "1"); -} - -#[test] -fn test_arithmetic_power() { - let output = compile_and_run("PRINT 2 ^ 10").unwrap(); - assert_eq!(output.trim(), "1024"); -} - -#[test] -fn test_logical_and() { +fn test_basic_arithmetic() { + // Tests: add, sub, mul, division, integer_division, mod, power let output = compile_and_run( r#" -IF 1 AND 1 THEN PRINT "yes" -IF 1 AND 0 THEN PRINT "no" +PRINT 10 + 5 +PRINT 10 - 3 +PRINT 6 * 7 +PRINT 10 / 4 +PRINT 10 \ 4 +PRINT 10 MOD 3 +PRINT 2 ^ 10 "#, ) .unwrap(); - assert_eq!(output.trim(), "yes"); -} - -#[test] -fn test_logical_or() { - let output = compile_and_run( - r#" -IF 0 OR 1 THEN PRINT "yes" -IF 0 OR 0 THEN PRINT "no" -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "yes"); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "15", "add"); + assert_eq!(lines[1], "7", "sub"); + assert_eq!(lines[2], "42", "mul"); + assert_eq!(lines[3], "2.5", "division"); + assert_eq!(lines[4], "2", "integer division"); + assert_eq!(lines[5], "1", "mod"); + assert_eq!(lines[6], "1024", "power"); } #[test] -fn test_logical_not() { +fn test_expressions() { + // Tests: precedence, parentheses, negative numbers let output = compile_and_run( r#" -IF NOT 0 THEN PRINT "yes" -IF NOT 1 THEN PRINT "no" +PRINT 2 + 3 * 4 +PRINT (2 + 3) * 4 +PRINT -5 + 10 "#, ) .unwrap(); - assert_eq!(output.trim(), "yes"); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "14", "precedence"); + assert_eq!(lines[1], "20", "parentheses"); + assert_eq!(lines[2], "5", "negative"); } #[test] -fn test_logical_xor() { +fn test_logical_operators() { + // Tests: AND, OR, NOT, XOR let output = compile_and_run( r#" -IF 1 XOR 0 THEN PRINT "a" -IF 0 XOR 1 THEN PRINT "b" -IF 1 XOR 1 THEN PRINT "c" -IF 0 XOR 0 THEN PRINT "d" +IF 1 AND 1 THEN PRINT "and-yes" +IF 1 AND 0 THEN PRINT "and-no" +IF 0 OR 1 THEN PRINT "or-yes" +IF 0 OR 0 THEN PRINT "or-no" +IF NOT 0 THEN PRINT "not-yes" +IF NOT 1 THEN PRINT "not-no" +IF 1 XOR 0 THEN PRINT "xor-a" +IF 0 XOR 1 THEN PRINT "xor-b" +IF 1 XOR 1 THEN PRINT "xor-c" +IF 0 XOR 0 THEN PRINT "xor-d" "#, ) .unwrap(); let lines: Vec<&str> = output.trim().lines().collect(); - assert_eq!(lines, vec!["a", "b"]); + assert_eq!( + lines, + vec!["and-yes", "or-yes", "not-yes", "xor-a", "xor-b"] + ); } #[test] @@ -133,379 +89,102 @@ IF 5 <> 6 THEN PRINT "ok6" assert_eq!(lines.len(), 6); } -// ============================================ -// Same-type arithmetic tests for Integer (%) -// ============================================ - -#[test] -fn test_integer_add() { - let output = compile_and_run( - r#" -A% = 100 -B% = 50 -PRINT A% + B% -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "150"); -} - -#[test] -fn test_integer_sub() { - let output = compile_and_run( - r#" -A% = 100 -B% = 30 -PRINT A% - B% -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "70"); -} - -#[test] -fn test_integer_mul() { - let output = compile_and_run( - r#" -A% = 12 -B% = 5 -PRINT A% * B% -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "60"); -} - -#[test] -fn test_integer_div() { - // Division always produces Double - let output = compile_and_run( - r#" -A% = 7 -B% = 2 -PRINT A% / B% -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "3.5"); -} - -#[test] -fn test_integer_intdiv() { - let output = compile_and_run( - r#" -A% = 17 -B% = 5 -PRINT A% \ B% -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "3"); -} - -#[test] -fn test_integer_mod() { - let output = compile_and_run( - r#" -A% = 17 -B% = 5 -PRINT A% MOD B% -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "2"); -} - -#[test] -fn test_integer_power() { - let output = compile_and_run( - r#" -A% = 2 -B% = 8 -PRINT A% ^ B% -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "256"); -} - -#[test] -fn test_integer_neg() { - let output = compile_and_run( - r#" -A% = 42 -PRINT -A% -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "-42"); -} - -// ============================================ -// Same-type arithmetic tests for Long (&) -// ============================================ - -#[test] -fn test_long_add() { - let output = compile_and_run( - r#" -A& = 100000 -B& = 50000 -PRINT A& + B& -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "150000"); -} - -#[test] -fn test_long_sub() { - let output = compile_and_run( - r#" -A& = 100000 -B& = 30000 -PRINT A& - B& -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "70000"); -} - -#[test] -fn test_long_mul() { - let output = compile_and_run( - r#" -A& = 1000 -B& = 500 -PRINT A& * B& -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "500000"); -} - -#[test] -fn test_long_div() { - let output = compile_and_run( - r#" -A& = 7 -B& = 2 -PRINT A& / B& -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "3.5"); -} - -#[test] -fn test_long_intdiv() { - let output = compile_and_run( - r#" -A& = 100 -B& = 30 -PRINT A& \ B& -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "3"); -} - -#[test] -fn test_long_mod() { - let output = compile_and_run( - r#" -A& = 100 -B& = 30 -PRINT A& MOD B& -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "10"); -} - -#[test] -fn test_long_power() { - let output = compile_and_run( - r#" -A& = 3 -B& = 5 -PRINT A& ^ B& -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "243"); -} - -#[test] -fn test_long_neg() { - let output = compile_and_run( - r#" -A& = 12345 -PRINT -A& -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "-12345"); -} - -// ============================================ -// Same-type arithmetic tests for Single (!) -// ============================================ - -#[test] -fn test_single_add() { - let output = compile_and_run( - r#" -A! = 1.5 -B! = 2.5 -PRINT A! + B! -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "4"); -} - -#[test] -fn test_single_sub() { - let output = compile_and_run( - r#" -A! = 5.5 -B! = 2.25 -PRINT A! - B! -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "3.25"); -} - -#[test] -fn test_single_mul() { - let output = compile_and_run( - r#" -A! = 2.5 -B! = 4.0 -PRINT A! * B! -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "10"); -} - -#[test] -fn test_single_div() { - let output = compile_and_run( - r#" -A! = 10.0 -B! = 4.0 -PRINT A! / B! -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "2.5"); -} - -#[test] -fn test_single_power() { - let output = compile_and_run( - r#" -A! = 2.0 -B! = 3.0 -PRINT A! ^ B! -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "8"); -} - -#[test] -fn test_single_neg() { - let output = compile_and_run( - r#" -A! = 3.14 -PRINT -A! -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "-3.14"); -} - -// ============================================ -// Same-type arithmetic tests for Double (#) -// ============================================ - -#[test] -fn test_double_add() { - let output = compile_and_run( - r#" -A# = 1.5 -B# = 2.5 -PRINT A# + B# -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "4"); -} - #[test] -fn test_double_sub() { +fn test_integer_arithmetic() { + // Tests: Integer (%) add, sub, mul, div, intdiv, mod, power, neg let output = compile_and_run( r#" -A# = 100.75 -B# = 50.25 -PRINT A# - B# +A% = 100: B% = 50: PRINT A% + B% +A% = 100: B% = 30: PRINT A% - B% +A% = 12: B% = 5: PRINT A% * B% +A% = 7: B% = 2: PRINT A% / B% +A% = 17: B% = 5: PRINT A% \ B% +A% = 17: B% = 5: PRINT A% MOD B% +A% = 2: B% = 8: PRINT A% ^ B% +A% = 42: PRINT -A% "#, ) .unwrap(); - assert_eq!(output.trim(), "50.5"); -} - -#[test] -fn test_double_mul() { - let output = compile_and_run( - r#" -A# = 3.5 -B# = 2.0 -PRINT A# * B# -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "7"); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "150", "int add"); + assert_eq!(lines[1], "70", "int sub"); + assert_eq!(lines[2], "60", "int mul"); + assert_eq!(lines[3], "3.5", "int div"); + assert_eq!(lines[4], "3", "int intdiv"); + assert_eq!(lines[5], "2", "int mod"); + assert_eq!(lines[6], "256", "int power"); + assert_eq!(lines[7], "-42", "int neg"); } #[test] -fn test_double_div() { +fn test_long_arithmetic() { + // Tests: Long (&) add, sub, mul, div, intdiv, mod, power, neg let output = compile_and_run( r#" -A# = 15.0 -B# = 4.0 -PRINT A# / B# +A& = 100000: B& = 50000: PRINT A& + B& +A& = 100000: B& = 30000: PRINT A& - B& +A& = 1000: B& = 500: PRINT A& * B& +A& = 7: B& = 2: PRINT A& / B& +A& = 100: B& = 30: PRINT A& \ B& +A& = 100: B& = 30: PRINT A& MOD B& +A& = 3: B& = 5: PRINT A& ^ B& +A& = 12345: PRINT -A& "#, ) .unwrap(); - assert_eq!(output.trim(), "3.75"); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "150000", "long add"); + assert_eq!(lines[1], "70000", "long sub"); + assert_eq!(lines[2], "500000", "long mul"); + assert_eq!(lines[3], "3.5", "long div"); + assert_eq!(lines[4], "3", "long intdiv"); + assert_eq!(lines[5], "10", "long mod"); + assert_eq!(lines[6], "243", "long power"); + assert_eq!(lines[7], "-12345", "long neg"); } #[test] -fn test_double_power() { +fn test_single_arithmetic() { + // Tests: Single (!) add, sub, mul, div, power, neg let output = compile_and_run( r#" -A# = 2.0 -B# = 10.0 -PRINT A# ^ B# +A! = 1.5: B! = 2.5: PRINT A! + B! +A! = 5.5: B! = 2.25: PRINT A! - B! +A! = 2.5: B! = 4.0: PRINT A! * B! +A! = 10.0: B! = 4.0: PRINT A! / B! +A! = 2.0: B! = 3.0: PRINT A! ^ B! +A! = 3.14: PRINT -A! "#, ) .unwrap(); - assert_eq!(output.trim(), "1024"); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "4", "single add"); + assert_eq!(lines[1], "3.25", "single sub"); + assert_eq!(lines[2], "10", "single mul"); + assert_eq!(lines[3], "2.5", "single div"); + assert_eq!(lines[4], "8", "single power"); + assert_eq!(lines[5], "-3.14", "single neg"); } #[test] -fn test_double_neg() { +fn test_double_arithmetic() { + // Tests: Double (#) add, sub, mul, div, power, neg let output = compile_and_run( r#" -A# = 2.71828 -PRINT -A# +A# = 1.5: B# = 2.5: PRINT A# + B# +A# = 100.75: B# = 50.25: PRINT A# - B# +A# = 3.5: B# = 2.0: PRINT A# * B# +A# = 15.0: B# = 4.0: PRINT A# / B# +A# = 2.0: B# = 10.0: PRINT A# ^ B# +A# = 2.71828: PRINT -A# "#, ) .unwrap(); - assert_eq!(output.trim(), "-2.71828"); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "4", "double add"); + assert_eq!(lines[1], "50.5", "double sub"); + assert_eq!(lines[2], "7", "double mul"); + assert_eq!(lines[3], "3.75", "double div"); + assert_eq!(lines[4], "1024", "double power"); + assert_eq!(lines[5], "-2.71828", "double neg"); } diff --git a/tests/arrays/mod.rs b/tests/arrays/mod.rs index f11028c..43d83dc 100644 --- a/tests/arrays/mod.rs +++ b/tests/arrays/mod.rs @@ -1,4 +1,4 @@ -//! Array tests +//! Array tests (consolidated) // Copyright (c) 2025-2026 Jeff Garzik // SPDX-License-Identifier: MIT @@ -6,7 +6,8 @@ use crate::common::{compile_and_run, normalize_output}; #[test] -fn test_dim_single_array() { +fn test_arrays_1d_2d() { + // Test 1D and 2D arrays with various access patterns let output = compile_and_run( r#" DIM A(5) @@ -14,38 +15,11 @@ A(1) = 10 A(3) = 30 PRINT A(1) PRINT A(3) -"#, - ) - .unwrap(); - assert_eq!(normalize_output(&output), "10\n30"); -} - -#[test] -fn test_2d_array() { - let output = compile_and_run( - r#" -DIM A(2, 3) -A(0, 0) = 1 -A(0, 1) = 2 -A(0, 2) = 3 -A(1, 0) = 4 -A(1, 1) = 5 -A(1, 2) = 6 -A(2, 0) = 7 -A(2, 1) = 8 -A(2, 2) = 9 -PRINT A(0, 0) + A(1, 1) + A(2, 2) -"#, - ) - .unwrap(); - // Diagonal sum: 1 + 5 + 9 = 15 - assert_eq!(output.trim(), "15"); -} - -#[test] -fn test_2d_array_loop() { - let output = compile_and_run( - r#" +DIM B(2, 3) +B(0, 0) = 1 +B(1, 1) = 5 +B(2, 2) = 9 +PRINT B(0, 0) + B(1, 1) + B(2, 2) DIM Grid(1, 2) FOR I = 0 TO 1 FOR J = 0 TO 2 @@ -56,9 +30,13 @@ PRINT Grid(0, 0), Grid(0, 1), Grid(0, 2), Grid(1, 0), Grid(1, 1), Grid(1, 2) "#, ) .unwrap(); - // Comma-separated prints with tabs - let values: Vec<&str> = output.split_whitespace().collect(); - assert_eq!(values, vec!["0", "1", "2", "10", "11", "12"]); + let normalized = normalize_output(&output); + let lines: Vec<&str> = normalized.lines().collect(); + assert_eq!(lines[0], "10", "1d a(1)"); + assert_eq!(lines[1], "30", "1d a(3)"); + assert_eq!(lines[2], "15", "2d diagonal sum"); + let values: Vec<&str> = lines[3].split_whitespace().collect(); + assert_eq!(values, vec!["0", "1", "2", "10", "11", "12"], "2d loop"); } #[test] diff --git a/tests/control/mod.rs b/tests/control/mod.rs index c72b545..f9c76df 100644 --- a/tests/control/mod.rs +++ b/tests/control/mod.rs @@ -1,4 +1,4 @@ -//! Control flow tests +//! Control flow tests (consolidated) // Copyright (c) 2025-2026 Jeff Garzik // SPDX-License-Identifier: MIT @@ -6,45 +6,20 @@ use crate::common::compile_and_run; #[test] -fn test_for_loop() { +fn test_for_loops() { + // Test FOR loop, STEP positive, STEP negative let output = compile_and_run( r#" -FOR I = 1 TO 5 - PRINT I -NEXT I -"#, - ) - .unwrap(); - let lines: Vec<&str> = output.trim().lines().collect(); - assert_eq!(lines, vec!["1", "2", "3", "4", "5"]); -} - -#[test] -fn test_for_step_positive() { - let output = compile_and_run( - r#" -FOR I = 0 TO 10 STEP 2 - PRINT I -NEXT I +FOR I = 1 TO 3: PRINT I: NEXT I +FOR I = 0 TO 6 STEP 2: PRINT I: NEXT I +FOR I = 3 TO 1 STEP -1: PRINT I: NEXT I "#, ) .unwrap(); let lines: Vec<&str> = output.trim().lines().collect(); - assert_eq!(lines, vec!["0", "2", "4", "6", "8", "10"]); -} - -#[test] -fn test_for_step_negative() { - let output = compile_and_run( - r#" -FOR I = 5 TO 1 STEP -1 - PRINT I -NEXT I -"#, - ) - .unwrap(); - let lines: Vec<&str> = output.trim().lines().collect(); - assert_eq!(lines, vec!["5", "4", "3", "2", "1"]); + assert_eq!(&lines[0..3], &["1", "2", "3"], "for basic"); + assert_eq!(&lines[3..7], &["0", "2", "4", "6"], "for step+"); + assert_eq!(&lines[7..10], &["3", "2", "1"], "for step-"); } #[test] @@ -64,7 +39,8 @@ WEND } #[test] -fn test_do_loop_while() { +fn test_do_loops() { + // Test DO WHILE, DO UNTIL, DO...LOOP WHILE let output = compile_and_run( r#" X = 1 @@ -72,33 +48,11 @@ DO WHILE X <= 3 PRINT X X = X + 1 LOOP -"#, - ) - .unwrap(); - let lines: Vec<&str> = output.trim().lines().collect(); - assert_eq!(lines, vec!["1", "2", "3"]); -} - -#[test] -fn test_do_loop_until() { - let output = compile_and_run( - r#" X = 1 DO UNTIL X > 3 PRINT X X = X + 1 LOOP -"#, - ) - .unwrap(); - let lines: Vec<&str> = output.trim().lines().collect(); - assert_eq!(lines, vec!["1", "2", "3"]); -} - -#[test] -fn test_do_loop_while_post() { - let output = compile_and_run( - r#" X = 1 DO PRINT X @@ -108,11 +62,14 @@ LOOP WHILE X <= 3 ) .unwrap(); let lines: Vec<&str> = output.trim().lines().collect(); - assert_eq!(lines, vec!["1", "2", "3"]); + assert_eq!(&lines[0..3], &["1", "2", "3"], "do while"); + assert_eq!(&lines[3..6], &["1", "2", "3"], "do until"); + assert_eq!(&lines[6..9], &["1", "2", "3"], "do...loop while"); } #[test] -fn test_if_then_else() { +fn test_if_statements() { + // Test IF/THEN/ELSE and ELSEIF let output = compile_and_run( r#" X = 10 @@ -121,32 +78,12 @@ IF X > 5 THEN ELSE PRINT "small" END IF -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "big"); -} - -#[test] -fn test_if_then_else_false() { - let output = compile_and_run( - r#" X = 3 IF X > 5 THEN PRINT "big" ELSE PRINT "small" END IF -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "small"); -} - -#[test] -fn test_elseif() { - let output = compile_and_run( - r#" X = 2 IF X = 1 THEN PRINT "one" @@ -160,63 +97,49 @@ END IF "#, ) .unwrap(); - assert_eq!(output.trim(), "two"); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "big", "if true"); + assert_eq!(lines[1], "small", "if false"); + assert_eq!(lines[2], "two", "elseif"); } #[test] -fn test_goto() { +fn test_goto_gosub() { + // Test GOTO, GOSUB/RETURN, ON GOTO let output = compile_and_run( r#" 10 PRINT "A" 20 GOTO 40 30 PRINT "B" 40 PRINT "C" -"#, - ) - .unwrap(); - let lines: Vec<&str> = output.trim().lines().collect(); - assert_eq!(lines, vec!["A", "C"]); -} - -#[test] -fn test_gosub_return() { - let output = compile_and_run( - r#" -10 PRINT "start" -20 GOSUB 100 -30 PRINT "end" -40 END +50 GOSUB 100 +60 PRINT "end" +70 X = 2 +80 ON X GOTO 200, 300, 400 +90 PRINT "none" +95 END 100 PRINT "in sub" 110 RETURN -"#, - ) - .unwrap(); - let lines: Vec<&str> = output.trim().lines().collect(); - assert_eq!(lines, vec!["start", "in sub", "end"]); -} - -#[test] -fn test_on_goto() { - let output = compile_and_run( - r#" -10 X = 2 -20 ON X GOTO 100, 200, 300 -30 PRINT "none" -40 END -100 PRINT "first" -110 END -200 PRINT "second" +200 PRINT "first" 210 END -300 PRINT "third" +300 PRINT "second" 310 END +400 PRINT "third" +410 END "#, ) .unwrap(); - assert_eq!(output.trim(), "second"); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "A", "before goto"); + assert_eq!(lines[1], "C", "after goto"); + assert_eq!(lines[2], "in sub", "gosub"); + assert_eq!(lines[3], "end", "after return"); + assert_eq!(lines[4], "second", "on goto"); } #[test] fn test_select_case() { + // Test SELECT CASE and CASE ELSE let output = compile_and_run( r#" X = 2 @@ -230,16 +153,6 @@ SELECT CASE X CASE ELSE PRINT "other" END SELECT -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "two"); -} - -#[test] -fn test_select_case_else() { - let output = compile_and_run( - r#" X = 99 SELECT CASE X CASE 1 @@ -252,11 +165,14 @@ END SELECT "#, ) .unwrap(); - assert_eq!(output.trim(), "other"); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "two", "case match"); + assert_eq!(lines[1], "other", "case else"); } #[test] -fn test_end_statement() { +fn test_end_stop() { + // Test END and STOP statements let output = compile_and_run( r#" PRINT "before" @@ -265,12 +181,9 @@ PRINT "after" "#, ) .unwrap(); - assert_eq!(output.trim(), "before"); -} + assert_eq!(output.trim(), "before", "end"); -#[test] -fn test_stop_statement() { - let output = compile_and_run( + let output2 = compile_and_run( r#" PRINT "before" STOP @@ -278,5 +191,46 @@ PRINT "after" "#, ) .unwrap(); - assert_eq!(output.trim(), "before"); + assert_eq!(output2.trim(), "before", "stop"); +} + +#[test] +fn test_gosub_stress() { + // Test GOSUB with many calls and nested calls + let output = compile_and_run( + r#" +X = 0 +FOR I = 1 TO 500 + GOSUB 100 +NEXT I +PRINT X +GOSUB 200 +PRINT "done" +END + +100 X = X + 1 +RETURN + +200 PRINT "L1 start" +GOSUB 300 +PRINT "L1 end" +RETURN + +300 PRINT "L2 start" +GOSUB 400 +PRINT "L2 end" +RETURN + +400 PRINT "L3" +RETURN +"#, + ) + .unwrap(); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "500", "many gosub"); + assert_eq!( + &lines[1..7], + &["L1 start", "L2 start", "L3", "L2 end", "L1 end", "done"], + "nested gosub" + ); } diff --git a/tests/data/mod.rs b/tests/data/mod.rs index 841c7ea..5c020d6 100644 --- a/tests/data/mod.rs +++ b/tests/data/mod.rs @@ -1,4 +1,4 @@ -//! DATA/READ/RESTORE tests +//! DATA/READ/RESTORE tests (consolidated) // Copyright (c) 2025-2026 Jeff Garzik // SPDX-License-Identifier: MIT @@ -6,7 +6,8 @@ use crate::common::compile_and_run; #[test] -fn test_data_read() { +fn test_data_read_restore() { + // Test DATA/READ and RESTORE let output = compile_and_run( r#" DATA 10, 20, 30 @@ -14,25 +15,14 @@ READ A READ B READ C PRINT A + B + C -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "60"); -} - -#[test] -fn test_data_restore() { - let output = compile_and_run( - r#" DATA 5, 10 -READ A -READ B RESTORE -READ C -PRINT A + B + C +READ D +PRINT D "#, ) .unwrap(); - // A=5, B=10, C=5 (restored) - assert_eq!(output.trim(), "20"); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "60", "data read sum"); + assert_eq!(lines[1], "10", "restore reads first data"); } diff --git a/tests/input/mod.rs b/tests/input/mod.rs index 123a4bc..8a3376d 100644 --- a/tests/input/mod.rs +++ b/tests/input/mod.rs @@ -1,4 +1,4 @@ -//! INPUT statement tests +//! INPUT statement tests (consolidated) // Copyright (c) 2025-2026 Jeff Garzik // SPDX-License-Identifier: MIT @@ -6,7 +6,8 @@ use crate::common::compile_and_run_with_stdin; #[test] -fn test_input_number() { +fn test_input() { + // Test INPUT with number and string let output = compile_and_run_with_stdin( r#" INPUT X @@ -15,12 +16,9 @@ PRINT X * 2 "21\n", ) .unwrap(); - assert!(output.contains("42")); -} + assert!(output.contains("42"), "number input"); -#[test] -fn test_input_string() { - let output = compile_and_run_with_stdin( + let output2 = compile_and_run_with_stdin( r#" INPUT A$ PRINT "Hello, "; A$ @@ -28,7 +26,7 @@ PRINT "Hello, "; A$ "World\n", ) .unwrap(); - assert!(output.contains("Hello, World")); + assert!(output2.contains("Hello, World"), "string input"); } #[test] diff --git a/tests/math/mod.rs b/tests/math/mod.rs index c4e1a12..68f062b 100644 --- a/tests/math/mod.rs +++ b/tests/math/mod.rs @@ -1,4 +1,4 @@ -//! Math function tests +//! Math function tests (consolidated) // Copyright (c) 2025-2026 Jeff Garzik // SPDX-License-Identifier: MIT @@ -6,651 +6,215 @@ use crate::common::compile_and_run; #[test] -fn test_sqr_function() { - let output = compile_and_run("PRINT SQR(16)").unwrap(); - assert_eq!(output.trim(), "4"); -} - -#[test] -fn test_abs_function() { - let output = compile_and_run("PRINT ABS(-42)").unwrap(); - assert_eq!(output.trim(), "42"); -} - -#[test] -fn test_int_function() { - let output = compile_and_run("PRINT INT(3.7)").unwrap(); - assert_eq!(output.trim(), "3"); -} - -#[test] -fn test_fix_function() { - // FIX truncates toward zero, INT floors - let output = compile_and_run("PRINT FIX(-3.7)").unwrap(); - assert_eq!(output.trim(), "-3"); -} - -#[test] -fn test_sgn_function() { +fn test_sqr() { + // SQR with various input types let output = compile_and_run( r#" -PRINT SGN(-5) -PRINT SGN(0) -PRINT SGN(5) +PRINT SQR(16) +A% = 25: PRINT SQR(A%) +A& = 10000: PRINT SQR(A&) +A! = 2.25: PRINT SQR(A!) +A# = 2.25: PRINT SQR(A#) "#, ) .unwrap(); let lines: Vec<&str> = output.trim().lines().collect(); - assert_eq!(lines, vec!["-1", "0", "1"]); + assert_eq!(lines[0], "4", "sqr literal"); + assert_eq!(lines[1], "5", "sqr integer"); + assert_eq!(lines[2], "100", "sqr long"); + assert_eq!(lines[3], "1.5", "sqr single"); + assert_eq!(lines[4], "1.5", "sqr double"); } #[test] -fn test_sin_cos() { - let output = compile_and_run("PRINT INT(SIN(0) * 100), INT(COS(0) * 100)").unwrap(); - // sin(0) = 0, cos(0) = 1 - let values: Vec<&str> = output.split_whitespace().collect(); - assert_eq!(values, vec!["0", "100"]); -} - -#[test] -fn test_tan_atn() { - let output = compile_and_run("PRINT INT(TAN(0) * 100), INT(ATN(0) * 100)").unwrap(); - // tan(0) = 0, atn(0) = 0 - let values: Vec<&str> = output.split_whitespace().collect(); - assert_eq!(values, vec!["0", "0"]); -} - -#[test] -fn test_exp_log() { - let output = compile_and_run("PRINT INT(EXP(0)), INT(LOG(1))").unwrap(); - // exp(0) = 1, log(1) = 0 - let values: Vec<&str> = output.split_whitespace().collect(); - assert_eq!(values, vec!["1", "0"]); -} - -#[test] -fn test_rnd_function() { - // RND returns a value between 0 and 1 +fn test_abs() { + // ABS with various input types let output = compile_and_run( r#" -X = RND(1) -IF X >= 0 AND X < 1 THEN PRINT "ok" -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "ok"); -} - -#[test] -fn test_timer_function() { - // TIMER returns seconds since midnight; just verify it returns a number - let output = compile_and_run( - r#" -T = TIMER -IF T >= 0 THEN PRINT "ok" -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "ok"); -} - -// ============================================ -// Math functions with Integer (%) input -// ============================================ - -#[test] -fn test_sqr_integer_input() { - let output = compile_and_run( - r#" -A% = 25 -PRINT SQR(A%) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "5"); -} - -#[test] -fn test_abs_integer_input() { - let output = compile_and_run( - r#" -A% = -42 -PRINT ABS(A%) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "42"); -} - -#[test] -fn test_sgn_integer_input() { - let output = compile_and_run( - r#" -A% = -5 -B% = 0 -C% = 5 -PRINT SGN(A%) -PRINT SGN(B%) -PRINT SGN(C%) +PRINT ABS(-42) +A% = -42: PRINT ABS(A%) +A& = -100000: PRINT ABS(A&) +A! = -3.14: PRINT ABS(A!) +A# = -3.14159: PRINT ABS(A#) "#, ) .unwrap(); let lines: Vec<&str> = output.trim().lines().collect(); - assert_eq!(lines, vec!["-1", "0", "1"]); -} - -#[test] -fn test_sin_integer_input() { - let output = compile_and_run( - r#" -A% = 0 -PRINT INT(SIN(A%) * 100) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "0"); + assert_eq!(lines[0], "42", "abs literal"); + assert_eq!(lines[1], "42", "abs integer"); + assert_eq!(lines[2], "100000", "abs long"); + assert_eq!(lines[3], "3.14", "abs single"); + assert_eq!(lines[4], "3.14159", "abs double"); } #[test] -fn test_cos_integer_input() { +fn test_int_fix() { + // INT floors, FIX truncates toward zero let output = compile_and_run( r#" -A% = 0 -PRINT INT(COS(A%) * 100) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "100"); -} - -#[test] -fn test_exp_integer_input() { - let output = compile_and_run( - r#" -A% = 0 -PRINT INT(EXP(A%)) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "1"); -} - -#[test] -fn test_log_integer_input() { - let output = compile_and_run( - r#" -A% = 1 -PRINT INT(LOG(A%)) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "0"); -} - -// ============================================ -// Math functions with Long (&) input -// ============================================ - -#[test] -fn test_sqr_long_input() { - let output = compile_and_run( - r#" -A& = 10000 -PRINT SQR(A&) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "100"); -} - -#[test] -fn test_abs_long_input() { - let output = compile_and_run( - r#" -A& = -100000 -PRINT ABS(A&) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "100000"); -} - -#[test] -fn test_sgn_long_input() { - let output = compile_and_run( - r#" -A& = -50000 -B& = 0 -C& = 50000 -PRINT SGN(A&) -PRINT SGN(B&) -PRINT SGN(C&) +PRINT INT(3.7) +A! = 3.7: PRINT INT(A!) +A# = 3.7: PRINT INT(A#) +PRINT FIX(-3.7) +A! = -3.7: PRINT FIX(A!) +A# = -3.7: PRINT FIX(A#) "#, ) .unwrap(); let lines: Vec<&str> = output.trim().lines().collect(); - assert_eq!(lines, vec!["-1", "0", "1"]); -} - -#[test] -fn test_sin_long_input() { - let output = compile_and_run( - r#" -A& = 0 -PRINT INT(SIN(A&) * 100) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "0"); -} - -#[test] -fn test_cos_long_input() { - let output = compile_and_run( - r#" -A& = 0 -PRINT INT(COS(A&) * 100) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "100"); -} - -// ============================================ -// Math functions with Single (!) input -// ============================================ - -#[test] -fn test_sqr_single_input() { - let output = compile_and_run( - r#" -A! = 2.25 -PRINT SQR(A!) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "1.5"); -} - -#[test] -fn test_abs_single_input() { - let output = compile_and_run( - r#" -A! = -3.14 -PRINT ABS(A!) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "3.14"); -} - -#[test] -fn test_int_single_input() { - let output = compile_and_run( - r#" -A! = 3.7 -PRINT INT(A!) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "3"); + assert_eq!(lines[0], "3", "int literal"); + assert_eq!(lines[1], "3", "int single"); + assert_eq!(lines[2], "3", "int double"); + assert_eq!(lines[3], "-3", "fix literal"); + assert_eq!(lines[4], "-3", "fix single"); + assert_eq!(lines[5], "-3", "fix double"); } #[test] -fn test_fix_single_input() { +fn test_sgn() { + // SGN with various input types let output = compile_and_run( r#" -A! = -3.7 -PRINT FIX(A!) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "-3"); -} - -#[test] -fn test_sgn_single_input() { - let output = compile_and_run( - r#" -A! = -2.5 -B! = 0.0 -C! = 2.5 -PRINT SGN(A!) -PRINT SGN(B!) -PRINT SGN(C!) +PRINT SGN(-5) +PRINT SGN(0) +PRINT SGN(5) +A% = -5: PRINT SGN(A%) +A& = -50000: PRINT SGN(A&) +A! = -2.5: PRINT SGN(A!) +A# = -2.5: PRINT SGN(A#) "#, ) .unwrap(); let lines: Vec<&str> = output.trim().lines().collect(); - assert_eq!(lines, vec!["-1", "0", "1"]); -} - -#[test] -fn test_sin_single_input() { - let output = compile_and_run( - r#" -A! = 0.0 -PRINT INT(SIN(A!) * 100) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "0"); + assert_eq!(lines[0], "-1", "sgn neg"); + assert_eq!(lines[1], "0", "sgn zero"); + assert_eq!(lines[2], "1", "sgn pos"); + assert_eq!(lines[3], "-1", "sgn integer"); + assert_eq!(lines[4], "-1", "sgn long"); + assert_eq!(lines[5], "-1", "sgn single"); + assert_eq!(lines[6], "-1", "sgn double"); } #[test] -fn test_cos_single_input() { +fn test_trig_sin_cos() { + // SIN and COS with various input types let output = compile_and_run( r#" -A! = 0.0 -PRINT INT(COS(A!) * 100) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "100"); -} - -#[test] -fn test_tan_single_input() { - let output = compile_and_run( - r#" -A! = 0.0 -PRINT INT(TAN(A!) * 100) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "0"); -} - -#[test] -fn test_atn_single_input() { - let output = compile_and_run( - r#" -A! = 0.0 -PRINT INT(ATN(A!) * 100) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "0"); -} - -#[test] -fn test_exp_single_input() { - let output = compile_and_run( - r#" -A! = 0.0 -PRINT INT(EXP(A!)) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "1"); -} - -#[test] -fn test_log_single_input() { - let output = compile_and_run( - r#" -A! = 1.0 -PRINT INT(LOG(A!)) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "0"); -} - -// ============================================ -// Math functions with Double (#) input -// ============================================ - -#[test] -fn test_sqr_double_input() { - let output = compile_and_run( - r#" -A# = 2.25 -PRINT SQR(A#) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "1.5"); -} - -#[test] -fn test_abs_double_input() { - let output = compile_and_run( - r#" -A# = -3.14159 -PRINT ABS(A#) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "3.14159"); -} - -#[test] -fn test_int_double_input() { - let output = compile_and_run( - r#" -A# = 3.7 -PRINT INT(A#) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "3"); -} - -#[test] -fn test_fix_double_input() { - let output = compile_and_run( - r#" -A# = -3.7 -PRINT FIX(A#) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "-3"); -} - -#[test] -fn test_sgn_double_input() { - let output = compile_and_run( - r#" -A# = -2.5 -B# = 0.0 -C# = 2.5 -PRINT SGN(A#) -PRINT SGN(B#) -PRINT SGN(C#) +PRINT INT(SIN(0) * 100), INT(COS(0) * 100) +A% = 0: PRINT INT(SIN(A%) * 100) +A% = 0: PRINT INT(COS(A%) * 100) +A& = 0: PRINT INT(SIN(A&) * 100) +A& = 0: PRINT INT(COS(A&) * 100) +A! = 0.0: PRINT INT(SIN(A!) * 100) +A! = 0.0: PRINT INT(COS(A!) * 100) +A# = 0.0: PRINT INT(SIN(A#) * 100) +A# = 0.0: PRINT INT(COS(A#) * 100) "#, ) .unwrap(); let lines: Vec<&str> = output.trim().lines().collect(); - assert_eq!(lines, vec!["-1", "0", "1"]); + let values: Vec<&str> = lines[0].split_whitespace().collect(); + assert_eq!(values, vec!["0", "100"], "sin/cos literals"); + assert_eq!(lines[1], "0", "sin integer"); + assert_eq!(lines[2], "100", "cos integer"); + assert_eq!(lines[3], "0", "sin long"); + assert_eq!(lines[4], "100", "cos long"); + assert_eq!(lines[5], "0", "sin single"); + assert_eq!(lines[6], "100", "cos single"); + assert_eq!(lines[7], "0", "sin double"); + assert_eq!(lines[8], "100", "cos double"); } #[test] -fn test_sin_double_input() { +fn test_trig_tan_atn() { + // TAN and ATN with various input types let output = compile_and_run( r#" -A# = 0.0 -PRINT INT(SIN(A#) * 100) +PRINT INT(TAN(0) * 100), INT(ATN(0) * 100) +A! = 0.0: PRINT INT(TAN(A!) * 100) +A! = 0.0: PRINT INT(ATN(A!) * 100) +A# = 0.0: PRINT INT(TAN(A#) * 100) +A# = 0.0: PRINT INT(ATN(A#) * 100) "#, ) .unwrap(); - assert_eq!(output.trim(), "0"); -} - -#[test] -fn test_cos_double_input() { - let output = compile_and_run( - r#" -A# = 0.0 -PRINT INT(COS(A#) * 100) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "100"); -} - -#[test] -fn test_tan_double_input() { - let output = compile_and_run( - r#" -A# = 0.0 -PRINT INT(TAN(A#) * 100) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "0"); -} - -#[test] -fn test_atn_double_input() { - let output = compile_and_run( - r#" -A# = 0.0 -PRINT INT(ATN(A#) * 100) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "0"); -} - -#[test] -fn test_exp_double_input() { - let output = compile_and_run( - r#" -A# = 0.0 -PRINT INT(EXP(A#)) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "1"); -} - -#[test] -fn test_log_double_input() { - let output = compile_and_run( - r#" -A# = 1.0 -PRINT INT(LOG(A#)) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "0"); -} - -// ============================================ -// Type conversion functions with various inputs -// ============================================ - -#[test] -fn test_cint_integer_input() { - let output = compile_and_run( - r#" -A% = 42 -PRINT CINT(A%) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "42"); -} - -#[test] -fn test_cint_long_input() { - let output = compile_and_run( - r#" -A& = 12345 -PRINT CINT(A&) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "12345"); -} - -#[test] -fn test_cint_single_input() { - let output = compile_and_run( - r#" -A! = 3.7 -PRINT CINT(A!) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "4"); -} - -#[test] -fn test_clng_single_input() { - let output = compile_and_run( - r#" -A! = 3.7 -PRINT CLNG(A!) -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "4"); -} - -#[test] -fn test_csng_integer_input() { - let output = compile_and_run( - r#" -A% = 42 -B! = CSNG(A%) -PRINT B! -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "42"); -} - -#[test] -fn test_csng_long_input() { - let output = compile_and_run( - r#" -A& = 12345 -B! = CSNG(A&) -PRINT B! -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "12345"); + let lines: Vec<&str> = output.trim().lines().collect(); + let values: Vec<&str> = lines[0].split_whitespace().collect(); + assert_eq!(values, vec!["0", "0"], "tan/atn literals"); + assert_eq!(lines[1], "0", "tan single"); + assert_eq!(lines[2], "0", "atn single"); + assert_eq!(lines[3], "0", "tan double"); + assert_eq!(lines[4], "0", "atn double"); } #[test] -fn test_cdbl_integer_input() { +fn test_exp_log() { + // EXP and LOG with various input types let output = compile_and_run( r#" -A% = 42 -B# = CDBL(A%) -PRINT B# +PRINT INT(EXP(0)), INT(LOG(1)) +A% = 0: PRINT INT(EXP(A%)) +A% = 1: PRINT INT(LOG(A%)) +A! = 0.0: PRINT INT(EXP(A!)) +A! = 1.0: PRINT INT(LOG(A!)) +A# = 0.0: PRINT INT(EXP(A#)) +A# = 1.0: PRINT INT(LOG(A#)) "#, ) .unwrap(); - assert_eq!(output.trim(), "42"); + let lines: Vec<&str> = output.trim().lines().collect(); + let values: Vec<&str> = lines[0].split_whitespace().collect(); + assert_eq!(values, vec!["1", "0"], "exp/log literals"); + assert_eq!(lines[1], "1", "exp integer"); + assert_eq!(lines[2], "0", "log integer"); + assert_eq!(lines[3], "1", "exp single"); + assert_eq!(lines[4], "0", "log single"); + assert_eq!(lines[5], "1", "exp double"); + assert_eq!(lines[6], "0", "log double"); } #[test] -fn test_cdbl_long_input() { +fn test_rnd_timer() { + // RND returns 0-1, TIMER returns seconds since midnight let output = compile_and_run( r#" -A& = 12345 -B# = CDBL(A&) -PRINT B# +X = RND(1) +IF X >= 0 AND X < 1 THEN PRINT "rnd-ok" +T = TIMER +IF T >= 0 THEN PRINT "timer-ok" "#, ) .unwrap(); - assert_eq!(output.trim(), "12345"); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "rnd-ok"); + assert_eq!(lines[1], "timer-ok"); } #[test] -fn test_cdbl_single_input() { +fn test_type_conversions() { + // CINT, CLNG, CSNG, CDBL with various inputs let output = compile_and_run( r#" -A! = 3.5 -B# = CDBL(A!) -PRINT B# +A% = 42: PRINT CINT(A%) +A& = 12345: PRINT CINT(A&) +A! = 3.7: PRINT CINT(A!) +A! = 3.7: PRINT CLNG(A!) +A% = 42: B! = CSNG(A%): PRINT B! +A& = 12345: B! = CSNG(A&): PRINT B! +A% = 42: B# = CDBL(A%): PRINT B# +A& = 12345: B# = CDBL(A&): PRINT B# +A! = 3.5: B# = CDBL(A!): PRINT B# "#, ) .unwrap(); - assert_eq!(output.trim(), "3.5"); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "42", "cint integer"); + assert_eq!(lines[1], "12345", "cint long"); + assert_eq!(lines[2], "4", "cint single"); + assert_eq!(lines[3], "4", "clng single"); + assert_eq!(lines[4], "42", "csng integer"); + assert_eq!(lines[5], "12345", "csng long"); + assert_eq!(lines[6], "42", "cdbl integer"); + assert_eq!(lines[7], "12345", "cdbl long"); + assert_eq!(lines[8], "3.5", "cdbl single"); } diff --git a/tests/print/mod.rs b/tests/print/mod.rs index 2adfd17..dd444bc 100644 --- a/tests/print/mod.rs +++ b/tests/print/mod.rs @@ -1,4 +1,4 @@ -//! Print statement tests +//! Print statement tests (consolidated) // Copyright (c) 2025-2026 Jeff Garzik // SPDX-License-Identifier: MIT @@ -6,21 +6,12 @@ use crate::common::compile_and_run; #[test] -fn test_hello_world() { - let output = compile_and_run(r#"PRINT "Hello, World!""#).unwrap(); - assert_eq!(output.trim(), "Hello, World!"); -} - -#[test] -fn test_print_number() { - let output = compile_and_run("PRINT 42").unwrap(); - assert_eq!(output.trim(), "42"); -} - -#[test] -fn test_multiple_prints() { +fn test_print_combined() { + // Test print with strings, numbers, and multiple statements let output = compile_and_run( r#" +PRINT "Hello, World!" +PRINT 42 PRINT "A" PRINT "B" PRINT "C" @@ -28,5 +19,9 @@ PRINT "C" ) .unwrap(); let lines: Vec<&str> = output.trim().lines().collect(); - assert_eq!(lines, vec!["A", "B", "C"]); + assert_eq!(lines[0], "Hello, World!", "string"); + assert_eq!(lines[1], "42", "number"); + assert_eq!(lines[2], "A", "multi-a"); + assert_eq!(lines[3], "B", "multi-b"); + assert_eq!(lines[4], "C", "multi-c"); } diff --git a/tests/procedures/mod.rs b/tests/procedures/mod.rs index 15299ec..bd25f88 100644 --- a/tests/procedures/mod.rs +++ b/tests/procedures/mod.rs @@ -1,4 +1,4 @@ -//! Function and subroutine tests +//! Function and subroutine tests (consolidated) // Copyright (c) 2025-2026 Jeff Garzik // SPDX-License-Identifier: MIT @@ -6,22 +6,31 @@ use crate::common::compile_and_run; #[test] -fn test_function_definition() { +fn test_basic_procedures() { + // Test function definition, sub definition, and sub with params let output = compile_and_run( r#" FUNCTION Double(X) Double = X * 2 END FUNCTION +SUB PrintSum(A, B) + PRINT A + B +END SUB + PRINT Double(21) +PrintSum(10, 20) "#, ) .unwrap(); - assert_eq!(output.trim(), "42"); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "42", "function"); + assert_eq!(lines[1], "30", "sub with params"); } #[test] -fn test_sub_definition() { +fn test_sub_no_params() { + // Test subroutine without parameters let output = compile_and_run( r#" PrintHello @@ -39,17 +48,57 @@ END SUB } #[test] -fn test_sub_with_params() { +fn test_many_params() { + // Test procedures with 7, 8, and 10 parameters (overflow handling) let output = compile_and_run( r#" -AddPrint(10, 20) -END - -SUB AddPrint(A, B) - PRINT A + B +SUB Sum7(A, B, C, D, E, F, G) + PRINT A + B + C + D + E + F + G END SUB + +FUNCTION Sum8(A, B, C, D, E, F, G, H) + Sum8 = A + B + C + D + E + F + G + H +END FUNCTION + +FUNCTION Sum10(A, B, C, D, E, F, G, H, I, J) + Sum10 = A + B + C + D + E + F + G + H + I + J +END FUNCTION + +Sum7(1, 2, 3, 4, 5, 6, 7) +PRINT Sum8(1, 2, 3, 4, 5, 6, 7, 8) +PRINT Sum10(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) +"#, + ) + .unwrap(); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "28", "7 params: 1+2+3+4+5+6+7"); + assert_eq!(lines[1], "36", "8 params: 1+..+8"); + assert_eq!(lines[2], "55", "10 params: 1+..+10"); +} + +#[test] +fn test_nested_calls() { + // Test nested function calls in arguments + let output = compile_and_run( + r#" +FUNCTION Add(A, B) + Add = A + B +END FUNCTION + +FUNCTION Mul(A, B) + Mul = A * B +END FUNCTION + +FUNCTION AddThree(A, B, C) + AddThree = A + B + C +END FUNCTION + +PRINT Add(Mul(2, 3), Mul(4, 5)) +PRINT AddThree(Mul(2, 3), Mul(4, 5), Mul(6, 7)) "#, ) .unwrap(); - assert_eq!(output.trim(), "30"); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "26", "nested: 2*3 + 4*5 = 6+20"); + assert_eq!(lines[1], "68", "nested three: 6+20+42"); } diff --git a/tests/strings/mod.rs b/tests/strings/mod.rs index ebe0d63..aab4fea 100644 --- a/tests/strings/mod.rs +++ b/tests/strings/mod.rs @@ -1,4 +1,4 @@ -//! String function tests +//! String function tests (consolidated) // Copyright (c) 2025-2026 Jeff Garzik // SPDX-License-Identifier: MIT @@ -6,59 +6,72 @@ use crate::common::compile_and_run; #[test] -fn test_len_function() { - let output = compile_and_run(r#"PRINT LEN("Hello")"#).unwrap(); - assert_eq!(output.trim(), "5"); -} - -#[test] -fn test_left_right() { +fn test_string_functions() { + // Test LEN, LEFT$, RIGHT$, MID$, CHR$, ASC, VAL, STR$, INSTR let output = compile_and_run( r#" +PRINT LEN("Hello") PRINT LEFT$("Hello", 2) PRINT RIGHT$("Hello", 2) +PRINT MID$("Hello", 2, 3) +PRINT CHR$(65) +PRINT ASC("A") +X = VAL("42"): PRINT X + 8 +PRINT STR$(100) +PRINT INSTR("Hello World", "World") "#, ) .unwrap(); let lines: Vec<&str> = output.trim().lines().collect(); - assert_eq!(lines, vec!["He", "lo"]); -} - -#[test] -fn test_mid_function() { - let output = compile_and_run(r#"PRINT MID$("Hello", 2, 3)"#).unwrap(); - assert_eq!(output.trim(), "ell"); + assert_eq!(lines[0], "5", "len"); + assert_eq!(lines[1], "He", "left$"); + assert_eq!(lines[2], "lo", "right$"); + assert_eq!(lines[3], "ell", "mid$"); + assert_eq!(lines[4], "A", "chr$"); + assert_eq!(lines[5], "65", "asc"); + assert_eq!(lines[6], "50", "val"); + assert_eq!(lines[7], "100", "str$"); + assert_eq!(lines[8], "7", "instr"); } #[test] -fn test_chr_asc() { +fn test_nested_string_calls() { + // Test LEFT$, RIGHT$, MID$ with nested function calls let output = compile_and_run( r#" -PRINT CHR$(65) -PRINT ASC("A") +FUNCTION GetStart() + GetStart = 2 +END FUNCTION + +FUNCTION GetLen() + GetLen = 3 +END FUNCTION + +A$ = "HELLO" +B$ = "WORLD" +PRINT LEFT$(A$ + B$, LEN(A$)) +PRINT RIGHT$(A$ + B$, LEN(B$)) +PRINT MID$("ABCDEF", GetStart(), GetLen()) "#, ) .unwrap(); let lines: Vec<&str> = output.trim().lines().collect(); - assert_eq!(lines, vec!["A", "65"]); + assert_eq!(lines[0], "HELLO", "left$ with len()"); + assert_eq!(lines[1], "WORLD", "right$ with len()"); + assert_eq!(lines[2], "BCD", "mid$ with functions"); } #[test] -fn test_val_str() { +fn test_string_concat_multiple() { + // Test string concatenation with multiple operands let output = compile_and_run( r#" -X = VAL("42") -PRINT X + 8 -PRINT STR$(100) +A$ = "Hello" +B$ = " " +C$ = "World" +PRINT A$ + B$ + C$ "#, ) .unwrap(); - let lines: Vec<&str> = output.trim().lines().collect(); - assert_eq!(lines, vec!["50", "100"]); -} - -#[test] -fn test_instr_function() { - let output = compile_and_run(r#"PRINT INSTR("Hello World", "World")"#).unwrap(); - assert_eq!(output.trim(), "7"); + assert_eq!(output.trim(), "Hello World"); } diff --git a/tests/types/mod.rs b/tests/types/mod.rs index a10a7ca..4e2730b 100644 --- a/tests/types/mod.rs +++ b/tests/types/mod.rs @@ -5,676 +5,196 @@ use crate::common::compile_and_run; -// === Conversion Function Tests === - #[test] -fn test_cint_clng() { +fn test_type_conversions() { + // CINT, CLNG, CSNG, CDBL conversion functions let output = compile_and_run( r#" PRINT CINT(3.7) PRINT CLNG(3.7) +X! = CSNG(3): PRINT X! +Y# = CDBL(3): PRINT Y# "#, ) .unwrap(); let lines: Vec<&str> = output.trim().lines().collect(); - assert_eq!(lines, vec!["4", "4"]); -} - -#[test] -fn test_csng_cdbl() { - // These convert to float types; test that they work - let output = compile_and_run( - r#" -X! = CSNG(3) -Y# = CDBL(3) -PRINT X! + Y# -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "6"); -} - -// === Type Promotion Tests === - -#[test] -fn test_type_promotion_integer_long() { - // Integer + Long should promote to Long - let output = compile_and_run( - r#" -A% = 100 -B& = 200 -PRINT A% + B& -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "300"); -} - -#[test] -fn test_type_promotion_integer_single() { - // Integer + Single should promote to Single - let output = compile_and_run( - r#" -A% = 10 -B! = 2.5 -PRINT A% + B! -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "12.5"); -} - -#[test] -fn test_type_promotion_integer_double() { - // Integer + Double should promote to Double - let output = compile_and_run( - r#" -A% = 10 -B# = 2.5 -PRINT A% + B# -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "12.5"); -} - -#[test] -fn test_type_promotion_long_single() { - // Long + Single should promote to Single - let output = compile_and_run( - r#" -A& = 100 -B! = 0.5 -PRINT A& + B! -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "100.5"); -} - -#[test] -fn test_type_promotion_long_double() { - // Long + Double should promote to Double - let output = compile_and_run( - r#" -A& = 100 -B# = 0.25 -PRINT A& + B# -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "100.25"); -} - -#[test] -fn test_type_promotion_single_double() { - // Single + Double should promote to Double - let output = compile_and_run( - r#" -A! = 1.5 -B# = 2.5 -PRINT A! + B# -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "4"); -} - -// === Cross-Type Assignment Tests === - -#[test] -fn test_cross_type_assignment_int_to_long() { - // Assign Integer to Long variable - let output = compile_and_run( - r#" -A% = 42 -B& = A% -PRINT B& -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "42"); -} - -#[test] -fn test_cross_type_assignment_int_to_double() { - // Assign Integer to Double variable - let output = compile_and_run( - r#" -A% = 42 -B# = A% -PRINT B# -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "42"); -} - -#[test] -fn test_cross_type_assignment_double_to_int() { - // Assign Double to Integer variable (truncation) - let output = compile_and_run( - r#" -A# = 3.7 -B% = A# -PRINT B% -"#, - ) - .unwrap(); - // Truncates toward zero - assert_eq!(output.trim(), "3"); + assert_eq!(lines[0], "4", "cint rounds"); + assert_eq!(lines[1], "4", "clng rounds"); + assert_eq!(lines[2], "3", "csng"); + assert_eq!(lines[3], "3", "cdbl"); } -// === Truncation Tests === - #[test] -fn test_truncation_positive() { - // Positive float truncation +fn test_truncation_and_assignment() { + // Truncation and cross-type assignments let output = compile_and_run( r#" PRINT CINT(3.1) PRINT CINT(3.5) PRINT CINT(3.9) -"#, - ) - .unwrap(); - let lines: Vec<&str> = output.trim().lines().collect(); - // CINT uses banker's rounding (round half to even) - assert_eq!(lines[0], "3"); - assert_eq!(lines[1], "4"); - assert_eq!(lines[2], "4"); -} - -#[test] -fn test_truncation_negative() { - // Negative float truncation - let output = compile_and_run( - r#" PRINT CINT(-3.1) PRINT CINT(-3.5) PRINT CINT(-3.9) +A% = 42: B& = A%: PRINT B& +A% = 42: B# = A%: PRINT B# +A# = 3.7: B% = A#: PRINT B% "#, ) .unwrap(); let lines: Vec<&str> = output.trim().lines().collect(); - // CINT rounds toward nearest - assert_eq!(lines[0], "-3"); - assert_eq!(lines[1], "-4"); - assert_eq!(lines[2], "-4"); -} - -// === Division Tests === - -#[test] -fn test_division_produces_double() { - // Division (/) always produces Double - let output = compile_and_run( - r#" -A% = 7 -B% = 2 -PRINT A% / B% -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "3.5"); -} - -#[test] -fn test_integer_division() { - // Integer division (\) produces Long - let output = compile_and_run( - r#" -A% = 7 -B% = 2 -PRINT A% \ B% -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "3"); + assert_eq!(lines[0], "3", "cint 3.1"); + assert_eq!(lines[1], "4", "cint 3.5"); + assert_eq!(lines[2], "4", "cint 3.9"); + assert_eq!(lines[3], "-3", "cint -3.1"); + assert_eq!(lines[4], "-4", "cint -3.5"); + assert_eq!(lines[5], "-4", "cint -3.9"); + assert_eq!(lines[6], "42", "int to long"); + assert_eq!(lines[7], "42", "int to double"); + assert_eq!(lines[8], "3", "double to int truncates"); } -// === Complex Expression Tests === - #[test] -fn test_mixed_expression_complex() { - // Complex expression with multiple type promotions +fn test_division_types() { + // Division (/) always produces Double, integer division (\) produces Long let output = compile_and_run( r#" -A% = 10 -B& = 20 -C! = 0.5 -D# = 100.0 -PRINT A% + B& * C! + D# +A% = 7: B% = 2: PRINT A% / B% +A% = 7: B% = 2: PRINT A% \ B% "#, ) .unwrap(); - // 10 + 20*0.5 + 100 = 10 + 10 + 100 = 120 - assert_eq!(output.trim(), "120"); -} - -// ============================================ -// Cross-type subtraction tests -// ============================================ - -#[test] -fn test_type_promotion_sub_integer_long() { - let output = compile_and_run( - r#" -A% = 50 -B& = 20 -PRINT A% - B& -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "30"); -} - -#[test] -fn test_type_promotion_sub_integer_single() { - let output = compile_and_run( - r#" -A% = 10 -B! = 2.5 -PRINT A% - B! -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "7.5"); -} - -#[test] -fn test_type_promotion_sub_integer_double() { - let output = compile_and_run( - r#" -A% = 10 -B# = 3.25 -PRINT A% - B# -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "6.75"); -} - -#[test] -fn test_type_promotion_sub_long_single() { - let output = compile_and_run( - r#" -A& = 100 -B! = 0.5 -PRINT A& - B! -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "99.5"); -} - -#[test] -fn test_type_promotion_sub_long_double() { - let output = compile_and_run( - r#" -A& = 100 -B# = 0.25 -PRINT A& - B# -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "99.75"); -} - -#[test] -fn test_type_promotion_sub_single_double() { - let output = compile_and_run( - r#" -A! = 5.5 -B# = 2.25 -PRINT A! - B# -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "3.25"); -} - -// ============================================ -// Cross-type multiplication tests -// ============================================ - -#[test] -fn test_type_promotion_mul_integer_long() { - let output = compile_and_run( - r#" -A% = 10 -B& = 20 -PRINT A% * B& -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "200"); -} - -#[test] -fn test_type_promotion_mul_integer_single() { - let output = compile_and_run( - r#" -A% = 4 -B! = 2.5 -PRINT A% * B! -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "10"); -} - -#[test] -fn test_type_promotion_mul_integer_double() { - let output = compile_and_run( - r#" -A% = 3 -B# = 2.5 -PRINT A% * B# -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "7.5"); -} - -#[test] -fn test_type_promotion_mul_long_single() { - let output = compile_and_run( - r#" -A& = 100 -B! = 0.5 -PRINT A& * B! -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "50"); -} - -#[test] -fn test_type_promotion_mul_long_double() { - let output = compile_and_run( - r#" -A& = 100 -B# = 0.25 -PRINT A& * B# -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "25"); -} - -#[test] -fn test_type_promotion_mul_single_double() { - let output = compile_and_run( - r#" -A! = 2.5 -B# = 4.0 -PRINT A! * B# -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "10"); -} - -// ============================================ -// Cross-type division tests (always produces Double) -// ============================================ - -#[test] -fn test_type_promotion_div_integer_long() { - let output = compile_and_run( - r#" -A% = 7 -B& = 2 -PRINT A% / B& -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "3.5"); -} - -#[test] -fn test_type_promotion_div_integer_single() { - let output = compile_and_run( - r#" -A% = 5 -B! = 2.0 -PRINT A% / B! -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "2.5"); -} - -#[test] -fn test_type_promotion_div_long_single() { - let output = compile_and_run( - r#" -A& = 9 -B! = 2.0 -PRINT A& / B! -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "4.5"); -} - -#[test] -fn test_type_promotion_div_long_double() { - let output = compile_and_run( - r#" -A& = 11 -B# = 4.0 -PRINT A& / B# -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "2.75"); -} - -#[test] -fn test_type_promotion_div_single_double() { - let output = compile_and_run( - r#" -A! = 7.0 -B# = 2.0 -PRINT A! / B# -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "3.5"); -} - -// ============================================ -// Cross-type integer division tests (always produces Long) -// ============================================ - -#[test] -fn test_type_promotion_intdiv_integer_long() { - let output = compile_and_run( - r#" -A% = 17 -B& = 5 -PRINT A% \ B& -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "3"); -} - -#[test] -fn test_type_promotion_intdiv_integer_single() { - let output = compile_and_run( - r#" -A% = 17 -B! = 5.0 -PRINT A% \ B! -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "3"); -} - -#[test] -fn test_type_promotion_intdiv_long_double() { - let output = compile_and_run( - r#" -A& = 25 -B# = 7.0 -PRINT A& \ B# -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "3"); -} - -#[test] -fn test_type_promotion_intdiv_single_double() { - let output = compile_and_run( - r#" -A! = 100.0 -B# = 30.0 -PRINT A! \ B# -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "3"); -} - -// ============================================ -// Cross-type MOD tests (always produces Long) -// ============================================ - -#[test] -fn test_type_promotion_mod_integer_long() { - let output = compile_and_run( - r#" -A% = 17 -B& = 5 -PRINT A% MOD B& -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "2"); -} - -#[test] -fn test_type_promotion_mod_integer_single() { - let output = compile_and_run( - r#" -A% = 17 -B! = 5.0 -PRINT A% MOD B! -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "2"); -} - -#[test] -fn test_type_promotion_mod_long_double() { - let output = compile_and_run( - r#" -A& = 25 -B# = 7.0 -PRINT A& MOD B# -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "4"); -} - -#[test] -fn test_type_promotion_mod_single_double() { - let output = compile_and_run( - r#" -A! = 100.0 -B# = 30.0 -PRINT A! MOD B# -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "10"); -} - -// ============================================ -// Cross-type power tests (always produces Double) -// ============================================ - -#[test] -fn test_type_promotion_pow_integer_long() { - let output = compile_and_run( - r#" -A% = 2 -B& = 8 -PRINT A% ^ B& -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "256"); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "3.5", "division produces double"); + assert_eq!(lines[1], "3", "integer division"); } #[test] -fn test_type_promotion_pow_integer_single() { +fn test_promotion_add() { + // Type promotion for addition: int+long, int+single, int+double, long+single, long+double, single+double let output = compile_and_run( r#" -A% = 4 -B! = 0.5 -PRINT A% ^ B! +A% = 100: B& = 200: PRINT A% + B& +A% = 10: B! = 2.5: PRINT A% + B! +A% = 10: B# = 2.5: PRINT A% + B# +A& = 100: B! = 0.5: PRINT A& + B! +A& = 100: B# = 0.25: PRINT A& + B# +A! = 1.5: B# = 2.5: PRINT A! + B# "#, ) .unwrap(); - assert_eq!(output.trim(), "2"); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "300", "int+long"); + assert_eq!(lines[1], "12.5", "int+single"); + assert_eq!(lines[2], "12.5", "int+double"); + assert_eq!(lines[3], "100.5", "long+single"); + assert_eq!(lines[4], "100.25", "long+double"); + assert_eq!(lines[5], "4", "single+double"); } #[test] -fn test_type_promotion_pow_integer_double() { +fn test_promotion_sub() { + // Type promotion for subtraction let output = compile_and_run( r#" -A% = 2 -B# = 3.0 -PRINT A% ^ B# +A% = 50: B& = 20: PRINT A% - B& +A% = 10: B! = 2.5: PRINT A% - B! +A% = 10: B# = 3.25: PRINT A% - B# +A& = 100: B! = 0.5: PRINT A& - B! +A& = 100: B# = 0.25: PRINT A& - B# +A! = 5.5: B# = 2.25: PRINT A! - B# "#, ) .unwrap(); - assert_eq!(output.trim(), "8"); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "30", "int-long"); + assert_eq!(lines[1], "7.5", "int-single"); + assert_eq!(lines[2], "6.75", "int-double"); + assert_eq!(lines[3], "99.5", "long-single"); + assert_eq!(lines[4], "99.75", "long-double"); + assert_eq!(lines[5], "3.25", "single-double"); } #[test] -fn test_type_promotion_pow_long_single() { +fn test_promotion_mul() { + // Type promotion for multiplication let output = compile_and_run( r#" -A& = 9 -B! = 0.5 -PRINT A& ^ B! +A% = 10: B& = 20: PRINT A% * B& +A% = 4: B! = 2.5: PRINT A% * B! +A% = 3: B# = 2.5: PRINT A% * B# +A& = 100: B! = 0.5: PRINT A& * B! +A& = 100: B# = 0.25: PRINT A& * B# +A! = 2.5: B# = 4.0: PRINT A! * B# "#, ) .unwrap(); - assert_eq!(output.trim(), "3"); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "200", "int*long"); + assert_eq!(lines[1], "10", "int*single"); + assert_eq!(lines[2], "7.5", "int*double"); + assert_eq!(lines[3], "50", "long*single"); + assert_eq!(lines[4], "25", "long*double"); + assert_eq!(lines[5], "10", "single*double"); } #[test] -fn test_type_promotion_pow_long_double() { +fn test_promotion_div_intdiv_mod() { + // Type promotion for division, integer division, and mod let output = compile_and_run( r#" -A& = 3 -B# = 4.0 -PRINT A& ^ B# +A% = 7: B& = 2: PRINT A% / B& +A% = 5: B! = 2.0: PRINT A% / B! +A& = 9: B! = 2.0: PRINT A& / B! +A& = 11: B# = 4.0: PRINT A& / B# +A! = 7.0: B# = 2.0: PRINT A! / B# +A% = 17: B& = 5: PRINT A% \ B& +A% = 17: B! = 5.0: PRINT A% \ B! +A& = 25: B# = 7.0: PRINT A& \ B# +A! = 100.0: B# = 30.0: PRINT A! \ B# +A% = 17: B& = 5: PRINT A% MOD B& +A% = 17: B! = 5.0: PRINT A% MOD B! +A& = 25: B# = 7.0: PRINT A& MOD B# +A! = 100.0: B# = 30.0: PRINT A! MOD B# "#, ) .unwrap(); - assert_eq!(output.trim(), "81"); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "3.5", "int/long"); + assert_eq!(lines[1], "2.5", "int/single"); + assert_eq!(lines[2], "4.5", "long/single"); + assert_eq!(lines[3], "2.75", "long/double"); + assert_eq!(lines[4], "3.5", "single/double"); + assert_eq!(lines[5], "3", "int\\long"); + assert_eq!(lines[6], "3", "int\\single"); + assert_eq!(lines[7], "3", "long\\double"); + assert_eq!(lines[8], "3", "single\\double"); + assert_eq!(lines[9], "2", "int mod long"); + assert_eq!(lines[10], "2", "int mod single"); + assert_eq!(lines[11], "4", "long mod double"); + assert_eq!(lines[12], "10", "single mod double"); } #[test] -fn test_type_promotion_pow_single_double() { +fn test_promotion_pow() { + // Type promotion for power operation let output = compile_and_run( r#" -A! = 2.0 -B# = 10.0 -PRINT A! ^ B# +A% = 2: B& = 8: PRINT A% ^ B& +A% = 4: B! = 0.5: PRINT A% ^ B! +A% = 2: B# = 3.0: PRINT A% ^ B# +A& = 9: B! = 0.5: PRINT A& ^ B! +A& = 3: B# = 4.0: PRINT A& ^ B# +A! = 2.0: B# = 10.0: PRINT A! ^ B# +A% = 10: B& = 20: C! = 0.5: D# = 100.0: PRINT A% + B& * C! + D# "#, ) .unwrap(); - assert_eq!(output.trim(), "1024"); + let lines: Vec<&str> = output.trim().lines().collect(); + assert_eq!(lines[0], "256", "int^long"); + assert_eq!(lines[1], "2", "int^single"); + assert_eq!(lines[2], "8", "int^double"); + assert_eq!(lines[3], "3", "long^single"); + assert_eq!(lines[4], "81", "long^double"); + assert_eq!(lines[5], "1024", "single^double"); + assert_eq!(lines[6], "120", "mixed expression"); } diff --git a/tests/variables/mod.rs b/tests/variables/mod.rs index 0d865e6..2c5bf4f 100644 --- a/tests/variables/mod.rs +++ b/tests/variables/mod.rs @@ -1,4 +1,4 @@ -//! Variable assignment and type suffix tests +//! Variable assignment and type suffix tests (consolidated) // Copyright (c) 2025-2026 Jeff Garzik // SPDX-License-Identifier: MIT @@ -6,90 +6,32 @@ use crate::common::{compile_and_run, normalize_output}; #[test] -fn test_variable_assignment() { +fn test_variable_types() { + // Test variable assignment and type suffixes (%, &, !, #) let output = compile_and_run( r#" -X = 100 -Y = 23 -PRINT X + Y -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "123"); -} - -#[test] -fn test_integer_suffix() { - let output = compile_and_run( - r#" -X% = 32000 -PRINT X% -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "32000"); -} - -#[test] -fn test_long_suffix() { - let output = compile_and_run( - r#" -X& = 100000 -PRINT X& -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "100000"); -} - -#[test] -fn test_single_suffix() { - // Test Single (!) type suffix - 32-bit float - let output = compile_and_run( - r#" -X! = 3.14159 -PRINT X! -"#, - ) - .unwrap(); - // Single has ~7 significant digits - assert!(output.contains("3.14159")); -} - -#[test] -fn test_single_arithmetic() { - // Test arithmetic with Single type - let output = compile_and_run( - r#" -A! = 2.5 -B! = 3.5 -PRINT A! + B! -PRINT A! * B! +X = 100: Y = 23: PRINT X + Y +X% = 32000: PRINT X% +X& = 100000: PRINT X& +X! = 3.14159: PRINT X! "#, ) .unwrap(); let lines: Vec<&str> = output.trim().lines().collect(); - assert_eq!(lines[0], "6"); - assert_eq!(lines[1], "8.75"); -} - -#[test] -fn test_string_variable() { - let output = compile_and_run( - r#" -X$ = "Hello" -Y$ = " World" -PRINT X$ + Y$ -"#, - ) - .unwrap(); - assert_eq!(output.trim(), "Hello World"); + assert_eq!(lines[0], "123", "default vars"); + assert_eq!(lines[1], "32000", "integer suffix"); + assert_eq!(lines[2], "100000", "long suffix"); + assert!(lines[3].contains("3.14159"), "single suffix"); } #[test] -fn test_rem_comment() { +fn test_variable_misc() { + // Test single arithmetic, string variables, and comments let output = compile_and_run( r#" +A! = 2.5: B! = 3.5: PRINT A! + B! +A! = 2.5: B! = 3.5: PRINT A! * B! +X$ = "Hello": Y$ = " World": PRINT X$ + Y$ REM This is a comment PRINT "before" REM Another comment @@ -97,5 +39,11 @@ PRINT "after" "#, ) .unwrap(); - assert_eq!(normalize_output(&output), "before\nafter"); + let normalized = normalize_output(&output); + let lines: Vec<&str> = normalized.lines().collect(); + assert_eq!(lines[0], "6", "single add"); + assert_eq!(lines[1], "8.75", "single mul"); + assert_eq!(lines[2], "Hello World", "string concat"); + assert_eq!(lines[3], "before", "before comment"); + assert_eq!(lines[4], "after", "after comment"); }