katra
Version:
BASIC language interpreter in CoffeeScript
1,971 lines (1,709 loc) • 43.6 kB
text/coffeescript
#+--------------------------------------------------------------------+
#| katra.coffee
#+--------------------------------------------------------------------+
#| Copyright DarkOverlordOfData (c) 2013
#+--------------------------------------------------------------------+
#|
#| This file is a part of Katra
#|
#| Katra is free software; you can copy, modify, and distribute
#| it under the terms of the MIT License
#|
#+--------------------------------------------------------------------+
#
# The Katra AST Framework
#
rte = if window? then window.rte else require("./rte.node")
V_HP2000 = 0 # circa 1973 - StarTrek
V_ATARI = 1 # circa 1976 - Hunt the Wumpus
V_GWBASIC = 2 # circa 1979 - Eliza
GOSUB = 1 # Stack frame identifier: Gosub..Return
FOR = 2 # Stack frame identifier: For..Next
PHASE_SCAN = 0 # Processed during scan
PHASE_EXEC = 1 # Executable statements
MODE_REPL = 0 # Console REPL mode
MODE_RUN = 1 # Console RUN mode
PRINTF = /// # Printf style format parser
(\%) # pct - % escape
([-])? # flag left justify
([+ ])? # flag sign
([0])? # flag padding
(\d*)? # field width
(\.\d*)? # decimal precision
([\%ds]) # output specifier
///g
__con = null # console object
__fs = null # file system object
_ary = {} # arrays storage
_com = [] # common storage variables
_dat = [] # data statements
_dbg = false # trace on/off
_dp = 0 # data pointer
_eop = false # end of program flag
_fn = {} # user defined functions
_gw = false # use GW-Basic style strings
_mrk = {} # benchmarks
_nam = '' # workspace name
_ofs = 0 # option base offset for DIM
_pc = 0 # instruction counter
_prg = [] # executing code
_raw = {} # raw code as entered
_stk = [] # execution stack
_str = {} # strings storage
_txt = '' # source text
_var = {} # non-scalar variables storage
_ver = 0 # HP2000 | ATARI | GW
_wel = '' # console welcome message
_xrf = {} # line number in _raw[]
#
# Bind to the environment
#
# sets up the
#
# Console
# FileSystem
#
#
_bind = () ->
#
# @ resolves to the global object
#
Object.defineProperties @,
_con: get: -> if not __con? then __con = new Console(_wel) else __con
_fs: get: -> if not __fs? then __fs = new rte.FileSystem() else __fs
#
# Initialize the program memory
#
# @param [Bool] all if true, then clear code data also
# @return none
#
_init = ($all) ->
_ary = {}
_com = []
_dat = []
_dp = 0
_eop = false
_fn = {}
_mrk = {}
_ofs = 0
_pc = 0
_raw = {} if $all
_stk = []
_str = {}
_var = {}
_xrf = {}
#
# Eval
#
# @param [Mixed] the object to eval
# @return [Mixed] value
#
_eval = ($value) ->
if $value.eval? then $value.eval()
else $value
#
# Qualify the filename
#
# @param [String] name the raw file name
# @param [String] version basic: V_ATARI | V_GWBASIC | V_HP2000
# @return [String] the qualified file name
#
_qualify = ($name, $version = V_HP2000) ->
switch $version
when V_ATARI then 'bas/atari/'+$name
when V_GWBASIC then 'bas/gwbasic/'+$name
when V_HP2000
switch $name[0]
when "*" then 'bas/hp2k/group/'+$name[1..]
when "$" then 'bas/hp2k/system/'+$name[1..]
when "#" then 'bas/hp2k/test/'+$name[1..]
else 'bas/hp2k/'+$name
#
# Clean up the raw source code
#
# @param [String] code the raw source code
# @return [Array<String>]
#
_clean = ($code) ->
$code = $code[1..] if $code.charCodeAt(0) is 0xfeff # Skip BOM
$code = ($code + '\n') # make sure there is an ending LF
.replace(/\r/g, '\n') # replace CR's with LF's
.replace(/\n+/g, '\n') # remove duplicate LF's
#
# Save the source code
#
# @param [String] version basic: V_ATARI | V_GWBASIC | V_HP2000
# @param [String] name the file path/name
# @param [String] code the raw source code text
# @return [Void]
#
_save = ($version, $name, $data, $next) ->
$name = if $name[0] is '"' then $name[1...-1] else $name
_con.pause true
_fs.writeFile _qualify($name, $version), $data, () ->
$next?()
_con.pause false
true
#
# Load a program from storage
#
# @return true
#
_load = ($version, $name, $init=true, $next) ->
$name = if $name[0] is '"' then $name[1...-1] else $name
_init $init
_con.pause true
_fs.readFile _qualify($name, $version), ($err, $data) ->
if $err? then _con.println $err
else
$data = _clean($data).split('\n') # Get lines of text
$data.shift() if isNaN($data[0][0]) # Skip header
$data.shift() if $data[0] is "" # Skip blank line
_nam = if /^[A-Za-z]/.test($name) then $name else $name[1..]
_ver = $version
_gw = if _ver is V_GWBASIC then true else false
_txt = $data.join('\n')
$next?(_txt)
_parse _txt
_con.pause false
true
#
# Load & execute a program from storage
#
# @return true
#
_exec = ($version, $name, $init=true) ->
_init $init
_con.pause true
#_con.reset()
_fs.readFile _qualify($name, $version), ($err, $data) ->
if $err? then _con.println $err
else
$data = _clean($data).split('\n') # Get lines of text
$data.shift() if isNaN($data[0][0]) # Skip header
$data.shift() if $data[0] is "" # Skip blank line
_nam = if /^[A-Za-z]/.test($name) then $name else $name[1..]
_ver = $version
_gw = if _ver is V_GWBASIC then true else false
_txt = $data.join('\n')
_parse _txt
_start()
_run()
_con.reset()
_con.pause false
true
#
# Start running
#
# @return none
#
_start = () ->
#
# Load the raw code into the program buffer
#
_prg = []
for $lineno, $statement of _raw
while $lineno.length<4
$lineno = '0'+$lineno
_prg.push [$lineno, $statement]
_prg.sort()
#
# Initialize and create lineno cross reference
#
_init false
for [$lineno, $statement] in _prg
if $statement.code.type is PHASE_SCAN
$statement.code.eval()
_xrf[parseInt($lineno, 10)] = _pc++
_pc = 0
#
# Run a started program
#
# @return none
#
_run = () ->
$wait = false
_con.mode = MODE_RUN
try
until _eop or $wait
[$lineno, $statement] = _prg[_pc++]
$code = $statement.code
if $statement.code.type is PHASE_EXEC
_con.debug $lineno+' '+$code.toString() if _dbg
$wait = $code.eval()
_con.setPrompt $wait
_eop = true if _pc >= _prg.length
catch $e
_con.println $e
$wait = false
unless $wait
_con.mode = MODE_REPL
_con.println 'DONE'
#
# Chain the next program overlay
#
# @param [String] code the next program
# @return none
#
_chain = ($code) ->
$save = Array(_com.length)
#
# save common values
#
for $var, $ix in _com
switch $var.type
when 0 then $save[$ix] = _str[$var.name]
when 1 then $save[$ix] = _var[$var.name]
when 2 then $save[$ix] = _ary[$var.name]
_init true
_parse $code
_start()
#
# restore common from saved values
#
for $var, $ix in _com
switch $var.type
when 0 then _str[$var.name] = $save[$ix]
when 1 then _var[$var.name] = $save[$ix]
when 2 then _ary[$var.name] = $save[$ix]
_run()
#
# Parse
#
# @param [String] code basic code to parse
# @return true if successful, else false
#
_parse = ($code) ->
kc = if window? then window.kc else require("./kc")
$code = $code.split('\n')
#
# BASIC is a square peg,
# BNF is a round hole.
# To make it fit, sand the edges:
#
for $line, $index in $code
#
# Replace IF ...= with IF ...==
#
if /^\d*\s*IF/i.test $line
$code[$index] = $line = _fixup_if($line)
#
# Insert missing semicolons in PRINT statements
#
if /^\d*\s*PRINT/i.test $line
$code[$index] = $line = _fixup_print($line)
#
# Remove trailing comments (GWBASIC) ...
#
if /\'(?=[^"]*(?:"[^"]*"[^"]*)*$)/.test $line
$code[$index] = $line = $line.replace(/(\'.*(?=[^"]*(?:"[^"]*"[^"]*)*$))/g, "")
#
# Change exponent operator ** to ^
#
if /\*\*(?=[^"]*(?:"[^"]*"[^"]*)*$)/.test $line
$code[$index] = $line = $line.replace(/(\*\*(?=[^"]*(?:"[^"]*"[^"]*)*$))/g, "^")
try
kc.parse $code.join('\n')
catch $e
_con.debug String($e)
return true
#
# Fix If
#
# Fixup Conditionals
#
# = to ==
# # to <>
#
# @param [String] line line of basic code
# @retun [String] the cleansed string
#
_fixup_if = ($line) ->
$line = $line.split(/THEN/i)
$line[0] = $line[0]
.replace(/\=/g, '==')
.replace(/\<\=\=/g, '<=')
.replace(/\>\=\=/g, '>=')
.replace(/\#/g, '<>') # ATARI Basic
$line.join(" THEN ")
#
# Fix Print
#
# Add missing ;'s to print lists
#
# @param [String] line line of basic code
# @retun [String] the cleansed string
#
_fixup_print = ($line) ->
SEP = ';:,' # print list separators
$is_string = false
$match = ($chunk for $chunk in $line.match(/[^"]*(?!\\"[^"]*\")/g) when $chunk isnt '')
#
# Chunks alternate unquoted / quoted
#
for $chunk, $index in $match
$sep = if /^\d*\s*PRINT\s*$/i.test($chunk) or $index is $match.length-1 then '' else ';'
if $is_string
#
# put the quotes back
#
$match[$index] = $chunk = '"'+$chunk+'"'
#
# if the next chunk doesn't start with a separator
# then add one to this chunk
#
if SEP.indexOf($match[$index+1]?[0] ? '') is -1
$match[$index] = $chunk+$sep
else
#
# if this chunk doesn't end with a separator
# then add one
#
if SEP.indexOf($chunk.substr(-1)) is -1
$match[$index] = $chunk+$sep
$is_string = not $is_string
$match.join('')
#
# allocate an array
#
# @param [Mixed] init value to initialize each cell
# @param [Number] dim1 size of the 1st dimension
# @param [Number] dim2 size of the 2nd dimension
# @return [Array] the new array
#
_dim = ($init, $dim1, $dim2) ->
$a = []
switch arguments.length
when 2
for $i in [_ofs...$dim1+1]
$a[$i] = $init
when 3
for $i in [_ofs...$dim1+1]
$a[$i] = []
for $j in [_ofs...$dim2+1]
$a[$i][$j] = $init
return $a
#
# flatten a nested list
#
# @param [Array] list nested list
# @return [Array] the flattened list
#
_flatten = ($list) ->
return [] unless $list?
$a = []
for $item in $list
if Array.isArray($item)
$a = $a.concat _flatten($item)
else
$a.push $item
return $a
#
# printf style output
#
# @param [String] fmt printf style format string
# @param [Array] list list of args
# @return [String] the formatted output
#
_sprintf = ($fmt, $list) ->
$count = 0
#
# format each print spec/value pair
#
_foreach = ($match, $pct, $just, $sign, $pad=' ', $width, $prec, $spec, $ofset, $string) ->
$value = String($list[$count++])
switch $spec
#
# %% = Escaped %
#
when '%'
'%'
#
# String format
#
when 's'
if $width?
$width = parseInt($width, 10)
if $value.length < $width
if $just? # left
return Array($width-$value.length+1).join($pad) + $value
else # right
return $value + Array($width-$value.length+1).join($pad)
$value
#
# Number format
#
when 'd'
if $width?
$width = parseInt($width, 10)
if $value.length < $width
if $just? # left
return $value + Array($width-$value.length+1).join($pad)
else # right
return Array($width-$value.length+1).join($pad) + $value
$value
$fmt.replace(PRINTF, _foreach)
#
# Construct a printf style format string from an IMAGE
#
# @param [Array] image
# @return [Array] the printf style format string
#
_format = ($image = []) ->
$out = ''
$count = 1
while $image.length > 0
$head = $image.shift()
if isNaN($head)
switch $head
when ','
$count = 1
when 'D'
$out += if $count > 1 then '%'+ $count + 'd' else '%d'
$count = 1
when 'A'
$out += if $count > 1 then '%'+ $count + 's' else '%s'
$count = 1
when 'X'
$out += Array($count+1).join(' ')
$count = 1
when '('
$out += Array($count+1).join(_format($image))
$count = 1
when ')'
return $out
else
$out += $head[1...-1]
$count = 1
else
$count = $head
return $out
#
# Pad string to length
#
# @param [String] value
# @param [Integer] len
# @param [String] pad char
# @return [String] the padded string
#
_pad = ($value, $len, $pad = ' ') ->
$right = not isNaN($value) # right justify numerics
$value = String($value) # ensure that we work with a string
if $right
if $value.length < $len
Array($len-$value.length+1, $pad) + $value
else
$value.substr(0, $len)
else
if $value.length < $len
$value + Array($len-$value.length+1, $pad)
else
$value.substr(0, $len)
#
# Set a benchmark
#
# Mark the time at name
#
# @param [String] name name of the marker
# @return [Void]
#
_mark = ($name) ->
_mrk[$name] = new Date()
#
# Elapsed Time
#
# Returns the time elapsed between two markers
#
# @param [String] point1 a particular marked point
# @param [String] point2 a particular marked point
# @return [Integer]
#
_elapsed_time = ($point1, $point2) ->
return 0 if not _mrk[$point1]?
_mrk[$point2] = new Date() if not _mrk[$point2]?
_mrk[$point2] - _mrk[$point1]
#
# ZER
#
Zer =
eval: -> 0
toString: -> 'ZER'
#
# CON
#
Con =
eval: -> 1
toString: -> 'CON'
Semic =
eval: -> ''
toString: -> ';'
Comma =
eval: -> ' '
toString: -> ','
#
# Abstract Operator class
#
class Operator
#
# Set Operator properties
#
# @param [String] left part to the left of the operator
# @param [String] right part to the right of the operator
# @return [Void]
#
constructor: (@left, @right) ->
#
# Abstract Built-In function class
#
class BuiltIn
#
# Set expression properties
#
# @param [String] $0 1st param
# @param [String] $1 optional 2nd param
# @param [String] $2 optional 3rd param
# @return [Void]
#
constructor: (@$0, @$1, @$2) ->
toString: -> "#{@constructor.name.toUpperCase()}(#{@$0})"
#
# Abstract Keyword class
#
class Keyword
type: PHASE_EXEC
eval: -> false
#
# Implement the abstract Console class
# by overriding the handler method
#
class Console extends rte.Console
#
# @property [Integer] mode - repl or program execution?
#
mode: MODE_REPL
exec: true
constructor: ($welcome) ->
@welcomeMessage = $welcome
super()
#
# callback to handle interrupt
#
# @return none
#
cancelHandle: () ->
_eop = true
_con.print '^C'
_con.reset()
_con.setPrompt false
_run()
_con.console.scrollToBottom()
#
# callback to handle the input
#
# @param [String] line the line that was wntered
# @return none
#
commandHandle: ($line) =>
switch @mode
#
# Input statement
#
when MODE_RUN
for $item in $line.trim().split(",")
@buffer.push if isNaN($item) then String($item) else Number($item)
if @buffer.length < @vars.length
@continuedPrompt = true
return
else
for $name, $ix in @vars
if /\$$/.test($name)
_str[$name] = @buffer[$ix]
else
_var[$name] = @buffer[$ix]
@continuedPrompt = false
_run()
return true
#
# Interacive (REPL)
#
when MODE_REPL
$line = if /\n$/.test($line) then $line else "#{$line}\n"
_parse $line
#
# Bind to the environmet
#
_bind()
#
# The Katra Public API
#
katra =
#
# main entry point
#
# @param [Array<String>] args
# @return [Void]
#
main: ($args) ->
_wel = $args.title ? _wel
switch $args.basic
when 'atari' then _exec V_ATARI, $args.program
when 'gwbasic' then _exec V_GWBASIC, $args.program
when 'hp2k' then _exec V_HP2000, $args.program
else _con.reset()
#
# Set the file system root
#
# @param [String] root the path to root the file system at
# @return [Void]
#
setRoot: ($root) ->
_fs.setRoot $root
#
# Get the raw program text
#
# @return [String] the program text
#
getText: () ->
_txt
#
# Command
#
# Basic commands for manipulating the code
# Commands cannot occur in a program.
#
command:
#
# Load a program from file storage and append to current
#
# @param [String] file program filename
# @return none
#
append: ($0) ->
_load V_HP2000, $0.split('-')[1], false
#
# Load an atari basic program from file storage
#
# @param [String] file program filename
# @return none
#
atari: ($0, $next) ->
_load V_ATARI, $0, true, $next
#
# Display the folder contents
#
# @param [String] folder to list
# @return none
#
cat: ($dir) ->
$nc = 4 # Number of columns in display
$cw = 20 # Column width
$hdr = 'name '
_fs.readDir $dir, ($files) ->
$col = 0
_con.hilite "\n#{$dir}\n#{Array($nc+1).join($hdr)}"
for $file in $files
$file = $file.split('.')[0]
$file += " " while $file.length < $cw
_con.print $file
_con.println() if ($col++) % $nc is $nc-1
_con.print "\n#{_con.prompt}" unless window?
#
# Clears the console display
#
# @return none
#
cls: () ->
_con.clear()
#
# Delete line(s)
#
# @param [Number] start starting line number
# @param [Number] end ending line number
# @return none
#
del: ($0) ->
[$start, $end] = $0.split('-')[1].split(',')
$end = $start unless $end
for $lineno in [$start...$end]
if _raw[$lineno]? then delete _raw[$lineno]
#
# Dir
#
# list gwbasic folder
#
# @param [Number] start starting line number
# @param [Number] end ending line number
# @return none
#
dir: ($0) ->
return
#
# Load a program from file storage and run
#
# @param [String] file program filename
# @return none
#
exec: ($0) ->
_exec V_HP2000, $0.split('-')[1]
#
# Files
#
# list atari folder
#
# @param [Number] start starting line number
# @param [Number] end ending line number
# @return none
#
files: ($0) ->
return
#
# Load a program from file storage
#
# @param [String] file program filename
# @return none
#
get: ($0, $next) ->
_load V_HP2000, $0.split('-')[1], true, $next
#
# Load a gwbasic program from file storage
#
# @param [String] file program filename
# @return none
#
gwbasic: ($0, $next) ->
_load V_GWBASIC, $0, true, $next
#
# List line(s)
#
# @param [Number] start starting line number
# @param [Number] end ending line number
# @return none
#
list: ($0) ->
$1 = $0.split('-')[1]
[$start, $end] = $1.split(',') if $1?
if $start?
$end = $end ? $start
$start = parseInt($start, 10)
$end = parseInt($end, 10)
else
$start = 1
$end = 9999
$lines = []
for $lineno, $statement of _raw
while $lineno.length<5
$lineno = '0'+$lineno
$lines.push [$lineno, $statement]
$lines.sort()
for [$lineno, $statement] in $lines
$lineno = parseInt($statement.lineno, 10)
$code = $statement.code
if $start?
if $lineno >= parseInt($start, 10) and $lineno <= parseInt($end, 10)
_con.println $lineno+' '+$code
else
_con.println $lineno+' '+ $code
#
# Name the program in local storage
#
# @param [String] file program filename
# @return none
#
name: ($0) ->
_nam = $0.split('-')[1]
#
# Delete a program from local storage
#
# @param [String] file program filename
# @return none
#
purge: ($0) ->
_fs.deleteFile _qualify($0.split('-')[1], _ver), ($err) ->
if $err? then _con.println $err
#
# Exit Katra
#
quit: () ->
process?.exit()
#
# Renumber line(s)
#
# @param [Number] start starting line number
# @param [Number] end ending line number
# @return none
#
renum: ($0) ->
_con.println "Renumber Not Implemented"
#
# Run the program
#
# @param [String] lineno to start at
# @return none
#
run: ($0) ->
if Object.keys(_raw).length>0
_start()
_run()
#
# Save the program
#
# @return none
#
save: () ->
return _con.println "Name not set" if _nam is ''
$lines = []
$text = ''
for $lineno, $statement of _raw
$lines.push [$lineno, $statement.code]
$lines.sort()
for [$lineno, $code] in $lines
$text += $lineno+' '+$code+'\n'
_save _ver, _nam, $text[0...-1], ($err) ->
if $err? then _con.println $err
#
# Scratch memory
#
scr: () ->
_init true
#
# Tron|Troff
#
troff: () -> _dbg = false
tron: () -> _dbg = true
#
# Ast Nodes
#
# Basic keyword definitions
#
keyword:
#
# Predefined constants:
#
Zer: Zer
Con: Con
Semic: Semic
Comma: Comma
#
# A single Basic statement
#
Statement: class Statement
#
# Set statement properties
#
# @param [Number] lineno the statement label
# @param [Object] code the executable part
# @return none
#
constructor: ($code, $lineno) ->
if $lineno?
_raw[$lineno] =
lineno: $lineno
code: $code
else
$code?.eval?()
#
# Constant
#
Const: class Const
#
# Create a new Constant
#
# @param value
#
constructor: (@value) ->
@is_string = if 'string' is typeof @value then true else false
if @is_string
if @value.charAt(0) is '"'
@value = @value[1..-2]
#
# Get the constant value
#
value: -> @value
#
# Get the constant value
#
eval: -> @value
#
# @return the string representaion
#
toString: ->
if @is_string then "\"#{@value}\"" else "#{@value}"
#
# Variable
#
Var: class Var
#
# Create a new Variable reference
#
# @param name the variable name
# @param delim array delimiter (optional)
# @param dims array imdex (optional)
#
constructor: (@name, $delim, $dims) ->
@is_string = /\$$/.test(@name)
if $delim?
@is_array = true
@dims = _flatten($dims)
@dim1 = @dims[0]
@dim2 = @dims[1]
else @is_array = false
#
# Set the variable value
#
let: ($value) ->
if @is_string
if _gw
if @dim2?
$dim1 = @dim1.eval()
$dim2 = @dim2.eval()
_str[@name][$dim1][$dim2] = $value
else if @dim1?
$dim1 = @dim1.eval()
_str[@name][$dim1] = $value
else
_str[@name] = $value
else
if @dim2?
$start = @dim1.eval()-1
$end = @dim2.eval()
if $end < $start
throw 'Invalid String index: '+@toString()
$len = $end-$start
$value = $value.substr(0,$len)
$value = _pad($value, $len)
$str = _str[@name]
_str[@name] = $str.substr(0,$start)+$value+$str.substr($end)
else if @dim1?
$start = @dim1.eval()-1
$str = _str[@name]
_str[@name] = $str.substr(0,$start)+$value+$str.substr($start+$value.length)
else
$len = _str[@name].length
if $value.length < $len
$value += Array($len-$value.length+1).join(' ')
_str[@name] = $value
else if @dim2?
$dim1 = @dim1.eval()
$dim2 = @dim2.eval()
_ary[@name][$dim1][$dim2] = $value
else if @dim1?
$dim1 = @dim1.eval()
_ary[@name][$dim1] = $value
else
_var[@name] = $value
#
# Get the variable value
#
eval: () ->
if @is_string
if _gw
if @dim2?
$dim1 = @dim1.eval()
$dim2 = @dim2.eval()
_str[@name]?[$dim1]?[$dim2] ? ''
else if @dim1?
$dim1 = @dim1.eval()
_str[@name]?[$dim1] ? ''
else
_str[@name] ? ''
else
if @dim2?
$start = @dim1.eval()-1
$end = @dim2.eval()
if $end < $start
throw 'Invalid String index: '+@toString()
_str[@name]?.slice($start, $end) ? ''
else if @dim1?
$start = @dim1.eval()-1
_str[@name]?.slice($start) ? ''
else
_str[@name] ? ''
else if @dim2?
$dim1 = @dim1.eval()
$dim2 = @dim2.eval()
_ary[@name]?[$dim1]?[$dim2] ? 0
else if @dim1?
$dim1 = @dim1.eval()
_ary[@name]?[$dim1] ? 0
else
_var[@name] ? 0
#
# @return the string representaion
#
toString: ->
if @is_array
"#{@name}[#{@dims.join(',')}]"
else @name
#
# Basic Command Keywords:
#
#
# Base n
#
Base: class Base extends Keyword
#
# Set the option base
#
# @param base index offset 0 or 1
#
constructor: (@base) ->
#
# Execute
#
eval: ->
_ofs = @base
return false
#
# @return the string representaion
#
toString: ->
"BASE #{@base}"
#
# Chain "filename"
#
Chain: class Chain extends Keyword
constructor: (@program) ->
eval: ->
_con.pause true
_fs.readFile @program, ($err, $data) ->
if $err?
_con.println $err
else
_ver = $data.type
_nam = $data.name
_gw = if _ver is V_GWBASIC then true else false
_chain $data.data
_con.pause false
#
# @return the string representaion
#
toString: ->
"CHAIN \"#{@program}\""
#
# Com A(1)
# Com A(1,1)
# Com A$(1)
#
Com: class Com extends Keyword
type: PHASE_SCAN # Execute during scan phase
constructor: ($vars) ->
@vars = _flatten($vars)
eval: ->
for $var in @vars
if /\$$/.test($var.name)
if _gw
if $var.dims.length is 0
_str[$var.name] = ''
else
_str[$var.name] = _dim('', $var.dims...)
else
_str[$var.name] = Array($var.dims[0]+1).join(' ')
_com.push
type: 0
name: $var.name
else
if $var.dims.length is 0
_var[$var.name] = 0
_com.push
type: 1
name: $var.name
else
_ary[$var.name] = _dim(0, $var.dims...)
_com.push
type: 2
name: $var.name
return false
#
# @return the string representaion
#
toString: ->
"COM #{@vars.join(', ')}"
#
# Data v1, v2, ... vN
#
Data: class Data extends Keyword
type: PHASE_SCAN # Execute during scan phase
constructor: ($data) ->
@data = _flatten($data)
eval: ->
if _dat is null then _dat = []
_dat = _dat.concat @data
return false
#
# @return the string representaion
#
toString: ->
"DATA #{@data.join(', ')}"
#
# Def FNA(P) = X
#
Def: class Def extends Keyword
type: PHASE_SCAN # Execute during scan phase
constructor: (@name, @par, @body) ->
eval: ->
_fn[@name] = ($par) =>
$tmp = _var[@par] # variable goes out of scope,
_var[@par] = $par # occluded by the local param
$ret = @body.eval() # while we execute the function body
_var[@par] = $tmp # variable returns to scope
$ret
return false
#
# @return the string representaion
#
toString: ->
"DEF #{@name}(#{@par}) = #{@body}"
#
# Dim A(1)
# Dim A(1,1)
# Dim A$(1)
#
Dim: class Dim extends Keyword
type: PHASE_SCAN # Execute during scan phase
constructor: ($vars) ->
@vars = _flatten($vars)
eval: ->
for $var in @vars
if /\$$/.test($var.name)
if _gw
if $var.dims.length is 0
_str[$var.name] = ''
else
_str[$var.name] = _dim('', $var.dims...)
else
_str[$var.name] = Array($var.dims[0]+1).join(' ')
else
if $var.dims.length is 0
_var[$var.name] = 0
else
_ary[$var.name] = _dim(0, $var.dims...)
return false
#
# @return the string representaion
#
toString: ->
"DIM #{@vars.join(', ')}"
#
# End
#
End: class End extends Keyword
eval: ->
_eop = true
return false
#
# @return the string representaion
#
toString: ->
"END"
#
# Enter
#
Enter: class Enter extends Keyword
constructor: (@port, @time, @status, @var) ->
unless @var?
[@port, @time, @status, @var] = [null, @port, @time, @status]
eval: ->
_con.input '', [@var]
return true
#
# @return the string representaion
#
toString: ->
"ENTER #{@port}, #{@time}, #{@status}, #{@var}"
#
# For VAR = start To end
# For VAR = start To end Step n
#
For: class For extends Keyword
constructor: (@var, @start, @end, @step=new Const(1)) ->
eval: ->
_var[@var] = _eval(@start)
_stk.push {
id: FOR
pc: _pc
name: @var
end: @end
step: @step
}
return false
#
# @return the string representaion
#
toString: ->
if @step is 1
"FOR #{@var} = #{@start} TO #{@end}"
else
"FOR #{@var} = #{@start} TO #{@end} STEP #{@step}"
#
# Goto line
# Goto expression Of line1, line2, ...
#
Goto: class Goto extends Keyword
constructor: (@lineno, $of) ->
@of = _flatten($of)
eval: ->
if @of.length>0
$index = _eval(@lineno)-1
if @of[$index]?
_pc = _xrf[@of[$index]]
else
_pc = _xrf[parseInt(@lineno, 10)]
return false
#
# @return the string representaion
#
toString: ->
if @of.length>0?
"GOTO #{@lineno} OF #{@of.join(',')}"
else
"GOTO #{@lineno}"
#
# Gosub line
#
Gosub: class Gosub extends Goto
constructor: (@lineno, $of) ->
@of = _flatten($of)
eval: ->
_stk.push {
id: GOSUB
pc: _pc
}
super()
#
# @return the string representaion
#
toString: ->
if @of.length>0?
"GOSUB #{@lineno} OF #{@of.join(',')}"
else
"GOSUB #{@lineno}"
#
# If <Cond> Then line
#
If: class If extends Keyword
constructor: (@cond, @then) ->
eval: ->
if @cond.eval()
if @then.eval?
@then.eval()
else
_pc = _xrf[parseInt(@then, 10)]
return false
#
# @return the string representaion
#
toString: ->
"IF #{@cond} THEN #{@then}"
#
# Image
#
Image: class Image extends Keyword
constructor: ($format = []) ->
@source = _flatten($format)
@format = _format(@source)
eval: ->
return false
#
# @return the string representaion
#
toString: ->
"IMAGE #{@source.join('')}"
#
# Input var1, var2, ... varN
# Input "Prompt", var1, var2, ... varN
#
Input: class Input extends Keyword
constructor: ($vars, @prompt) ->
@vars = _flatten($vars)
eval: ->
_con.input(@prompt, @vars)
return true
#
# @return the string representaion
#
toString: ->
if @prompt?
"INPUT #{@prompt}, #{@vars.join(',')}"
else
"INPUT #{@vars.join(',')}"
#
# Let VAR= <VAR= > value
#
Let: class Let extends Keyword
constructor: ($vars, @value) ->
@vars = []
for $var in _flatten($vars)
if 'string' is typeof $var
@vars.push new Var($var)
else
@vars.push $var
eval: ->
$value = _eval(@value)
for $var in @vars
$var.let $value
return false
#
# @return the string representaion
#
toString: ->
$s = ''
for $var in @vars
$s += $var + ' = '
"LET #{$s}#{@value}"
#
# Mat VAR = value
#
Mat: class Mat extends Keyword
constructor: (@var, @value) ->
eval: ->
$value = @value.eval()
if _ary[@var]?
$i = _ary[@var].length
$j = _ary[@var][_ofs].length
_ary[@var] = _dim($value, $i, $j)
else
_ary[@var] = _dim($value, 10)
return false
#
# Mat Read var1, var2, ... varN
#
MatRead: class MatRead extends Keyword
constructor: ($vars) ->
@vars = _flatten($vars)
eval: ->
for $var in @vars
if _dp < _dat.length
$var.let _dat[_dp++].value
else
$var.let undefined
return false
#
# @return the string representaion
#
toString: ->
"MAT READ #{@vars.join(',')}"
#
# Next VAR
#
Next: class Next extends Keyword
constructor: (@var) ->
eval: ->
$frame = _stk[_stk.length-1]
if $frame.id isnt FOR
throw "Next without for"
$name = @var.name
if $frame.name isnt $name
throw "Mismatched For/Next #{$name}"
$step = _eval($frame.step)
$counter = @var.eval() + $step
@var.let $counter
if $step < 0
if $counter < _eval($frame.end)
_stk.pop()
else
_pc = $frame.pc
else
if $counter > _eval($frame.end)
_stk.pop()
else
_pc = $frame.pc
return false
#
# @return the string representaion
#
toString: ->
"NEXT #{@var}"
#
# Print item1, item2, ... itemN
#
Print: class Print extends Keyword
constructor: ($items...) ->
@items = _flatten([$items])
eval: ->
$str = ''
for $item in @items
$str += if isNaN($val = _eval($item)) then $val else ' '+$val
if $item in [Semic, Comma]
_con.print $str
else
_con.println $str
return false
#
# @return the string representaion
#
toString: ->
$str = ''
for $item in @items
$str += $item.toString()
"PRINT #{$str}"
#
# Print Using line;item1, item2, ... itemN
#
Using: class Using extends Keyword
constructor: (@lineno, $items...) ->
@items = _flatten($items)
eval: ->
$i = _xrf[@lineno]
[$lineno, $statement] = _prg[$i]
$args = []
for $item in @items
$args.push _eval($item)
if $item in [Semic, Comma]
_con.print _sprintf($statement.code.format, $args)
else
_con.println _sprintf($statement.code.format, $args)
return false
#
# @return the string representaion
#
toString: ->
if @items.length is 0
"PRINT USING #{@lineno}"
else
$str = ''
for $item in @items
$str += $item.toString()+','
$str = $str[0...-1]
"PRINT USING #{@lineno};#{$str}"
#
# Randomize
#
Randomize: class Randomize extends Keyword
constructor: ->
eval: ->
return false
#
# @return the string representaion
#
toString: ->
"RANDOMIZE"
#
# Read var1, var2, ... varN
#
Read: class Read extends Keyword
constructor: ($vars) ->
@vars = _flatten($vars)
eval: ->
for $var in @vars
if _dp < _dat.length
$var.let _dat[_dp++].value
else
$var.let undefined
return false
#
# @return the string representaion
#
toString: ->
"READ #{@vars.join(',')}"
#
# Restore
#
Restore: class Restore extends Keyword
constructor: (@lineno) ->
eval: ->
_dp = 0
return false
#
# @return the string representaion
#
toString: ->
if @lineno? then "RESTORE #{@lineno}" else "RESTORE"
#
# Return
#
Return: class Return extends Keyword
constructor: ->
eval: ->
$frame = _stk.pop()
while $frame.id isnt GOSUB
$frame = _stk.pop()
_pc = $frame.pc
return false
#
# @return the string representaion
#
toString: ->
"RETURN"
#
# Rem ...
#
Rem: class Rem extends Keyword
constructor: ($text) ->
@text = $text.replace(/^REM/i, '')
eval: -> return false
#
# @return the string representaion
#
toString: ->
"REM#{@text}"
#
# Stop
#
Stop: class Stop extends Keyword
constructor: ->
eval: ->
_eop = true
return false
#
# @return the string representaion
#
toString: ->
"STOP"
#
# Basic Operators:
# used by BNF grammars
#
Max: class Max extends Operator
eval: -> Math.max(_eval(@left), _eval(@right))
toString: -> "#{@left} MAX #{@right}"
Min: class Min extends Operator
eval: -> Math.min(_eval(@left), _eval(@right))
toString: -> "#{@left} MIN #{@right}"
#
# +
#
Add: class Add extends Operator
eval: -> _eval(@left) + _eval(@right)
toString: -> "#{@left} + #{@right}"
#
# -
#
Sub: class Sub extends Operator
eval: -> _eval(@left) - _eval(@right)
toString: -> "#{@left} - #{@right}"
#
# *
#
Mul: class Mul extends Operator
eval: -> _eval(@left) * _eval(@right)
toString: -> "#{@left} * #{@right}"
#
# /
#
Div: class Div extends Operator
eval: -> _eval(@left) / _eval(@right)
toString: -> "#{@left} / #{@right}"
#
# ^
#
Pow: class Pow extends Operator
eval: -> Math.pow(_eval(@left), _eval(@right))
toString: -> "#{@left} ^ #{@right}"
#
# OR
#
OR: class OR extends Operator
eval: -> _eval(@left) or _eval(@right)
toString: -> "#{@left} OR #{@right}"
#
# AND
#
AND: class AND extends Operator
eval: -> _eval(@left) and _eval(@right)
toString: -> "#{@left} AND #{@right}"
#
# NOT
#
NOT: class NOT extends Operator
eval: -> not _eval(@left)
toString: -> "NOT #{@left}"
#
# <
#
LT: class LT extends Operator
eval: -> _eval(@left) < _eval(@right)
toString: -> "#{@left} < #{@right}"
#
# >
#
GT: class GT extends Operator
eval: -> _eval(@left) > _eval(@right)
toString: -> "#{@left} > #{@right}"
#
# <=
#
LE: class LE extends Operator
eval: -> _eval(@left) <= _eval(@right)
toString: -> "#{@left} <= #{@right}"
#
# >=
#
GE: class GE extends Operator
eval: -> _eval(@left) >= _eval(@right)
toString: -> "#{@left} >= #{@right}"
#
# ==
#
EQ: class EQ extends Operator
eval: -> if _eval(@left) is _eval(@right) then true else false
toString: -> "#{@left} = #{@right}"
#
# !=
#
NE: class NE extends Operator
eval: -> if _eval(@left) isnt _eval(@right) then true else false
toString: -> "#{@left} <> #{@right}"
#
# User Defined Functions
#
FN: class FN
constructor: (@name, @parm) ->
eval: -> _fn[@name](_eval(@parm))
toString: -> "#{@name}(#{@parm})"
#
# Basic Builtin Functions
#
ABS: class ABS extends BuiltIn
eval: -> Math.abs(_eval(@$0))
ATN: class ATN extends BuiltIn
eval: -> Math.atan(_eval(@$0))
COS: class COS extends BuiltIn
eval: -> Math.cos(_eval(@$0))
EXP: class EXP extends BuiltIn
eval: -> Math.exp(_eval(@$0))
INT: class INT extends BuiltIn
eval: -> Math.floor(_eval(@$0))
LEN: class LEN extends BuiltIn
eval: -> _eval(@$0).length
LIN: class LIN extends BuiltIn
eval: -> Array(Math.abs(_eval(@$0))+1).join('\n')
LOG: class LOG extends BuiltIn
eval: -> Math.log(_eval(@$0))
RND: class RND extends BuiltIn
eval: -> Math.random()
SGN: class SGN extends BuiltIn
eval: ->
$0 = _eval(@$0)
if $0 < 0 then -1 else if $0 > 0 then 1 else 0
SIN: class SIN extends BuiltIn
eval: -> Math.sin(_eval(@$0))
SPA: class SPA extends BuiltIn
eval: -> Array(_eval(@$0)).join(" ")
SQR: class SQR extends BuiltIn
eval: -> Math.sqrt(_eval(@$0))
TAB: class TAB extends BuiltIn
eval: -> Array(Math.floor(_eval(@$0))).join(" ")
TAN: class TAN extends BuiltIn
eval: -> Math.tan(_eval(@$0))
TIM: class TIM extends BuiltIn
eval: ->
if _eval(@$0) is 0 then (new Date()).getMinutes() else (new Date()).getSeconds()
LCASE: class LCASE extends BuiltIn
eval: -> _eval(@$0).toLowerCase()
toString: -> "LCASE(#{@$0}, #{@$1}, #{@$2})"
LEFT: class LEFT extends BuiltIn
eval: -> _eval(@$0).substr(0, _eval(@$1)-1)
toString: -> "LEFT(#{@$0}, #{@$1}, #{@$2})"
MID: class MID extends BuiltIn
eval: -> _eval(@$0).substring(_eval(@$1), _eval(@$2))
toString: -> "MID(#{@$0}, #{@$1}, #{@$2})"
RIGHT: class RIGHT extends BuiltIn
eval: -> _eval(@$0).substr(_eval(@$1)-1)
toString: -> "RIGHT(#{@$0}, #{@$1}, #{@$2})"
SUBSTR: class SUBSTR extends BuiltIn
eval: -> _eval(@$0).substr(_eval(@$1)-1, _eval(@$2))
toString: -> "SUBSTR(#{@$0}, #{@$1}, #{@$2})"
UCASE: class UCASE extends BuiltIn
eval: -> _eval(@$0).toUpperCase()
toString: -> "UCASE(#{@$0}, #{@$1}, #{@$2})"
if window? then window.katra = katra else module.exports = katra