Skip to content

Commit bc1dd4e

Browse files
jgarzikclaude
andcommitted
Fix codegen correctness issues: register clobbering, >6 params, GOSUB overflow
- Fix argument register clobbering in function calls by evaluating all args to stack temporaries before loading registers - Fix LEFT$/RIGHT$/MID$ builtins to use callee-saved registers (r12-r14) - Support >6 parameters in SUB/FUNCTION via stack-based overflow args - Add GOSUB stack overflow protection with runtime error handler - Increase GOSUB stack from 1K to 64K entries (512KB) - Add expression nesting depth warning at 256 levels - Use consistent sub rsp/add rsp pattern for string concatenation temps - Add integration tests for all fixes Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 4966261 commit bc1dd4e

File tree

8 files changed

+515
-48
lines changed

8 files changed

+515
-48
lines changed

src/codegen.rs

Lines changed: 251 additions & 48 deletions
Large diffs are not rendered by default.

src/runtime/sysv/data_defs.s

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ _chr_buf: .skip 2
1212
_str_buf: .skip 64
1313
_rng_state: .quad 0x12345678DEADBEEF
1414
_cls_seq: .asciz "\033[2J\033[H"
15+
_gosub_overflow_msg: .asciz "Error: GOSUB stack overflow\n"

src/runtime/sysv/print.s

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,22 @@ _rt_print_float:
130130
.Lprint_float_done:
131131
leave
132132
ret
133+
134+
# ------------------------------------------------------------------------------
135+
# _rt_gosub_overflow - Handle GOSUB stack overflow error
136+
# ------------------------------------------------------------------------------
137+
# Called when the GOSUB return stack is exhausted. Prints an error message
138+
# and terminates the program with exit code 1.
139+
#
140+
# Arguments: none
141+
# Returns: never (calls exit)
142+
# ------------------------------------------------------------------------------
143+
.globl _rt_gosub_overflow
144+
_rt_gosub_overflow:
145+
push rbp
146+
mov rbp, rsp
147+
lea rdi, [rip + _gosub_overflow_msg]
148+
xor eax, eax
149+
call {libc}printf
150+
mov edi, 1 # exit code 1
151+
call {libc}exit

src/runtime/win64-native/data_defs.s

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@
1515
_fmt_int: .asciz "%lld"
1616
_fmt_float: .asciz "%g"
1717

18+
# Error messages
19+
_gosub_overflow_msg: .ascii "Error: GOSUB stack overflow\r\n"
20+
.equ _gosub_overflow_msg_len, 29
21+

src/runtime/win64-native/print.s

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,33 @@ _rt_print_float:
178178

179179
leave
180180
ret
181+
182+
# ------------------------------------------------------------------------------
183+
# _rt_gosub_overflow - Handle GOSUB stack overflow error
184+
# ------------------------------------------------------------------------------
185+
# Called when the GOSUB return stack is exhausted. Prints an error message
186+
# and terminates the program with exit code 1.
187+
#
188+
# Arguments: none
189+
# Returns: never (calls ExitProcess)
190+
# ------------------------------------------------------------------------------
191+
.globl _rt_gosub_overflow
192+
_rt_gosub_overflow:
193+
push rbp
194+
mov rbp, rsp
195+
sub rsp, 48
196+
197+
# Get stdout handle
198+
lea rax, [rip + _stdout_handle]
199+
mov rcx, [rax]
200+
201+
# WriteFile(handle, message, length, &bytesWritten, NULL)
202+
lea rdx, [rip + _gosub_overflow_msg]
203+
mov r8, _gosub_overflow_msg_len
204+
lea r9, [rip + _bytes_written]
205+
mov QWORD PTR [rsp + 32], 0
206+
call WriteFile
207+
208+
# ExitProcess(1)
209+
mov ecx, 1
210+
call ExitProcess

tests/control/mod.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,3 +280,54 @@ PRINT "after"
280280
.unwrap();
281281
assert_eq!(output.trim(), "before");
282282
}
283+
284+
#[test]
285+
fn test_gosub_many_calls() {
286+
// Test GOSUB with many calls to stress the return stack
287+
let output = compile_and_run(
288+
r#"
289+
X = 0
290+
FOR I = 1 TO 500
291+
GOSUB 100
292+
NEXT I
293+
PRINT X
294+
END
295+
296+
100 X = X + 1
297+
RETURN
298+
"#,
299+
)
300+
.unwrap();
301+
assert_eq!(output.trim(), "500");
302+
}
303+
304+
#[test]
305+
fn test_gosub_nested() {
306+
// Test nested GOSUB calls
307+
let output = compile_and_run(
308+
r#"
309+
GOSUB 100
310+
PRINT "done"
311+
END
312+
313+
100 PRINT "L1 start"
314+
GOSUB 200
315+
PRINT "L1 end"
316+
RETURN
317+
318+
200 PRINT "L2 start"
319+
GOSUB 300
320+
PRINT "L2 end"
321+
RETURN
322+
323+
300 PRINT "L3"
324+
RETURN
325+
"#,
326+
)
327+
.unwrap();
328+
let lines: Vec<&str> = output.trim().lines().collect();
329+
assert_eq!(
330+
lines,
331+
vec!["L1 start", "L2 start", "L3", "L2 end", "L1 end", "done"]
332+
);
333+
}

tests/procedures/mod.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,96 @@ END SUB
5353
.unwrap();
5454
assert_eq!(output.trim(), "30");
5555
}
56+
57+
#[test]
58+
fn test_nested_fn_args() {
59+
// Test that nested function calls in arguments don't clobber registers
60+
let output = compile_and_run(
61+
r#"
62+
FUNCTION Add(A, B)
63+
Add = A + B
64+
END FUNCTION
65+
66+
FUNCTION Mul(A, B)
67+
Mul = A * B
68+
END FUNCTION
69+
70+
PRINT Add(Mul(2, 3), Mul(4, 5))
71+
"#,
72+
)
73+
.unwrap();
74+
// 2*3=6, 4*5=20, 6+20=26
75+
assert_eq!(output.trim(), "26");
76+
}
77+
78+
#[test]
79+
fn test_seven_params() {
80+
// Test procedure with 7 parameters (1 more than max register args on SysV)
81+
let output = compile_and_run(
82+
r#"
83+
SUB Sum7(A, B, C, D, E, F, G)
84+
PRINT A + B + C + D + E + F + G
85+
END SUB
86+
87+
Sum7(1, 2, 3, 4, 5, 6, 7)
88+
"#,
89+
)
90+
.unwrap();
91+
// 1+2+3+4+5+6+7 = 28
92+
assert_eq!(output.trim(), "28");
93+
}
94+
95+
#[test]
96+
fn test_eight_params() {
97+
// Test function with 8 parameters (2 more than max register args on SysV)
98+
let output = compile_and_run(
99+
r#"
100+
FUNCTION Sum8(A, B, C, D, E, F, G, H)
101+
Sum8 = A + B + C + D + E + F + G + H
102+
END FUNCTION
103+
104+
PRINT Sum8(1, 2, 3, 4, 5, 6, 7, 8)
105+
"#,
106+
)
107+
.unwrap();
108+
// 1+2+3+4+5+6+7+8 = 36
109+
assert_eq!(output.trim(), "36");
110+
}
111+
112+
#[test]
113+
fn test_ten_params() {
114+
// Test with 10 parameters to ensure multiple overflow args work
115+
let output = compile_and_run(
116+
r#"
117+
FUNCTION Sum10(A, B, C, D, E, F, G, H, I, J)
118+
Sum10 = A + B + C + D + E + F + G + H + I + J
119+
END FUNCTION
120+
121+
PRINT Sum10(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
122+
"#,
123+
)
124+
.unwrap();
125+
// 1+2+3+4+5+6+7+8+9+10 = 55
126+
assert_eq!(output.trim(), "55");
127+
}
128+
129+
#[test]
130+
fn test_nested_fn_many_params() {
131+
// Test nested function calls with many parameters
132+
let output = compile_and_run(
133+
r#"
134+
FUNCTION AddThree(A, B, C)
135+
AddThree = A + B + C
136+
END FUNCTION
137+
138+
FUNCTION MulTwo(A, B)
139+
MulTwo = A * B
140+
END FUNCTION
141+
142+
PRINT AddThree(MulTwo(2, 3), MulTwo(4, 5), MulTwo(6, 7))
143+
"#,
144+
)
145+
.unwrap();
146+
// 2*3=6, 4*5=20, 6*7=42, 6+20+42=68
147+
assert_eq!(output.trim(), "68");
148+
}

tests/strings/mod.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,69 @@ fn test_instr_function() {
6262
let output = compile_and_run(r#"PRINT INSTR("Hello World", "World")"#).unwrap();
6363
assert_eq!(output.trim(), "7");
6464
}
65+
66+
#[test]
67+
fn test_left_with_nested_call() {
68+
// Test LEFT$ with nested function call in count argument
69+
let output = compile_and_run(
70+
r#"
71+
A$ = "HELLO"
72+
B$ = "WORLD"
73+
PRINT LEFT$(A$ + B$, LEN(A$))
74+
"#,
75+
)
76+
.unwrap();
77+
// A$+B$ = "HELLOWORLD", LEN(A$) = 5, LEFT$ takes first 5 chars
78+
assert_eq!(output.trim(), "HELLO");
79+
}
80+
81+
#[test]
82+
fn test_right_with_nested_call() {
83+
// Test RIGHT$ with nested function call in count argument
84+
let output = compile_and_run(
85+
r#"
86+
A$ = "HELLO"
87+
B$ = "WORLD"
88+
PRINT RIGHT$(A$ + B$, LEN(B$))
89+
"#,
90+
)
91+
.unwrap();
92+
// A$+B$ = "HELLOWORLD", LEN(B$) = 5, RIGHT$ takes last 5 chars
93+
assert_eq!(output.trim(), "WORLD");
94+
}
95+
96+
#[test]
97+
fn test_mid_with_nested_calls() {
98+
// Test MID$ with nested function calls in position and count arguments
99+
let output = compile_and_run(
100+
r#"
101+
FUNCTION GetStart()
102+
GetStart = 2
103+
END FUNCTION
104+
105+
FUNCTION GetLen()
106+
GetLen = 3
107+
END FUNCTION
108+
109+
PRINT MID$("ABCDEF", GetStart(), GetLen())
110+
"#,
111+
)
112+
.unwrap();
113+
// MID$("ABCDEF", 2, 3) = "BCD"
114+
assert_eq!(output.trim(), "BCD");
115+
}
116+
117+
#[test]
118+
fn test_string_concat_multiple() {
119+
// Test string concatenation with multiple operands
120+
let output = compile_and_run(
121+
r#"
122+
A$ = "Hello"
123+
B$ = " "
124+
C$ = "World"
125+
PRINT A$ + B$ + C$
126+
"#,
127+
)
128+
.unwrap();
129+
assert_eq!(output.trim(), "Hello World");
130+
}

0 commit comments

Comments
 (0)