9

Yesterday I was looking for a way to implement a delay loop on the C64. I came across this page: https://www.c64-wiki.com/wiki/TIME At the bottom of the page there is a code fragment that waits for a specified number of seconds. This code works, but I can't for the life of me understand how. I imitated the code in CBM prg Studio as follows:

10 t=2
20 t0=ti
30 ? "start " t0
40 for t=t0+t*60 to t
50   t=ti
60   t=t-5184E3*(t<t0)
70 next
80 ? "end "t

Line 40 is the one that confuses me. When you enter the loop, t0+t*60 evaluates to a value that, in this program, is guaranteed to be greater than t. Still the loop will execute 1 time, just like with FOR 5 TO 2. So far so good.

Then t is set to TIME and is corrected in case of a 24 hour wrap-around. I also understand that.

But then you get to the next TO t. Why do we stay in the loop until t0+2*60? I would assume that TO t either means TO 2 (t's initial value) or TO <whatever t's value is>. In either case, I'd think that the loop would end immediately.

4 Answers 4

5

TL;DR:

FOR T=<target value> TO T uses T as a temporary variable to store the target value inside the FOR stack frame, where it is used later to compare with the actual value of T after an iteration. This eliminates the need for a second, helper variable. Works whenever T is/can be set within the loop.


The Long Read:

I imitated the code in CBM prg Studio as follows:

In my eyes this seems an even more confusing notation than the original code:

100 PRINT "START "TI$ : T=2.5: GOSUB 900 : PRINT "END TIME: "TI$
199 END
900 T0=TI: FOR T=T0+T*60 TO T : T=TI : T=T-5184E3*(T<T0) : NEXT : RETURN

But then again, I'm maybe simply used to the way a PET worked :)

But yeah, needs some knowledge about inner workings of (Microsoft) BASIC.

Line 40 is the one that confuses me. When you enter the loop, t0+t*60 evaluates to a value that, in this program, is guaranteed to be greater than t.

No, it isn't, it's the same as T - or more exact the value of T when the loop is entered first. Note that:

  1. T is set as part of the FOR instruction to the evaluated expression after the equal sign.
  2. TO evaluates and stores the target value on the stack.
  3. A for-loop is iterated as long the actual value is lower or equal to the target value
  4. When comparing, it compares the actual value of T to the stored target value.

Step two is needed less for performance enhancement but to allow FOR loops with constant values like FOR I = 1 TO 10. After all, there is no variable to store that but the stack frame itself.

But then you get to the next 'TO t'. Why do we stay in the loop until t0+2*60?

Because the value to be compared against is not T, but T stored at the time the loop is entered, here T0+T*60.

I would assume that 'TO t' either means 'TO 2' (t's initial value) or 'TO <whatever t's value is>'.

No, it's 'TO T' with the value of T at the time the TO gets executed, so after the initial assignment of T0+T*60.

In either case, I'd think that the loop would end immediately.

Only if the actual value of T is above the limit given, as that's what FOR does. In this case, T is equal to the limit when entering the loop, so it gets executed once. This is the base to allow a FOR I=1 TO 1 loop to execute.

Execution in Detail

Let's see it step by step (leaving out framing):

  • 100 T=2.5 - Set wait time to 2.5 seconds
  • GOSUB 900 - Go and wait
  • 900 T0=TI - Lookup actual time
  • FOR T=T0+T*60 - Set T to the desired end time
  • TO T - Store the value of T as terminal value
  • T=TI - Set T to actual time (which should be lower than taget time
  • T=T-5184E3*(T<T0) - Overflow correction
  • NEXT - Continue to iterate the loop if the (corrected) value of T is still lower than the saved target value.

What's the Trick used?

Instead of having a second variable for the target value, which is never used within the loop it uses the internal storage within the FOR stack frame. This is quite handy in BASIC dialects without global variables, as the subroutine does not need/define any additional variable of its own.


Couldn't TI be Used Instead?

No, TI is not a real variable but a pseudo variable mirroring the time of day clock. So while TI can be used as target value in a FOR clause, like any other variable, the value stored would be the one at time of entrance to the loop. Also, this would make any overflow handling somewhat hard.


But It Would Work With a GOTO Loop, Right?

Yes. Something around a core like this

900 T=TI+T0*60
910 IF TI < T THEN GOTO 910
920 RETURN

May work quite well (though need some fine tuning for overflow) - as usual, many ways lead to Rome.


So All of This is Commodore Specific?

  • No, it should work with any BASIC, not just MS supplied ones, it also complies to the ECMA-55 spec.

  • BUT there seem to be at least one BASIC (Locomotive, but that needs more testing) that works different.

All it needs is to adapt machine specific values. For example BBC BASIC offers a TIME pseudo variable with similar workings - except it's incremented 100 times a second (*1):

100 PRINT "START ", TIME : T=2.5: GOSUB 900 : PRINT "END TIME: ",TIME
199 END
900 T0=TIME: FOR T=T0+T*100 TO T : T=TIME : NEXT : RETURN

The BBC is an interesting case here, as its clock is not incremented monotone by 1 but by 5 or 6 (*2). The implied greater than comparison of a FOR/NEXT loop does handle this right away.

The very same code should as well work with the CPC's Locomotive BASIC, this time with a Factor of 300 (*2), but it doesn't (*3):

900 T0=TIME: FOR T=T0+T*300 TO T : T=TIME : NEXT : RETURN

(This is still under research)

Locomotive BASIC seems to assign the new value to T (as part of FOR), but then compares to the previous value of T - as if that had been stored onto it's frame before the assignment gets evaluated. Quite a strange behaviour.


How the Stack Frame for FOR is Structured

This is Commodore specific, but other basics use similar structures.

  • Type Byte $81 to indicate a FOR entry
  • Loop variable address (two bytes)
  • Increment float (five bytes)
  • Increment sign, 01 or FF (one byte)
  • Target value, float (five bytes)
  • Line number (two bytes)
  • Return address (two bytes)

*1 - Time is only updated 18 times a second.

*2 - 300 can easy be divided by 5 or 6 do produce 50 or 60 Hz to fit into frame timing for European or US/Japanese TV

*3 - As Scruss did find by testing the program with a real CPC 6128.

7
  • 2
    Thank you so much for this very elaborate answer! The part about the initial value of t in the FOR loop (t0+t*60) being put on a stack and referenced throughout the loop really cleared it up for me. Indeed this also makes sense for a simple loop like FOR t=1 TO 10 - that 10 has to be stored somewhere after all... :)
    – JvH
    Feb 4, 2021 at 20:34
  • Note that this doesn't apply to Locomotive BASIC. It doesn't pause at all, printing something like START: 18994; END: 19024 - that is, 0.1s later
    – scruss
    Feb 5, 2021 at 1:11
  • @Raffzahn yes I pasted in your code exactly as you wrote it. It didn't pause at all.
    – scruss
    Feb 5, 2021 at 1:28
  • 2
    @Raffzahn - that's not an emulator, that's an implementation ("CPC BASIC Unleashed"). It doesn't get things quite right
    – scruss
    Feb 5, 2021 at 1:47
  • 1
    @scruss Ok, this is becoming real fun. Found another emulator (real crappy handling) but this time it works as you told. Seams like as if Locomotive BASIC's FOR does the assignment, but then compares to the previous value of T, which ofc is lower than the new one.
    – Raffzahn
    Feb 5, 2021 at 2:14
6
40 for t=t0+t*60 to t

... will repeat at least once and then for as long as t0+t*60 (i.e. start time + duration) does not equal t.

So this:

  1. captures t0+t*60 (the desired end time) such that t can be reused; and
  2. after the first iteration, continuously tests ti (which has been stored to t) against the desired end time.

i.e. Commodore's implementation of for with an implicit next captures the original variable value and updates its internal counter separately from the variable. So here you get: (i) set initial condition and perform once; (ii) continue until internal counter is equal to t, a proxy for ti.

ti is not directly usable as the target for a loop, being a special variable.

17
  • Not just Commodore does so, but essentially every basic, otherwise the loop end would be a moving target, which it must not be.
    – Raffzahn
    Feb 4, 2021 at 9:01
  • I tried the first three distinct non-Microsoft BASICs I could think of: Acorn, Locosoft(/Amstrad) and ZX Spectrum. All three of them did not behave like this; in all three cases the permitted structure closest to FOR A=1 to 10 A=2 NEXT resulted in an infinite loop. I therefore strongly disagree with "essentially every basic".
    – Tommy
    Feb 4, 2021 at 12:11
  • Tommy, not sure what that code should prove, as it's not only not the use case, but any MS Basic will as well go into an infinite loop, as A never reaches 10. Check here - type LIST to see the code and RUN to get an infinite loop in MS-BASIC. A structure reassembling the question would be: FOR A=1 TO 10:A=10:NEXT which I bet any of the non MS will execute exactly once - like any MS. This is a feature of BASIC, not a specific interpreter. (using FOR A=1 TO 10:PRINT A:A=10:NEXT may help seeing the issue).
    – Raffzahn
    Feb 4, 2021 at 12:24
  • I misunderstood your original comment and am now away from my computer. That being the case, and it being your claim: in making the “almost every basic” claim, which did you test?
    – Tommy
    Feb 4, 2021 at 12:53
  • 1
    @Raffzahn BBC BASIC behaves like C64 BASIC here, but Locomotive BASIC doesn't. Commodore BASIC will run (for example) FOR I1= 8 TO 5 at least once, when according to NBS Special Publication 500-70/2 test program 47, it shouldn't enter the loop even once.
    – scruss
    Feb 5, 2021 at 1:17
2

Commodore BASIC has a quirk that it will run a FOR loop at least once. This is what allows your t variable to update at least once, allowing the loop to be valid.

Raffzahn's explained how it works, but according to BASIC standards, it shouldn't. The ANSI/NBS Special Publication 500-70/1 test suite from 1980 go into great detail about how BASIC should behave. Particular tests FILE 47 and FILE 48 regarding FOR loops fail on C64 BASIC, but are the mechanism by which this code works on Commodore machines.

Microsoft fixed this quirk by the time of MBASIC-80 v5 (1981), as NBS compliance was deemed important enough to be mentioned in the manual for that interpreter.

0

From what I can gather, the code should be understood as if it read

10 t=2
20 t0=ti
30 ? "start " t0
40 t=t0+t*60 : t1=t
50 t=ti
60 t=t-5184E3*(t<t0)
70 t=t+1 : if t<=t1 then goto 50
80 ? "end "t

where t1 is a hidden variable, invisible to your code. In other words, FOR i = a TO b has neither of the two semantics you hypothesised: it first performs i = a, then computes b once and remembers the result for later. The NEXT statement increments i, compares it against the previously remembered value of b, and goes back to the start of the loop body if the upper bound has not been exceeded yet.

I don’t think there is a good way to test this purely from user code, since as far as I know, CBM BASIC does not support defining functions with side effects like later dialects of BASIC (i.e. FUNCTION). You’d probably have to look it up in the interpreter’s source code to be sure. But since the source code of Microsoft BASIC for the 6502 is not hard to find, this doesn’t seem so hopeless. In fact, a quick skim of the code reveals that the very first thing the FOR subroutine does is delegate to LET to immediately perform the initial assignment to the loop counter; only later is the upper bound of the loop evaluated and pushed on the stack, so that NEXT may use it.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .