I finally made my BASIC interpreter as I said I would last year. =D
Right now it's very slow since it just interprets BASIC. If I made it compile BASIC into Snap! code (which I'm planning to do) then it would run way faster.
More info the project notes
Removed iframe because it would freeze my computer for 2 seconds everytime it loaded
10 PRINT "Hello, " ;
20 PRINT "world!"
30 GOTO 10
What is your name?
10 INPUT "What is your name?" ; NAME$
20 PRINT "Hello, " ; NAME$
10 LET X1 = 0
20 LET X2 = 1
30 LET XOUT = X1 + X2
40 PRINT XOUT
50 LET X2 = X1
60 LET X1 = XOUT
70 GOTO 30
10 INPUT "Enter radius: " ; RADIUS
20 INPUT "Enter center x: " ; ORIGINX
30 INPUT "Enter center y: " ; ORIGINY
40 FOR AR=0 TO 2 * PI STEP (5) / 180 * PI
60 TGOTO COS(AR) * RADIUS + ORIGINX, SIN(AR) * RADIUS + ORIGINY
80 NEXT AR
5 GOSUB 1000
10 TGOTO 0, 0
15 TSAT 100
18 TBRI 100
20 LET TURTLESIZE = 5
40 TMOVE 5
50 THUEC 5
60 IF GETCHAR() = 39 THEN TTURN 5
70 IF GETCHAR() = 37 THEN TTURN -5
80 GOTO 40
1000 REM didn't have enough lines to put this at the beginning
1010 PRINT "Left and right arrow keys to control pen"
1020 PRINT "Press any key to start"
1030 IF GETCHAR() <> 0 THEN RETURN
1040 GOTO 1030
10 INPUT "first number: " ; NUM1
20 INPUT "second number: " ; NUM2
30 INPUT "operation: " ; OP$
40 IF OP$ = "+" THEN GOTO 1000
50 IF OP$ = "-" THEN GOTO 2000
60 IF OP$ = "*" THEN GOTO 3000
70 IF OP$ = "/" THEN GOTO 4000
80 PRINT "invalid operation"
90 GOTO 30
1000 REM Add operation
1010 LET RES = NUM1 + NUM2
1020 GOTO 9999
2000 REM Subtract operation
2010 LET RES = NUM1 - NUM2
2020 GOTO 9999
3000 REM Multiply operation
3010 LET RES = NUM1 * NUM2
3020 GOTO 9999
4000 REM Divide operation
4010 LET RES = NUM1 / NUM2
4020 GOTO 9999
9999 REM Print result
10000 PRINT NUM1 ; " " ; OP$ ; " " ; NUM2 ; " = " ; RES
Umm... why can't I edit the original post? I wanted to add another example:
10 LET NUM = 1
15 IF MOD(NUM, 3) = 0 AND MOD(NUM, 5) = 0 THEN GOTO 4000
20 IF MOD(NUM, 3) = 0 THEN GOTO 1000
30 IF MOD(NUM, 5) = 0 THEN GOTO 2000
40 GOTO 3000
50 LET NUM = NUM + 1
60 GOTO 15
1000 PRINT "Fizz"
1010 GOTO 50
2000 PRINT "Buzz"
2010 GOTO 50
3000 PRINT NUM
3010 GOTO 50
4000 PRINT "FizzBuzz"
4010 GOTO 50
Unbearably slow pong game
5 REM Initilization
10 LET PX = 0
20 LET PY = -150
30 LET PW = 50
40 LET BX = 0
50 LET BY = 0
60 LET BXV = 10
70 LET BYV = 10
75 LET TURTLESIZE = 5
80 GOTO 1000
90 REM -----------------------
1000 REM Game loop
1010 IF GETCHAR() = 39 THEN LET PX = PX + 10
1020 IF GETCHAR() = 37 THEN LET PX = PX - 10
1030 LET BX = BX + BXV
1040 LET BY = BY + BYV
1050 IF BY > 180 THEN GOSUB 2000
1060 IF BX > 240 THEN GOSUB 3000
1070 IF BX < -240 THEN GOSUB 4000
1080 IF BY < PY THEN GOSUB 5000
1090 REM Game rendering
1110 TGOTO PX, PY
1130 TGOTO PX + PW, PY
1150 TGOTO BX, BY
1170 TMOVE 1
1190 GOTO 1000
1200 REM -----------------------
2000 REM Ball hit top
2010 LET BY = 180
2020 LET BYV = -BYV
3000 REM -----Ball hit right-------
3010 LET BX = 240
3020 LET BXV = -BXV
4000 REM -----Ball hit left-----
4010 LET BX = -240
4020 LET BXV = -BXV
5000 REM ----Ball pass player----
5010 IF BX < PX OR BX > PX + PW THEN GOTO 6000
5020 LET BY = PY
5030 LET BYV = -BYV
6000 REM ---Game lost---
6010 PRINT "you lost :("
or whatever the right thing is. And constructors to make a token given labelled components: or whatever the components actually are. NEW TOKEN would just call LIST, and NAME OF TOKEN would just call ITEM, so they'd be brainless one-liners, but they'd make the code way easier to read.
And maybe more subprocedures. The rule of thumb is, if your scripting area or a block editor has a scroll bar on the right, the code is too long. It's just a rule of thumb, but it works pretty well for me.
Data-directed programming could help with the long else-if chains for all the BASIC commands. Make a dictionary (a Snap! one, not a JS one -- a list of two-item lists) with command names as the keys and procedures as the values, so you can replace the long chain of IFs with
Don't worry about the speed. Make it perfect and finished, then worry about speed. One of the fundamental rules of computer science is, "It's much easier to speed up a correct program than to debug a fast program."
I tried to trace out how you got the grouping of arithmetic operators to work, and couldn't figure it out. I saw two pieces of code, one commented with MD and the other with AS, but they looked the same apart from the operator symbols. Maybe data abstraction would have made that clearer for me. Or maybe you need a subprocedure with inputs of a list of operator symbols and a numeric priority, that you can call for all the infix operators.
I agree with your ideas. I wish I didn't start the BASIC code execution block by starting out with a bunch of if-elses because now it takes 1:30 to open the block editor for that block and it takes 5 seconds to redraw the script for when I drag out or change a value or put in a new block... My excuse to not doing that earlier was that I would have to redefine every script variable for each procedure, but I realize now doing so doesn't take that much time, and I would rather have the data-directed instead of the long if-else. (also, how do my script builder blocks help you make macros? couldn't you make the process execute the Context without making a new variable scope?)
I think I did the if-elses in the first place because I remembered I made a really basic program interpreter in Scratch (it didn't have expressions you could only do mathematical operations on variables (which were memory locations)), and because of the limitations of Scratch I used a whole bunch of if-elses. There was also a GameBoy emulator in Scratch that used a bunch of if-elses for each command in the gameboy machine language and I guess that encouraged me to think that a bunch of those if-elses were fine.
Also, I've never heard of "It's much easier to speed up a correct program than to debug a fast program." I think that will be very helpful.
The order of operations worked for Multiplication/Division and Addition/Subtraction by going left-right through the list of tokens, and for each item if it's a multiplication/division symbol then it does the operation with the token before and after. Then after that, it goes through the list again and does the same for addition/subtraction. The MD stood for Multiplication/Division (PEMDAS) and the AS stood for Addition/Subtraction (PEMDAS).
Yeah, your code in this message is fine. There's an Official Right Thing for parsing infix that lets you just scan through the list once, left to right, keeping a stack of unused values and a stack of unfinished operators. When you see an operator, you compare the precedence of that operator with the precedence of the one on the stack. If the new one is lower precedence, then it's time to carry out the one on the stack, so you pop off two operands and one operator, compute that result, and put it on the operand stack. Meanwhile you're still looking at the same new operator, but the operator on top of the stack of operators has changed (because you popped one off), so you do the comparison again. Eventually you get to where the new operator is higher precedence than the one on the stack, and so you push the new operator onto the operator stack and push the value to its left onto the operand stack. I think that's right. Anyway, the gory details are here: https://people.eecs.berkeley.edu/~bh/v3ch5/langi.html (search for "precedence").
I'm trying to write it entirely in Snap!, so that I can easily play around with the design. Honestly, I haven't started yet -- a combination of the forum keeping me busy and a general depression that started before covid but hasn't been helped any by enforced isolation. But it's on my list of things I really want to get going on! That and writing a Snap! tutorial book.
PS I understood what the "MD" and "AS" comments mean! I used to be a teacher. :~) I just couldn't figure out the code to which they were connected.