Skip to content

Commit 0d6dcd7

Browse files
authored
Merge pull request #113 from nvt/fix/begin-tail-flag
expr: evaluate begin sequentially, tail-flag only the last expression
2 parents b0dc2cc + 23261a5 commit 0d6dcd7

2 files changed

Lines changed: 78 additions & 5 deletions

File tree

core/expr-primitives.c

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -444,12 +444,37 @@ VM_FUNCTION(return)
444444

445445
VM_FUNCTION(begin)
446446
{
447-
/* Defer the evaluation of the arguments in order to set
448-
the tail call flag when executing the last argument. */
449-
VM_EVAL_SET_REQUESTED_RANGE(thread, 1, thread->expr->argc);
450-
VM_SET_FLAG(thread->expr->flags, VM_EXPR_TAIL_CALL);
451-
if(VM_EVAL_COMPLETED(thread, argc - 1)) {
447+
int last_eval_arg;
448+
449+
if(argc == 0) {
450+
/* (begin) evaluates to an unspecified value. */
451+
VM_EVAL_STOP(thread);
452+
return;
453+
}
454+
455+
/* Evaluate the expressions one at a time, in order, using the same
456+
sequential pattern as `and`/`or`. Only the FINAL expression is in tail
457+
position; the leading ones run for effect, so the tail-call flag must
458+
stay clear while they evaluate. Setting it for all of them (as a single
459+
up-front request did) wrongly marks a non-last recursive call, such as
460+
the first (f x) in (begin (f x) (g x)), as a tail call, so it gets
461+
tail-folded and never returns. The value of `begin` is its last
462+
expression. */
463+
last_eval_arg = highest_bit_set(thread->expr->eval_completed);
464+
if(last_eval_arg == argc - 1) {
452465
VM_PUSH(&argv[argc - 1]);
466+
VM_EVAL_STOP(thread);
467+
} else if(last_eval_arg == -1) {
468+
if(argc == 1) {
469+
VM_SET_FLAG(thread->expr->flags, VM_EXPR_TAIL_CALL);
470+
}
471+
VM_EVAL_ARG(thread, 0);
472+
} else {
473+
if(last_eval_arg + 2 == argc) {
474+
/* About to evaluate the last expression, which is in tail position. */
475+
VM_SET_FLAG(thread->expr->flags, VM_EXPR_TAIL_CALL);
476+
}
477+
VM_EVAL_ARG(thread, last_eval_arg + 1);
453478
}
454479
}
455480

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
;;; VeloxVM Unit Tests - begin tail-position semantics (R5RS 4.2.3 / 3.5)
2+
;;;
3+
;;; Regression for the begin tail-flag bug in core/expr-primitives.c: begin
4+
;;; used to mark ALL of its sub-expressions as tail calls, not just the
5+
;;; last. A non-last recursive call inside a begin was therefore wrongly
6+
;;; tail-folded (its frame reused) and never actually recursed, so the
7+
;;; expressions after it ran far too few times. Only the FINAL expression
8+
;;; of a begin is in tail position; the leading ones run for effect.
9+
;;;
10+
;;; Depths are kept small so the genuine (non-tail) recursion fits the
11+
;;; VM context stack; the point is correctness, not depth.
12+
13+
(include "../unit-test-framework.scm")
14+
15+
(test-suite "begin: only the last expression is in tail position")
16+
17+
;; In (begin (recurse) (bump!)) the recursion must run to completion, so
18+
;; bump! fires once per level on the way back up. Before the fix the
19+
;; recursive call was tail-folded and bump! fired exactly once.
20+
(define counter (make-vector 1 0))
21+
(define (descend n)
22+
(when (> n 0)
23+
(begin (descend (- n 1)) ; non-tail
24+
(vector-set! counter 0 (+ 1 (vector-ref counter 0)))))) ; tail
25+
(descend 12)
26+
(assert-equal 12 (vector-ref counter 0)
27+
"leading recursive call in begin runs to completion")
28+
29+
;; Three expressions: the first is a non-tail recursive call, the middle
30+
;; and last run for effect. Each level adds 2, so g(10) yields 20.
31+
(define c2 (make-vector 1 0))
32+
(define (g n)
33+
(when (> n 0)
34+
(begin (g (- n 1))
35+
(vector-set! c2 0 (+ 1 (vector-ref c2 0)))
36+
(vector-set! c2 0 (+ 1 (vector-ref c2 0))))))
37+
(g 10)
38+
(assert-equal 20 (vector-ref c2 0)
39+
"all leading expressions of a begin run, not just the last")
40+
41+
;; Tail recursion through begin's LAST expression must still fold (no
42+
;; overflow at depth far beyond the context stack).
43+
(define (countdown n)
44+
(when (> n 0) (begin 1 (countdown (- n 1)))))
45+
(countdown 200000)
46+
(assert-true #t "tail call in begin's last expression still folds (no overflow)")
47+
48+
(test-summary)

0 commit comments

Comments
 (0)