GOTO
was still state of the art ;-) "How about some fun coding or even performing a kata in BASIC?" Without any connection to the internet
gwbasic.exe
or any other interpreter was out of reach. But BASIC v2 (aka Commodore BASIC) was quite simple. So I started implementing an interpreter myself. The question was if it is possible to implement BASIC v2 as an internal DSL in Scala. DSLs are quite popular in Scala, even for other languages, e.g. Daniel Spiewak's DSL for calling JRuby from Scala. Let's see how far I got...Hello World
First I want to compile
10 PRINT "Hello World" RUNThat shouldn't be difficult using implicit conversions.
class Command(val lineNumber:Int, code: =>Unit) { def execute = code } class NewStatement(line:Int) { def PRINT(msg:String) = addToMemory(new Command(line) { println(msg) }) } implicit def Int2NewStmt(line:Int) = new NewStatement(line) def RUN ...Note that there has to be a blank after the line numbers. This is not necessary for BASIC. Method
addToMemory
adds the Command
instance containing the code to run (println(msg)
) to a list for later execution by the mixed in RUN
method. (In case you are not familiar with Scala, the code that finally gets compiled for line number 10
looks like new NewStatement(10).PRINT("Hello World")
. Scala is not strict about dots and braces.) By adding PRINT(num:Int)
to NewStatement
10 PRINT 12 + 12 20 PRINT 144/12work as well. Unfortunately I am not able to implement
PRINT
's concatenating features, i.e.40 PRINT "a" ; "b" 50 PRINT "a" , "b"because
;
is the empty statement in Scala and parameter lists containing ,
need braces.My old friend where have you gone?
One of the first examples in the Commodore 64 Manual is
10 ? "Commodore 64" 20 GOTO 10
?
is just a shortcut for PRINT
. So addclass NewStatement(line:Int) { ... def ?(msg:String) = PRINT(msg) def GOTO(target:Int) = addToMemory(new Command(line) { lineCounter = target }) }The implementation of
RUN
uses lineCounter
to keep track of executing line numbers. When the Command
with GOTO
's code is executed, it changes the number of the next line to be run. Tada! Now I can GOTO
Scala ;-)Assignment and Evaluation
Let's tackle some basic assignments.
10 X% = 15 20 X = 23.5 30 X$ = "SOME STRING" 40 PRINT X 50 PRINT X$Using the same approach as before (with some refactoring of
addToMemory()
):class NewStatement(line:Int) { ... def X_=(d:Double) = { addToMemory(line) { variables.X = d } } def X:Double = throw new Error() def X$_=(s:String) = { addToMemory(line) { variables.X$ = s } } def X$:String = throw new Error() }The empty getters are necessary for Scala to recognise the methods as overridden bean properties and are never called. Although
%
is a valid Scala method name, it's not possible to suffix method names with it. So integer "variables" like def X%_=(i:Int)
can't be defined and line 10
will never compile. (One could escape `X%`
, but then the BASIC code would have to look like 10 `X%` = 15
.)For evaluation I need a method without parameters called
X
, in the scope of the current code, like RUN
. The method must not return the value of the variable because that would be too early. It should return code that yields the value of the variable when called (yes that's a function ;-).def X:Function0[Double] = new Function0[Double] { override def apply = variables.X } def X$:Function0[String] = new Function0[String] { override def apply = variables.X$ }The container
variables
holds the values of all variables during "BASIC runtime", i.e. when RUN
is called. To use the variables in PRINT
, it must accept these functions as arguments as well.class NewStatement(line:Int) { ... def PRINT(msg:Function0[String]) = { addToMemory(line) { println(msg()) } } }Variables
To allow arbitrary variables all variable names have to be predefined for assignment and for evaluation. That's impossible. But fortunately early basic used only two letters for variables.
[A-Z]
and [A-Z][A-Z0-9]
yield 962 valid names for each Double
and String
variables. So all these getters and setters could be generated. The generated trait VariableAssignment
with all setters is mixed into NewStatement
and the generated trait VariableEvaluation
with all getters is mixed into the main code. (Unfortunately the Scala compiler crashes with StackOverflowError
when mixing in the trait with its nearly 4000 methods.) The container variables
is better replaced with two maps, one for Double
and one for String
variables.Express Yourself
To add two variables, i.e. to add the values of two variable evaluations, I change all literals and
Function0
s to expressions (read custom function instances). This is a major refactoring but Scala makes it easy for me. Literals are still covered by implicit conversions.abstract class DoubleExpression { def value:Double def +(d:DoubleExpression) = new DoubleExpression { override def value = DoubleExpression.this.value + d.value } def *(d:DoubleExpression) = new DoubleExpression { override def value = DoubleExpression.this.value * d.value } } class LiteralDoubleExpression(val value:Double) extends DoubleExpression implicit def Double2DoubleExpression(value:Double) = new LiteralDoubleExpression(value) implicit def Int2DoubleExpression(value:Int) = new LiteralDoubleExpression(value.toDouble) trait VariableAssignment { def assign(name:String, d:DoubleExpression) = { addToMemory(line) { memory.set(name, d.value) } } def unused = throw new Error("never used") def X_=(d:DoubleExpression) = assign("X", d) def X:DoubleExpression = unused ... } class NewStatement(line:Int) extends VariableAssignment { ... def PRINT(msg:StringExpression) = addToMemory(line) { println(msg.value) } } trait VariableEvaluation { class VariableDoubleExpression(val fromVariable:String) extends DoubleExpression { override def value = memory.get(fromVariable) } def X = new VariableDoubleExpression("X") ... }The code above shows the changes needed for
Double
. Similar classes and methods are needed to cover StringExpression
s. Now let's examine what happens for 10 X = X + 1
: The method call X
of trait VariableEvaluation
returns a VariableDoubleExpression
(which will yield the value of "X"
when called) on which +
is invoked with the implicitly converted new LiteralDoubleExpression(1.0)
. The resulting DoubleExpression
is the argument for the setter method X_=
of trait VariableAssignment
in NewStatement
.Let it Flow (Control)
Till now the Scala BASIC lacks any flow control, e.g. a conditional
GOTO
like 40 IF X < 5 THEN 20
class NewStatement(line:Int) { ... def IF(expr: => Boolean) = { class ConsumeThen(expr: => Boolean) { def THEN(target:Int) = addToMemory { if (expr) ... // goto target } } new ConsumeThen(expr) } }Method
IF
consumes the boolean expression (as a function) and returns some instance of a class that only allows the following THEN
. Method THEN
in turn adds the code to execute the GOTO
to the list of Command
s. BASIC comparison operators are different from Scala's and I have to implement them in DoubleExpression
:abstract class DoubleExpression { ... def <(d:DoubleExpression) = value < d.value def <>(d:DoubleExpression) = value != d.value }The implementation of
FOR
is a bit more complex mainly because the BASIC "runtime" has to keep track of the current loop, so NEXT
statements are executed proper.10 FOR X = 1 TO 5 20 PRINT "COMMODORE 64" 30 NEXT XTo allow
1.TO(5)
I define a LoopRangeExpression
that handles the boundaries of the loop:class LoopRangeExpression(val lower:DoubleExpression) { var upper:DoubleExpression = null def TO(u:DoubleExpression) = { upper = u this } } implicit def Int2LoopRangeExpression(value:Int) = new LoopRangeExpression(new LiteralDoubleExpression(value.toDouble))Scala compiles the loop statement into
new NewStatement(10).FOR().(X()) = new LoopRangeExpression(new LiteralDoubleExpression(1.0)).TO(new LiteralDoubleExpression(5.0))
. That's why method TO
has to return its instance, to match the update
method (used for setting array elements). So FOR
has to return an object that defines update(variable: VariableDoubleExpression, range: LoopRangeExpression)
. Inside method update
all necessary code to run the loop is put into the BASIC runtime part. Both FOR
and NEXT
use DoubleExpression
to capture the variable names but never evaluate these expressions.class NewStatement(line:Int) { ... def FOR = { new Object() { def update(variable:VariableDoubleExpression, range:LoopRangeExpression) = { val usedVariable = variable.fromVariable addToMemory { ... // set usedVariable to range.lower.value } addLoop(usedVariable) { ... // increment and check condition } } } } def NEXT(d:VariableDoubleExpression) = addToMemory { ... // perform next for name d.fromVariable } }Functions
Support for functions like
INT()
or LEN$()
is simple to add. I use another trait BasicFunctions
, that defines the functions in terms of expressions, e.g.def INT(d:DoubleExpression) = new DoubleExpression { override def value = d.value.floor }Time for Refactoring
Although I factored out the traits of getters, setters and functions there is still too much stuff in the main class. Parsing BASIC should be separated from the logic that stores and executes the lines of code, the "runtime". I call this logic the
BasicMemory
.Trait
BasicParser
is the main parser, which mixes in VariableEvaluation
and BasicFunctions
. It contains the NewStatement
and all implicit conversions. Trait BasicMemory
contains the program code as list of Command
s and the logic needed to run them. The parser communicates with the runtime by using a small interface LinesOfCode
for adding commands, runtime and memory (read variable) operations. BasicApplication
is just for convenience, so BASIC programs may be written asobject Demo extends org.codecop.basic.BasicApplication { 10 PRINT "Hello World" RUN }The Whole Beast
The shown code was developed with Scala 2.7.7. Download the full source here. There is more implemented than covered in this post, e.g.
INPUT
, END
or STEP
. This little DSL has been an experiment. It's far from complete. It lacks important features like DATA/READ
or GOSUB/RETURN
. I reckon that supporting DIM
or DEFFN
would be difficult as well. And there are things that can't be done in Scala, e.g. the shown problem with %
. Still if this is interesting to people, I may do a proper release into an OSS project somewhere later.
2 comments:
http://www.cstechz.com/gw-basic-urdu-part-1-10-class/
Hi shahbaz ahmad!
Ah GW-Basic, I have not seen that for a while ;-)
But why not, I always like retro. I just guess being in language Ursu limits the audience quite a bit.
Post a Comment