├── .gitignore ├── doc ├── TBASMAN.pdf ├── intro.tex ├── implementation.tex ├── commandsstatements.tex ├── errors.tex ├── concepts.tex ├── thelanguage.tex ├── theinterpreter.tex ├── TBASMAN.tex ├── functions.tex └── operators.tex ├── samples ├── CONSTEST.BAS ├── GOSUB.BAS ├── NUMGUESS.BAS ├── 99BEERS.BAS └── test_suite.bas ├── LICENSE.md ├── TBASEXTN.lua ├── Expanding the Language.md ├── tests └── tokenisertest.lua ├── README.md ├── TBASIC.lua ├── TBASEXEC.lua └── TBASINCL.lua /.gitignore: -------------------------------------------------------------------------------- 1 | doc/TBASMAN.aux 2 | doc/TBASMAN.log 3 | doc/TBASMAN.out 4 | doc/TBASMAN.toc 5 | .idea/ -------------------------------------------------------------------------------- /doc/TBASMAN.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curioustorvald/terran-basic-lua/HEAD/doc/TBASMAN.pdf -------------------------------------------------------------------------------- /samples/CONSTEST.BAS: -------------------------------------------------------------------------------- 1 | 1 PRINT "MATH CONST TEST SUITE" 2 | 2 PRINT 3 | 3 PRINT 4 | 10 PRINT "CONST PI CAN BE CALCULATED FROM 'RAD(180)'" 5 | 20 PRINT("RAD(180)", RAD(180)) 6 | 30 PRINT("CONST M_PI", M_PI) 7 | 40 PRINT 8 | 50 PRINT "CONST TAU (TWO-PI) CAN BE CALCULATED FROM 'RAD(360)'" 9 | 60 PRINT("RAD(360)", RAD(360)) 10 | 70 PRINT("CONST M_2PI", M_2PI) 11 | 80 PRINT 12 | 90 PRINT "IF WE GOT EULER CONSTANT 'E' RIGHT, LOG(M_E) MUST BE EQUAL TO OR VERY CLOSE TO 1." 13 | 100 PRINT("LOG(M_E)", LOG(M_E)) 14 | 110 PRINT 15 | 120 PRINT "IF WE MULTIPLY TWO SQUARE ROOT OF 2'S, WE MUST GET EXACTLY OR VERY CLOSELY 2." 16 | 130 PRINT("M_ROOT2^2", M_ROOT2^2) -------------------------------------------------------------------------------- /doc/intro.tex: -------------------------------------------------------------------------------- 1 | \emph{Terran BASIC} (\emph{TBASIC} afterwards) is an BASIC language heavily influenced by the Commodore Basic version 2 (the one used in the legendary \emph{Commodore 64}), meant to be used with (but not necessarily) Lua to provide \emph{retro} feeling for any virtual computers that run upon it, targeted especially the OpenComputers and ComputerCraft for Minecraft. BASIC is an non-structured\footnote{prone to, and really good at, making spaghetties}, general-purpose\footnote{does whatever computers do}, high-level\footnote{at least the codes are written using ordinary letters, instead of 0s and 1s} programming language which is meant to be easy to use. -------------------------------------------------------------------------------- /samples/GOSUB.BAS: -------------------------------------------------------------------------------- 1 | 0 REM Star-twinkling superpasta 2 | 0 REM You are NOT supposed to code like this! 3 | 0 REM Test features: GOTO, GOSUB, boolean literals and their evaluation 4 | 5 | 10 PRINT "GOSUB test" 6 | 11 PRINT "" 7 | 12 TWINKLED = FALSE 8 | 20 GOTO 100 9 | 100 PRINT "Twinkle twinkle little star" 10 | 101 IF TWINKLED THEN GOTO 4666 11 | 110 IF NOT TWINKLED THEN GOSUB 4666 12 | 120 IF TWINKLED THEN GOTO 100 13 | 2048 PRINT "Up above the world so high" 14 | 2049 RETURN 15 | 4666 PRINT "How I wonder what you are" 16 | 4667 IF NOT TWINKLED THEN GOSUB 2048 17 | 4668 IF TWINKLED THEN END 18 | 5000 PRINT "Like a diamond in the sky" 19 | 5001 TWINKLED = TRUE 20 | 5002 RETURN 21 | -------------------------------------------------------------------------------- /doc/implementation.tex: -------------------------------------------------------------------------------- 1 | \section{How The Interpreter Work} 2 | 3 | 4 | 5 | \section{Primitive Data Types} 6 | 7 | \subsection{Nil} 8 | Nils are identical to Null for most commonly used languages. 9 | 10 | \subsection{Boolean} 11 | Any number, including 0, must be evaluated as True. Only the \emph{NIL} and \emph{FALSE} are evaluated as False. 12 | 13 | \subsection{Number} 14 | Numbers must be implemented using double-precision floating-point, and must follow the IEEE 754 standard. Extra precision (just as x86 would do) may be used. 15 | 16 | \subsection{Number Arrays} 17 | Arrays can have as many as 255 dimensions, and it must be supported. 18 | 19 | %% TBA: array of other types 20 | 21 | 22 | \section{Operation} 23 | 24 | \subsection{Mathematics} 25 | Zero to the zeroth power must return 1.0 successfully. Zero divided by zero must raise \code{DIVISION BY ZERO} error. 26 | -------------------------------------------------------------------------------- /doc/commandsstatements.tex: -------------------------------------------------------------------------------- 1 | \section{Variable} 2 | 3 | Variable is a symbolic name that refers to certain value. Variable is like a jar with name on it: you can put (\emph{assign} is a proper term) new value to the jar, look inside of the jar, or even empty one. 4 | 5 | \subsection{Naming a Variable} 6 | 7 | Name of the variable should be easily understandable and concise. It is strongly discouraged to name a variable with only one or two letters. (e.g. \code{I}, \code{A1}) 8 | 9 | TBASIC is somewhat pedantic about the variable name: you are only allowed to use \textbf{alphabets, numbers and underscores}, and \textbf{the name cannot start with a number}. For example, \code{USER_INPUT3} is valid but \code{3INPUT} is not. In some version or implementation of the TBASIC, those bad names would actually work for some extent, but it \emph{will} break some other parts, so don't use illegal names. -------------------------------------------------------------------------------- /doc/errors.tex: -------------------------------------------------------------------------------- 1 | This chapter describes errors defined in TBASIC. 2 | 3 | \section{Error Description} 4 | 5 | \subsection{SYNTAX} error denotes that TBASIC could not recognize the statement you have entered. Caused by things like missing parenthesis, illegal characters, incorrect punctuation, misspelled keyword, etc. 6 | 7 | \subsection{ EXPECTED, GOT } error denotes that the statement expected certain type(s) as one of its argument, but got unsupported other type. 8 | 9 | \subsection{RETURN WITHOUT GOSUB} error denotes that RETURN statement is met, but GOSUB was not used before (or used up), and TBASIC does not know where to return. 10 | 11 | \subsection{NEXT WITHOUT FOR} error denotes that NEXT statement is met, but FOR was not used, and TBASIC does not know where to return. 12 | 13 | \subsection{ASSIGNMENT ON IF CLAUSE} error denotes that you have put assignment between IF and THEN. Usually caused by mistakenly typing \code{=} instead of \code{==}. -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Terran BASIC (TBASIC) 2 | Copyright (c) 2016 Torvald (minjaesong) and the contributors. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Software), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /samples/NUMGUESS.BAS: -------------------------------------------------------------------------------- 1 | 0 REM number guessing game using INPUT statement 2 | 3 | 4 | 10 PRINT 5 | 20 PRINT "GUESS THE NUMBER" 6 | 7 | 30 TRIES = 1 8 | 31 MAXNUMBER = 999 9 | 32 NUMBER = ROUND(RND() * MAXNUMBER) 10 | 33 IF NUMBER == 0 THEN GOTO 32 11 | 34 MAXTRIES = 10 12 | 13 | 40 PRINT "FROM 1 TO " ; MAXNUMBER 14 | 15 | 16 | 17 | 100 REM main loop 18 | 110 IF TRIES > MAXTRIES THEN GOTO 200 19 | 130 PRINT 20 | 131 PRINT "MAKE YOUR GUESS (" ; TRIES ; " out of " ; MAXTRIES ; ")" 21 | 140 TRIES += 1 22 | 150 INPUT GUESS 23 | 24 | 160 IF GUESS == NUMBER THEN GOTO 300 25 | 161 IF GUESS > NUMBER THEN PRINT "TOO HIGH" 26 | 162 IF GUESS < NUMBER THEN PRINT "TOO LOW" 27 | 28 | 170 GOTO 100 29 | 30 | 31 | 32 | 200 REM you're LOSER 33 | 210 PRINT 34 | 211 PRINT "YOU LOST!" 35 | 212 PRINT "THE ANSWER WAS " ; NUMBER 36 | 220 GOTO 400 37 | 38 | 300 REM you're WINNER (just in case, it's a quote from Big Rigs) 39 | 310 PRINT 40 | 311 PRINT "CORRECT!" 41 | 320 GOTO 400 42 | 43 | 400 REM try again? 44 | 410 PRINT "WANT TO TRY AGAIN (Y/N)?" 45 | 420 INPUT TRYAGAIN 46 | 430 IF TRYAGAIN == "Y" OR TRYAGAIN == "y" THEN GOTO 10 47 | 440 IF TRYAGAIN == "N" OR TRYAGAIN == "n" THEN END 48 | 450 GOTO 400 49 | -------------------------------------------------------------------------------- /samples/99BEERS.BAS: -------------------------------------------------------------------------------- 1 | 0 REM 99 Bottles of beer for TBASIC V0.4+ 2 | 0 REM this code 'almost' works with C64, with slight modifications 3 | 4 | 10 FOR beers = 99 TO 0 5 | 20 IF beers == 0 THEN GOSUB 400 6 | 21 IF beers == 1 THEN GOSUB 300 7 | 22 IF beers == 2 THEN GOSUB 200 8 | 23 IF beers > 2 THEN GOSUB 100 9 | 60 PRINT "" 10 | 70 NEXT beers 11 | 80 END 12 | 13 | 100 REM Plural beers 14 | 101 PRINT beers ; " bottles of beer on the wall, " ; beers ; " bottles of beer." 15 | 102 PRINT "Take one down and pass it around, " ; beers - 1 ; " bottles of beer on the wall." 16 | 103 RETURN 17 | 18 | 200 REM Plu-Singular beers 19 | 201 PRINT beers ; " bottles of beer on the wall, " ; beers ; " bottles of beer." 20 | 202 PRINT "Take one down and pass it around, " ; beers - 1 ; " bottle of beer on the wall." 21 | 203 RETURN 22 | 23 | 300 REM Singu-nil beers 24 | 301 PRINT beers ; " bottle of beer on the wall, " ; beers ; " bottle of beer." 25 | 302 PRINT "Take one down and pass it around, no more bottles of beer on the wall." 26 | 303 RETURN 27 | 28 | 400 REM nil beers 29 | 401 PRINT "No more bottles of beer on the wall, no more bottles of beer." 30 | 402 PRINT "Go to the store and buy some more, 99 bottles of beer on the wall." 31 | 403 RETURN -------------------------------------------------------------------------------- /TBASEXTN.lua: -------------------------------------------------------------------------------- 1 | -- TBASIC extension 2 | 3 | -- these are utilities. Do not touch these lines 4 | local __assert = _TBASIC.__assert 5 | local __assertlhand = _TBASIC.__assertlhand 6 | local __assertrhand = _TBASIC.__assertrhand 7 | local __checknumber = _TBASIC.__checknumber 8 | local __checkstring = _TBASIC.__checkstring 9 | local __readvar = _TBASIC.__readvar 10 | local __resolvevararg = _TBASIC.__resolvevarar 11 | local vararg = -13 -- magic 12 | -- end of utilities 13 | 14 | 15 | -- these are the sample code for defining your own words 16 | --[[ 17 | -- actual function that does the job 18 | local function _fnupgoer(n) 19 | print("Up-goer "..__checknumber(n).." goes up!") 20 | end 21 | 22 | -- add the word UPGOER to word list 23 | table.insert(_TBASIC._FNCTION, "UPGOER") 24 | -- add the actual function '_fnupgoer' and its number of arguments (1) to 25 | -- '_TBASIC.LUAFN'. 'UPGOER' part should match with the word you just 26 | -- inserted to _TBASIC._FNCTION. 27 | _TBASIC.LUAFN.UPGOER = {_fnupgoer, 1} 28 | ]] 29 | 30 | 31 | -- little debugger's blessing 32 | local function _fnenableluatrace() _TBASIC.SHOWLUAERROR = true end 33 | table.insert(_TBASIC._FNCTION, "LUATRACEON") 34 | _TBASIC.LUAFN.LUATRACEON = {_fnenableluatrace, 0} 35 | -------------------------------------------------------------------------------- /samples/test_suite.bas: -------------------------------------------------------------------------------- 1 | 0 REM TBASIC test suite 2 | 3 | 9 LUATRACEON 4 | 5 | 10 REM IF test 6 | 20 temp = FALSE 7 | 30 IF temp == TRUE THEN ABORTM "IF STATEMENT FAILED -- EXPECTED FALSE, GOT TRUE" 8 | 40 IF temp == FALSE THEN PRINT "IF STATEMENT OKAY" 9 | 10 | 50 REM math test 11 | 60 round_1 = 3.89 12 | 61 round_2 = -4.12 13 | 70 IF ROUND(round_1) <> 4 THEN ABORTM "ROUND STATEMENT FAILED -- EXPECTED 4, GOT " ; ROUND round_1 14 | 80 IF ROUND(round_2) == -4 THEN PRINT "ROUND STATEMENT OKAY" 15 | 16 | 90 REM substring test 17 | 100 string = "SMOOTH MCGROOVE IS SMOOTH AND GROOVY" 18 | 110 IF LEFT(string, 5) <> "SMOOT" THEN ABORTM "LEFT STATEMENT FAILED -- EXPECTED 'SMOOT', GOT " ; LEFT(string, 5) 19 | 120 IF RIGHT(string, 4) <> "OOVY" THEN ABORTM "RIGHT STATEMENT FAILED -- EXPECTED 'OOVY', GOT " ; RIGHT(string, 4) 20 | 130 IF MID(string, 8, 15) <> "MCGROOVE" THEN ABORTM "MID STATEMENT FAILED -- EXPECTED 'MCGROOVE', GOT" ; MID(string, 8, 15) 21 | 140 IF LEN(string) <> 36 THEN ABORTM "LEN STATEMENT FAILED" 22 | 150 PRINT "LENGTH OF STRING '" ; string ; "' IS " ; LEN(string) 23 | 160 PRINT "LEFT/RIGHT/MID STATEMENT OKAY" 24 | 25 | 170 REM max/min test 26 | 180 IF MAX(-54,-76,-4,-564,-1,-7664) <> -1 THEN ABORTM "MAX STATEMENT FAILED -- EXPECTED '-1', GOT " ; MAX(-54,-76,-4,-564,-1,-7664) 27 | 190 IF MIN(-54,-76,-4,-564,-1,-7664) <> -7664 THEN ABORTM "MIN STATEMENT FAILED -- EXPECTED '-7664', GOT " ; MIN(-54,-76,-4,-564,-1,-7664) 28 | 200 PRINT "MAX/MIN STATEMENT OKAY" -------------------------------------------------------------------------------- /doc/concepts.tex: -------------------------------------------------------------------------------- 1 | This chapter describes the basic concepts of the language. 2 | 3 | 4 | \section{Values and Types} 5 | 6 | BASIC is a \emph{Dynamically typed language}, which means variables do not know which group they should barge in; only values of the variable do. In fact, there are no type definitions in the language. We do want our variables to feel themselves awkward. 7 | 8 | There are five basic types: \emph{nil}, \emph{boolean}, \emph{number}, \emph{string} and \emph{array}. \emph{Nil} is the type of the literal value \textbf{NIL}, who likes to possess nothing (I think he's secretly a buddhist). \emph{Boolean} is the type of the values that is either \textbf{TRUE} or \textbf{FALSE}, he knows no in-between. Both \textbf{NIL} and \textbf{FALSE} makes condition \emph{false}, any other values, even the number 0, makes it \emph{true}, because \emph{0 is truly a number, ya See-barbarians}. \emph{Number} represents real (double-precision floating-point) numbers. Operations on numbers follow the same rules of the underlying virtual machine\footnote{if you are not a computer person, just disregard}, and such machines must follow the IEEE 754 standard\footnote{ditto.}. \emph{String} represents immutable\footnote{cannot be altered directly} sequences of bytes. However, you can't weave them to make something like \emph{string array}\footnote{future feature\ldots maybe\ldots? Probably not\ldots}. 9 | 10 | \subsection{Arrays} are somewhat special; they can be work as both array and a number. When you read array variable as an array using index notation \code{MYARRAY(3)}, the variable will behave as an ordinary array. However, if you evaluate the array as a number, using \code{MYARRAY}, their first element is returned, without any errors. This feature is there to use FOR statement conveniently and \emph{to screw ya debugging}. -------------------------------------------------------------------------------- /Expanding the Language.md: -------------------------------------------------------------------------------- 1 | # To Make It Do More Things You Want 2 | 3 | This is not an up-goer art! Brought to you by _O'Really? Press_. 4 | 5 | 6 | Every job is done within ```TBASEXTN.lua```. If you don't have one, make new one, and enter these lines and save: 7 | 8 | 9 | -- TBASIC extension 10 | 11 | -- these are utilities. Do not delete these lines 12 | local __assert = _TBASIC.__assert 13 | local __assertlhand = _TBASIC.__assertlhand 14 | local __assertrhand = _TBASIC.__assertrhand 15 | local __checknumber = _TBASIC.__checknumber 16 | local __checkstring = _TBASIC.__checkstring 17 | local __readvar = _TBASIC.__readvar 18 | local __resolvevararg = _TBASIC.__resolvevararg 19 | -- end of utilities 20 | 21 | 22 | ## Usual things that people should follow 23 | 24 | To check if it's a number, you use ```__checknumber```. To check if it's letters, you use ```__checkstring```. To check the type you'll get, you use ```__assert```. 25 | 26 | When it comes to name your *word*s that explains orders to the computer, their name starts with ```_fn```, if it's about *mark*s, their name starts with ```_op```. 27 | 28 | <> 29 | 30 | <<``__resolvevararg``>> 31 | 32 | ## To make it understand your new sets of words 33 | 34 | You will be dealing with something called ```_TBASIC._FNCTION``` and ```_TBASIC.LUAFN```. Add the name of your word to ```_TBASIC._FNCTION```, and add a computer word to ```_TBASIC.LUAFN``` with your word name as a place-name. To make the computer *actually* understand your new *words*, is up to you. 35 | 36 | 37 | ## To make it understand your new sets of marks 38 | 39 | Much the same as adding *word*s, but there's some different parts. You will add your *mark*s to ```_TBASIC._OPERATR``` and ```opprecedence```. Watch that ```opprecedence``` is *ordered*, and you should know the order of your new *mark*s — which mark comes before yours and which comes after. Think hard that order and add your *mark* to its right place. If it's the "right-to-left" thing, also add yours to ```opassoc.rtl```. 40 | 41 | 42 | ## Just tell me how to do that! 43 | 44 | Let's say you want to add a word ``````UPGOER``````. What it does is it takes a number, and say *Up-goer ```number``` goes up!* If you tell computer ```UPGOER 5```, the computer will reply ```Up-goer 5 goes up!``` 45 | 46 | Here's how it's done: 47 | 48 | 0. Open ```TBASEXTN.lua```. 49 | 1. Enter this: 50 | 51 | local function _fnupgoer(n) 52 | print("Up-goer "..__checknumber(n).." goes up!") 53 | end 54 | 55 | at the bottom. 56 | 57 | 2. Add ```"UPGOER"``` to ```_TBASIC._FNCTION``` (enter this: ```table.insert(_TBASIC._FNCTION, "UPGOER")``` to the next line) 58 | 3. Add ```{_fnupgoer, 1}``` to ```_TBASIC.LUAFN.UPGOER```. Meaning of the word ```UPGOER``` should be obvious, number ```1``` means that it will take that number of things to do something. (enter this: ```_TBASIC.LUAFN.UPGOER = {_fnupgoer, 1}``` to the next line) 59 | 4. Done! 60 | 61 | 62 | ## Tell me more! 63 | 64 | * ```__checknumber(number)``` returns real number ```number``` if it can be written as a number. If not, computer will stop and say it didn't get a number. 65 | 66 | * ```__checkstring(letters)``` returns letters ```letters``` if it can be written as letters. 67 | 68 | * ```__assert(thing, type)``` will check the type of ```thing``` is a ```type```. If it is, computer will move on. If not, it will stop and say something. 69 | 70 | * ```__readvar(varname)``` will 71 | -------------------------------------------------------------------------------- /doc/thelanguage.tex: -------------------------------------------------------------------------------- 1 | This chapter describes the lexis, the syntax, and the semantics of TBASIC. In other words, this chapter describes which words are good, how they can be jumbled up, and what the hell they could even mean. 2 | 3 | \section{Line Format} 4 | 5 | Program lines in BASIC program have the following format: 6 | 7 | \forceindent{\monofont nnnnn BASIC-statement } \ 8 | 9 | Only one BASIC statement may be placed on a line. A program line always begins with a line number and ends with a carriage return. 10 | 11 | 12 | \section{Lexical Conventions} 13 | 14 | \emph{Names} (also called \emph{identifiers}) in TBASIC can be any string of letters, digits, and underscores, not beginning with a digit. Identifiers are used to name variables. 15 | 16 | The following \emph{keywords} are reserved and cannot be used as names: 17 | 18 | \begin{multicols}{6} 19 | %{\condensedfont% 20 | ABORT\par 21 | ABORTM\par 22 | ABS\par 23 | AND\par % operator 24 | ASC\par 25 | BACKCOL\par 26 | BEEP\par 27 | CBRT\par 28 | CEIL\par 29 | CHR\par 30 | CLR\par 31 | CLS\par 32 | COS\par 33 | DEF\par 34 | DELETE\par 35 | DIM\par 36 | DO\par 37 | END\par 38 | FALSE\par 39 | FLOOR\par 40 | FN\par 41 | FOR\par 42 | GET\par 43 | GO\par 44 | GOSUB\par 45 | GOTO\par 46 | HTAB\par 47 | IF\par 48 | IN\par 49 | INPUT\par 50 | INT\par 51 | INV\par 52 | LEFT\par 53 | LEN\par 54 | LIST\par 55 | LOAD\par 56 | LOG\par 57 | M\_E\par % constant 58 | M\_PI\par % constant 59 | M\_2PI\par % constant 60 | M\_ROOT2\par % constant 61 | MAX\par 62 | MID\par 63 | MIN\par 64 | MINUS\par % operator 65 | NEW\par 66 | NEXT\par 67 | NIL\par 68 | NOT\par % operator 69 | OR\par % operator 70 | PRINT\par 71 | REM\par 72 | RENUM\par 73 | RETURN\par 74 | RIGHT\par 75 | RND\par 76 | ROUND\par 77 | RUN\par 78 | SAVE\par 79 | SCROLL\par 80 | SGN\par 81 | SIN\par 82 | SIZEOF\par % operator 83 | SQRT\par 84 | STEP\par % operator 85 | STR\par 86 | TAB\par 87 | TAN\par 88 | TEXTCOL\par 89 | THEN\par 90 | TO\par % operator 91 | TEMIT\par 92 | TRUE\par 93 | VAL\par 94 | VTAB\par 95 | XOR\par % operator 96 | %}% 97 | \end{multicols} 98 | 99 | TBASIC is a case-insensitive language, and so is the reserved word. 100 | 101 | The following strings denote other tokens: 102 | 103 | \begin{multicols}{6} 104 | %{\condensedfont% 105 | $>$$>$$>$\par 106 | $<$$<$\par 107 | $>$$>$\par 108 | |\par 109 | \&\par 110 | !\par 111 | ;\par 112 | $=$$=$\par 113 | $>$\par 114 | $<$\par 115 | $<$$=$\par 116 | $=$$<$\par 117 | $>$$=$\par 118 | $=$$>$\par 119 | !$=$\par 120 | $<$$>$\par 121 | $>$$<$\par 122 | $=$\par 123 | $:$$=$\par 124 | $^\wedge$\par 125 | *\par 126 | /\par 127 | $+$\par 128 | $-$\par 129 | \%\par 130 | (\par 131 | )\par 132 | ,\par 133 | %} 134 | \end{multicols} 135 | 136 | 137 | \section{Constants} 138 | 139 | TBASIC predefines some constants using its variable system. You can read from these variables, but any new assignment will have no effects and TBASIC will \emph{not} raise any errors. 140 | 141 | \begin{tabularx}{\textwidth}{l l X} 142 | \textbf{Name} & \textbf{Value} & \textbf{Remarks} 143 | \\ 144 | \endhead 145 | M\_PI & 3.141592653589793 & Mathematical constant $\pi$ \\ 146 | M\_2PI & 6.283185307179586 & Mathematical constant $2\pi$ \\ 147 | M\_E & 2.718281828459045 & Mathematical constant $e$ \\ 148 | M\_ROOT2 & 1.414213562373095 & Mathematical constant $\sqrt{2}$ \\ 149 | TRUE & \emph{true} & Self explanatory \\ 150 | FALSE & \emph{false} & do. \\ 151 | NIL & \emph{nil} & do. \\ 152 | \_VERSION & \tbasver & Current TBASIC version \\ 153 | \end{tabularx} 154 | -------------------------------------------------------------------------------- /tests/tokenisertest.lua: -------------------------------------------------------------------------------- 1 | if _G.bit32 then _G.bit = bit32 end -- Lua 5.2 and LuaJIT compatibility (which has 'bit32' but no 'bit') 2 | 3 | function table.binsearch(t, value, cmpval) 4 | local low = 1 5 | local high = #t 6 | local value = cmpval(value) 7 | 8 | while low <= high do 9 | local mid = bit.rshift((low + high), 1) 10 | local midVal = t[mid] 11 | 12 | if value > cmpval(midVal) then 13 | low = mid + 1 14 | elseif value < cmpval(midVal) then 15 | high = mid - 1 16 | else 17 | return mid -- key found 18 | end 19 | end 20 | return nil -- key not found 21 | end 22 | 23 | 24 | 25 | function string.hash(str) 26 | local hash = 2166136261 27 | for i = 1, #str do 28 | hash = hash * 16777619 29 | hash = bit.bxor(hash, str:byte(i)) 30 | end 31 | return hash 32 | end 33 | 34 | 35 | line = [[FOR beers=99 TO 0]] 36 | 37 | _G._TBASIC = {} 38 | _G._TBASIC._OPERATR = { 39 | -- operators 40 | ">>>", "<<", ">>", "|", "&", "XOR", "!", -- bitwise operations 41 | ";", -- string concatenation 42 | "SIZEOF", -- LENGTH OF string/array. This is not C 43 | "==", ">", "<", "<=", "=<", ">=", "=>", 44 | "!=", "<>", "><", -- not equal 45 | "=", ":=", -- assign 46 | "AND", "OR", "NOT", 47 | "^", -- math.pow, 0^0 should return 1. 48 | "*", "/", "+", "-", -- arithmetic operations 49 | "%", -- math.fmod 50 | "TO", "STEP", -- integer sequence operator 51 | "MINUS", -- unary minus 52 | } 53 | 54 | 55 | local tokens = {" ", "\t"} 56 | local longest_token_len = 0 57 | for _, v in ipairs(_TBASIC._OPERATR) do 58 | if not v:match("[A-Za-z]") then 59 | table.insert(tokens, v) 60 | local tokenlen = #v 61 | if longest_token_len < #v then 62 | longest_token_len = #v 63 | end 64 | end 65 | end 66 | table.sort(tokens, function(a, b) return string.hash(a) < string.hash(b) end) 67 | 68 | lextable = {} 69 | isquote = false 70 | quotemode = false 71 | wordbuffer = "" 72 | local function flush() 73 | if (#wordbuffer > 0) then 74 | table.insert(lextable, wordbuffer) 75 | wordbuffer = "" 76 | end 77 | end 78 | local function append(char) 79 | wordbuffer = wordbuffer..char 80 | end 81 | local function append_no_whitespace(char) 82 | if char ~= " " and char ~= "\t" then 83 | wordbuffer = wordbuffer..char 84 | end 85 | end 86 | 87 | -- return: lookless_count on success, nil on failure 88 | local function isdelimeter(string) 89 | local cmpval = function(table_elem) return string.hash(table_elem) end 90 | local lookless_count = #string 91 | local ret = nil 92 | repeat 93 | ret = table.binsearch(tokens, string:sub(1, lookless_count), cmpval) 94 | lookless_count = lookless_count - 1 95 | until ret or lookless_count < 1 96 | return ret and lookless_count + 1 or false 97 | end 98 | 99 | local i = 1 -- Lua Protip: variable in 'for' is immutable, and is different from general variable table, even if they have same name 100 | while i <= #line do 101 | local c = string.char(line:byte(i)) 102 | 103 | local lookahead = line:sub(i, i+longest_token_len) 104 | 105 | if isquote then 106 | if c == [["]] then 107 | flush() 108 | isquote = false 109 | else 110 | append(c) 111 | end 112 | else 113 | if c == [["]] then 114 | isquote = true 115 | append_no_whitespace("~") 116 | else 117 | local delimsize = isdelimeter(lookahead) 118 | if delimsize then 119 | flush() -- flush buffer 120 | append_no_whitespace(lookahead:sub(1, delimsize)) 121 | flush() -- flush this delimeter 122 | i = i + delimsize - 1 123 | else 124 | append_no_whitespace(c) 125 | end 126 | end 127 | end 128 | 129 | i = i + 1 130 | end 131 | flush() -- don't forget this! 132 | 133 | 134 | 135 | 136 | print("INPUT", line) 137 | print(table.concat(lextable, "|")) 138 | 139 | 140 | -------------------------------------------------------------------------------- /doc/theinterpreter.tex: -------------------------------------------------------------------------------- 1 | This chapter describes about the reference interpreter. 2 | 3 | \section{Implementaion} 4 | 5 | The interpreter does not do things like translating BASIC statement to Lua statement, instead it executes the statements using simple virtual machine. The virtual machine (TBASEXEC.lua) first reads the BASIC statements, tokenises them and convert them to Reverse Polish Notation to build command stack, and executes the stack. 6 | 7 | 8 | \section{Configuration} 9 | 10 | Reference interpreter provides some properties that can be configured. 11 | 12 | \begin{tabularx}{\textwidth}{l c X} 13 | \textbf{Field} & \textbf{Default} & \textbf{Description} 14 | \\ 15 | \endhead 16 | _TBASIC._INTPRTR.MAXLINES & 63999 & Maximum line number permitted 17 | \\ 18 | _TBASIC._INTPRTR.STACKMAX & 2000 & Maximum depth for the call stack 19 | \end{tabularx} 20 | 21 | You can modify these valueu by modifying \code{_G._TBASIC._INTPRTR.RESET} function in \code{TBASINCL.lua}. 22 | 23 | On the code, there's a part that is \code{local debug = false}. If you set the variable to \emph{true}, \code{TBASEXEC} will print out its lexer status, \code{TBASINCL} will print out intermediate states for RPN converter. 24 | 25 | 26 | \chapter{Language Extension} 27 | 28 | Reference interpreter provides language extension, you can define your own functions, operators and their precedence and associativity. 29 | 30 | 31 | \section{Conventions} 32 | 33 | Basically, any argements passed to your Lua function are \emph{variable names}, so you must resolve them. To read from the variable, simply use function \code{__readvar(varname)}. 34 | 35 | To filter out a number from the argument, use \code{__checknumber(varname)}, for a string, use \code{__checkstring(varname)}. These functions uses __readvar() internally, so you don't have to write like \code{__checknumber(__readvar(foo))}. 36 | 37 | To ensure the type of an argument, use \code{__assert(arg, expected_type)}. \code{arg} is an argument (which is usually a name of a variable) and \code{expected_type} is Lua data type, written in string. For operators that takes left-hand and right-hand values, you can use \code{__assertland()} and \code{__assertrhand()}. 38 | 39 | \section{Adding New Function} 40 | 41 | Reference interpreter stores function names separately as collective table called \code{_TBASIC._FNCTION}, as well as \code{_TBASIC.LUAFN}, which stores Lua function (the actor) and number of arguments. 42 | 43 | First you must implement that function. They need to be reside in a single Lua function, and their name should start with \code{_fn}. 44 | 45 | You will be adding the name of your new function (in all uppercase) to \code{_TBASIC._FNCTION}. Just add the following line: 46 | 47 | \begin{codeblock} 48 | table.insert(_TBASIC._FNCTION, YOUR_NEW_FUNCTION_NAME) 49 | \end{codeblock} 50 | 51 | And you add the description of your new function to \code{_TBASIC.LUAFN}. 52 | 53 | \begin{codeblock} 54 | _TBASIC.LUAFN.YOUR_NEW_FUNCTION_NAME = {_fnyour_new_function_impl, argument_count} 55 | \end{codeblock} 56 | 57 | \code{argument_count} is an integer that is equal to or greater than 0. If you want variable number of arguments, use \code{vararg}. Note that this is not a string, it's a variable (magic number) pre-defined in the extension file. Its actual value is -13. 58 | 59 | \subsection{Example code} 60 | ~ 61 | We will define a new function called \code{UPGOER}. What it does is it prints \emph{Up-goer (number) goes up!}, and takes one argument. 62 | 63 | \begin{codeblock} 64 | -- actual function that does the job 65 | local function _fnupgoer(n) 66 | print("Up-goer "..__checknumber(n).." goes up!") 67 | end 68 | 69 | -- add the word UPGOER to word list 70 | table.insert(_TBASIC._FNCTION, "UPGOER") 71 | 72 | -- add the actual function '_fnupgoer' and its number of arguments (1) to 73 | -- '_TBASIC.LUAFN'. 'UPGOER' part should match with the word you just 74 | -- inserted to _TBASIC._FNCTION. 75 | _TBASIC.LUAFN.UPGOER = {_fnupgoer, 1} 76 | \end{codeblock} 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TBASIC: BASIC language for Lua and OpenComputers/ComputerCraft/more 2 | 3 | Make BASIC great again 4 | 5 | ## BULLETIN 6 | 7 | ### Don't play with this crap 8 | this project was abandoned years ago; you'd have better luck backporting [my other BASIC](https://github.com/curioustorvald/TerranBASIC) (that actually works, unlike this crap) into Lua instead. 9 | 10 | ## Outline 11 | 12 | This is a BASIC language and its interpreter written in Lua, which can be used with Plain Lua/[OpenComputers](https://oc.cil.li/index.php?/page/index.html)/[ComputerCraft](http://computercraft.info/). 13 | 14 | The program will work with any Lua implementation that supports __Lua 5.2__ and higher (and Lua 5.1 with bit32 library). Tested with official Lua, __LuaJIT__, ComputerCraft (LuaJ) and OpenComputers (JNLua). 15 | 16 | ## Files 17 | 18 | * ```TBASIC.lua```: Terran BASIC shell 19 | * ```TBASEXEC.lua```: Terran BASIC interpreter 20 | * ```TBASINCL.lua```: Terran BASIC interpreter library 21 | * ```TBASEXTN.lua```: Terran BASIC extension 22 | * ```doc/TBASMAN.pdf```: Language manual. READ IT 23 | * ```doc/TBASMAN.tex```: Source for the manual 24 | * ```README.md```: What you are reading right now 25 | * ```LICENSE.md```: It's MIT 26 | 27 | 28 | ## How to Use 29 | 30 | Run ```TBASIC.lua``` to get started. 31 | 32 | * Prefix line numbers to input multiple-lined commands. 33 | * Enter command without line number to execute the one-liner command right away. Any input program will left untouched. 34 | * Enter ```RUN``` to execute the program you have written, enter ```LIST``` to see what you have entered to the buffer. 35 | * After the ```RUN```, your program will still be there. Enter ```NEW``` to wipe out them. 36 | * Made some mistakes? No worries, you can just overwrite by re-writing the line you wish to correct. 37 | * Line numbers too messy? Try ```RENUM```, your GOTOs and GOSUBs will be updated accordingly. 38 | 39 | 40 | ## Syntax 41 | 42 | Syntax of the TBASIC is heavily influenced by [Commodore 64 BASIC](https://www.c64-wiki.com/index.php/BASIC#Overview_of_BASIC_Version_2.0_.28second_release.29_Commands); Line number based, GOTO-controlled (uh-oh, [sounds harmful](http://homepages.cwi.nl/~storm/teaching/reader/Dijkstra68.pdf)), has GOSUB, etc. What doesn't work is direct memory controls (PEEK/POKE, SYS) and graphic/sound related commands. 43 | 44 | 45 | ## How to Contribute 46 | 47 | ### Code 48 | You clone it, make it great, and make a pull request. 49 | 50 | ### Documentation 51 | You can also help improving the documentation by adding missing texts, correcting my bad English (if any), or making a translation for non-English speaker like me. Don't know how to use TeX? Just leave them as plain text/MS Word/Rich Text Format and I'll take care of them. (Note: HWP is not to be accepted. C'mon, what the f--k is HWP?) 52 | 53 | ### In general 54 | If you have suggestions/bugs, feel free to report them on the Issue Tracker. 55 | 56 | 57 | ## How it works 58 | 59 | The interpreter is basically a stack-based machine, much like [FORTH](https://en.wikipedia.org/wiki/FORTH). Input commands are converted into [Reverse Polish Notation](https://en.wikipedia.org/wiki/Reverse_Polish_notation) using ```_G._TBASIC.TORPN()```, TORPN notates type of each token (function, operator, variable, string and number), and the interpreter executes RPN'd command token by token. More details are on the comments of the code. 60 | 61 | 62 | ## Disclaimer 63 | 64 | This project is in its early stages, meaning most of the keywords are not implemented, already implemented features _are_ buggy. 65 | 66 | Reference manual is work-in-progress. 67 | 68 | If you are to distribute this Software to be used on the OpenComputers/ComputerCraft, AND ONLY WHEN YOU DISTRIBUTE THE VERBATIM COPY OF THE ESSENTIAL PARTS OF THIS SOFTWARE (following three files: ```TBASINCL.lua```, ```TBASEXEC.lua``` and ```TBASIC.lua```), you can distribute without the license file. If not (made some modifications and/or to be used on other software/games, even if you are the contributor of this Software), the copyright notice and the permission notice shall be included in all copies or substantial portions of the Software. 69 | -------------------------------------------------------------------------------- /doc/TBASMAN.tex: -------------------------------------------------------------------------------- 1 | % !TEX TS-program = LuaLaTeX 2 | 3 | %% Copyright (c) 2016 Torvald (minjaesong) and the contributors. Freely available under the terms of the MIT License. 4 | 5 | \documentclass[10pt, stock, openany]{memoir} 6 | 7 | 8 | \usepackage{fontspec} 9 | \setmainfont[Ligatures=TeX]{TeX Gyre Heros} 10 | \newfontfamily\condensedfont{TeX Gyre Heros Cn} 11 | \newfontfamily\monofont{TeX Gyre Cursor} 12 | 13 | 14 | 15 | 16 | \usepackage{fapapersize} 17 | \usefapapersize{148mm,210mm,15mm,15mm,20mm,15mm} % A5 paper 18 | \usepackage{afterpage} 19 | \usepackage{hyperref} 20 | \usepackage{graphicx} 21 | \usepackage{xcolor} 22 | \usepackage{ltablex} 23 | \usepackage{parskip} 24 | \usepackage{multicol} 25 | \usepackage{soul} 26 | 27 | \usepackage{lineno} % debug 28 | 29 | 30 | \makeatletter 31 | \newlength{\mytextsize} 32 | \setlength{\mytextsize}{\f@size pt} 33 | \newlength{\mybaselineskip} 34 | \setlength{\mybaselineskip}{1.3\mytextsize} 35 | \makeatother 36 | \setlength{\baselineskip}{\mybaselineskip} 37 | 38 | \usepackage[fontsize=\mytextsize,baseline=\baselineskip,lines=38]{grid} 39 | 40 | \frenchspacing 41 | \setlength{\parindent}{0pt} 42 | \setlength{\parskip}{10pt} 43 | \setsecnumdepth{subsection} 44 | 45 | 46 | %% Idioms %% 47 | \hyphenation{Com-put-er-Craft} 48 | \hyphenation{O-pen-Com-put-ers} 49 | \hyphenation{T-BASIC} 50 | 51 | 52 | \newcommand\forceindent{\hskip1.5em} 53 | 54 | 55 | % Title styling 56 | \pretitle{\begin{flushright}} 57 | \posttitle{\par\end{flushright}} 58 | \preauthor{\begin{flushright}} 59 | \postauthor{\par\end{flushright}} 60 | 61 | % new sections are new page 62 | %\let\oldsection\chapter 63 | %\renewcommand\chapter{\clearpage\oldsection} 64 | 65 | % chapter title -- no now page after 66 | \renewcommand\chapterheadstart{} % kill the drop 67 | \renewcommand\afterchapternum{\vskip 0.5em} % space between number and title 68 | \setlength{\afterchapskip}{\baselineskip} % reduce space after chapter title 69 | \makeatletter 70 | \renewcommand\memendofchapterhook{% 71 | \m@mindentafterchapter\@afterheading} 72 | \makeatother 73 | 74 | % section/chapter header spacing 75 | \let\oldsection\section 76 | \renewcommand{\section}[1]{\oldsection{#1} \vskip\baselineskip} 77 | 78 | 79 | \definecolor{lgrey}{HTML}{eeeeee} 80 | \sethlcolor{lgrey} 81 | \renewcommand{\thefootnote}{\fnsymbol{footnote}} 82 | \newcommand{\code}[1]{{\monofont\hl{#1}}} 83 | \newcommand{\codebf}[1]{{\monofont \textbf{\hl{#1}}}} 84 | 85 | 86 | %\aliaspagestyle{part}{empty} 87 | %\aliaspagestyle{chapter}{empty} 88 | 89 | 90 | % The title 91 | \newcommand{\tbasver}{0.4} 92 | \newcommand{\theedition}{First Edition} 93 | \newcommand{\oreallypress}{\large\textbf{O'REALLY\raisebox{1ex}{\scriptsize ?}} \normalsize Press} 94 | 95 | \title{\HUGE\textbf{TERRAN BASIC \\ REFERENCE MANUAL} \\ \Large \vspace{1em} For Version \tbasver \\ \vspace{7mm} \theedition} 96 | \date{} 97 | \author{} 98 | \hypersetup{ 99 | pdfauthor={Torvald et al.}, 100 | pdftitle={Terran BASIC Reference Manual, \theedition\ (BASIC version \tbasver)}, 101 | unicode=true 102 | } 103 | 104 | \begin{document} 105 | \begin{titlingpage} 106 | \maketitle{} 107 | \vfill 108 | \oreallypress 109 | \end{titlingpage} 110 | 111 | \setcounter{page}{3} 112 | 113 | \tableofcontents* 114 | 115 | 116 | %\linenumbers % debug 117 | 118 | \openright 119 | \chapter{Introduction} 120 | \input{intro} 121 | 122 | \openany 123 | \chapter{Basic Concepts} 124 | \input{concepts} 125 | 126 | \chapter{The Language} 127 | \input{thelanguage} 128 | 129 | \chapter{Commands and Statements} 130 | \input{commandsstatements} 131 | 132 | 133 | \chapter{Operators} 134 | \input{operators} 135 | 136 | 137 | \chapter{Functions} 138 | \input{functions} 139 | 140 | 141 | \chapter{Errors} 142 | \input{errors} 143 | 144 | 145 | \chapter{Implementation} 146 | \input{implementation} 147 | \input{theinterpreter} 148 | 149 | %% Bibliography %% 150 | % http://www.commodore.ca/manuals/pdfs/Commodore_Basic_4_Users_Reference%20Manual.pdf 151 | % http://www.lua.org/manual/5.2/contents.html#contents 152 | % https://en.wikipedia.org/wiki/BASIC 153 | % http://www.commodore.ca/manuals/pdfs/commodore_error_messages.pdf 154 | 155 | 156 | \chapter{Index} 157 | 158 | 159 | 160 | 161 | \afterpage{\pagestyle{empty}\null\newpage} 162 | 163 | \end{document} -------------------------------------------------------------------------------- /doc/functions.tex: -------------------------------------------------------------------------------- 1 | This section describes built-in functions for the TBASIC. 2 | 3 | Functions in TBASIC can take \emph{argument}s. Arguments are the values you pass to functions. Functions can take one or more arguments. If the function only requires single argument, you can call the function with an argument without parentheses, like \code{GOTO 30}, but if the function takes two or more arguments, they must be surrounded with parentheses, such as \code{PRINT("HL", 3, "CONFIRMED")}. 4 | 5 | \section{Synopsis} 6 | 7 | Note: trigonometric functions assumes \emph{radian} as its input; $2\pi$\ is a full circle. 8 | 9 | \subsection{ABORT} --- halts current program 10 | \subsection{ABORTM} \emph{reason} --- halts current program with message 11 | \subsection{ABS} \emph{number} --- returns absolute value for the number 12 | \subsection{ASC} \emph{character} --- returns ASCII code for the character 13 | \subsection{BACKCOL} \emph{number} --- sets background colour of the terminal (if applicable) 14 | \subsection{BEEP} \emph{pattern} --- make computer produce beeping sound (does not work with CC) 15 | \subsection{CBRT} \emph{number} --- returns cubic root of the number 16 | \subsection{CEIL} \emph{number} --- returns round-up of the number 17 | \subsection{CHR} \emph{ASCII-code} --- returns character for the code 18 | \subsection{CLR} --- deletes all variables declared 19 | \subsection{CLS} --- clears screen buffer (if supported) 20 | \subsection{COS} \emph{radian} --- returns trigonometric cosine for the number 21 | \subsection{DEF FN} \emph{name}, \emph{code} --- defines new function 22 | \subsection{DELETE} \emph{from[-to]} --- deletes specified line that have entered (single line or range of lines, shell function) 23 | \subsection{DIM} \emph{name}, \emph{(d1[, d2[, d3\ldots]])} --- defines new array with given dimensions 24 | \subsection{END} --- successfully exits program 25 | \subsection{FLOOR} \emph{number} --- returns round-down of the number 26 | \subsection{FOR} \emph{a = x FROM y[ STEP z]} --- starts counted loop with counter \emph{a} that goes from \emph{x} to \emph{y}, with optional step. Use NEXT \emph{a} to make a loop 27 | \subsection{GET} \emph{variable[, variable\ldots]} --- get a single character from the keyboard and saves the code of the character to the given variable(s) 28 | \subsection{GOSUB} \emph{line} --- jumps to a subroutine. Use RETURN to jump back 29 | \subsection{GOTO} \emph{line} --- jumps to a line 30 | \subsection{HTAB} \emph{amount} --- moves output cursor horizontally by given amount (if applicable) 31 | \subsection{IF} \emph{condition} THEN \emph{command} --- evaluates condition and executes command if the condition is true 32 | \subsection{INPUT} \emph{var1[, var2[, var2\ldots]]} --- gets user input(s) from the console 33 | \subsection{INT} \emph{number} --- returns integer part of the number 34 | \subsection{INV} \emph{number} --- returns (1.0 / number) 35 | \subsection{LABEL} \emph{name} --- creates alias for the current line number, so that they can be used with \code{GOTO} or \code{GOSUB} commands 36 | \subsection{LEFT} \emph{string}, \emph{number} --- returns substring of leftmost \emph{number} characters 37 | \subsection{LEN} \emph{string or table} --- returns length of the string/table 38 | \subsection{LIST} \emph{[from[-to]]} --- prints out commands that have entered (entire commands or single line or range of lines, shell function) 39 | \subsection{LOAD} \emph{path} --- loads the file as a command. Previously entered commands will be deleted (shell function) 40 | \subsection{LOG} \emph{number} --- returns natural logarithm of the number 41 | \subsection{MAX} \emph{number, number[, \ldots]]} --- returns the biggest of the numbers 42 | \subsection{MID} \emph{string}, \emph{start}, \emph{end} --- returns substring of the string 43 | \subsection{MIN} \emph{number, number[, \ldots]]} --- returns the smallest of the numbers 44 | \subsection{NEW} --- deletes all the commands that have entered (shell function) 45 | \subsection{NEXT} \emph{variable} --- advances variable which is used by FOR loop 46 | \subsection{PRINT} \emph{string} --- prints string to the terminal 47 | \subsection{RAD} \emph{degree} --- converts degree to radian 48 | \subsection{REM} --- marks current line as remarks, or ``comments'' 49 | \subsection{RENUM} --- cleans up messy line numbers. Line numbers for GOTO and GOSUB will be modified accordingly (shell function) 50 | \subsection{RETURN} --- returns subroutine called by GOSUB 51 | \subsection{RIGHT} \emph{string}, \emph{number} --- returns substring of rightmost \emph{number} characters 52 | \subsection{RND} --- returns random decimal number that is $ 0.0 \leq n < 1.0 $ 53 | \subsection{ROUND} --- returns half-up of the number 54 | \subsection{RUN} --- executes the commands that have entered (shell function) 55 | \subsection{SAVE} \emph{path} --- saves the command that have entered to the file (shell function) 56 | \subsection{SCROLL} \emph{amount} --- scrolls the terminal by given amount 57 | \subsection{SGN} \emph{number} --- returns sign of the number; $ -1.0 $ for $ n < 0 $, $ 1.0 $ for $ n > 0 $, $ 0.0 $ otherwise. 58 | \subsection{SIN} \emph{radian} --- returns trigonometric sine for the number 59 | \subsection{SQRT} \emph{number} --- returns square root of the number 60 | \subsection{STR} \emph{number} --- returns string representation of the numerical value 61 | \subsection{TAB} \emph{amount} --- same as HTAB 62 | \subsection{TAN} \emph{radian} --- returns trigonometric tangent for the number 63 | \subsection{TEXTCOL} \emph{number} --- sets text colour of the terminal (if applicable) 64 | \subsection{TEMIT} \emph{frequency}, \emph{length} --- emits tone of given frequency for given length (in seconds) (does not work with CC) 65 | \subsection{VAL} \emph{string} --- returns numerical representation of the string 66 | \subsection{VTAB} \emph{amount} --- moves output cursor vertically by given amount (if applicable) 67 | 68 | \section{In-depth description} -------------------------------------------------------------------------------- /doc/operators.tex: -------------------------------------------------------------------------------- 1 | This chapter describes operators that can be used in TBASIC. 2 | 3 | \section{Overview} 4 | 5 | There are 37 operators in TBASIC version \tbasver. 6 | 7 | \section{Precedence} 8 | 9 | Operators has something called \emph{precedence}. It's not something hard despite of its name (especially if you are not a native English speaker, like me). It's just a thing that describes our language programmatically\footnote{in computer language}, which goes like \emph{multiplication comes before the addition and subtraction}. 10 | 11 | \begin{tabularx}{\textwidth}{c X} 12 | \textbf{Order} & \textbf{Operators} 13 | \\ 14 | \endhead 15 | 1 & :$=$\quad $=$\quad $+=$\quad $-=$\quad *$=$\quad /$=$\quad \%$=$ \\ 16 | 2 & OR \\ 17 | 3 & AND \\ 18 | 4 & | \\ 19 | 5 & XOR \\ 20 | 6 & \& \\ 21 | 7 & $=$$=$\quad !$=$\quad $<$$>$\quad $>$$<$ \\ 22 | 8 & $<$$=$\quad $>$$=$\quad $=$$<$\quad $=$$>$\quad $<$\quad $>$ \\ 23 | 9 & TO\quad STEP \\ 24 | 10 & $>$$>$$>$\quad $<$$<$\quad $>$$>$ \\ 25 | 11 & ; \\ 26 | 12 & $+$\quad $-$\\ 27 | 13 & *\quad /\quad \% \\ 28 | 14 & NOT\quad ! \\ 29 | 15 & $^\wedge$ \\ 30 | 16 & MINUS \\ 31 | \end{tabularx} 32 | 33 | \section{Precautions} 34 | 35 | Because of the way the language is desingned\footnote{don't write like \codebf{1+3*2+4/7}, they look hideous}, you must be careful when using $ - $ operator. 36 | 37 | \begin{tabularx}{\textwidth}{l l X} 38 | \textbf{Code} & \textbf{Evaluation} & \textbf{Remarks} 39 | \\ 40 | \endhead 41 | \code{FOO - 1} & subtraction(FOO, 1) & Variable FOO subtracted by 1 42 | \\ 43 | \code{FOO -1} & FOO(MINUS 1) & Function FOO() with argument -1 44 | \\ 45 | \code{FOO-1} & FOOMINUS1 & Syntax error 46 | \end{tabularx} 47 | 48 | 49 | \section{Arithmetic Operators} 50 | 51 | \subsection{PLUS} \emph{number} $+$ \emph{number} --- adds two numbers 52 | \subsection{MINUS} \emph{number} $-$ \emph{number} --- subtracts two numbers 53 | \subsection{TIMES} \emph{number} * \emph{number} --- multiplies two numbers 54 | \subsection{DIVIDE} \emph{number} / \emph{number} --- divides two numbers 55 | \subsection{MODULO} \emph{number} \% \emph{number} --- gets modulo (remainder) of two numbers 56 | \subsection{POWEROF} \emph{number a} $^\wedge$ \emph{number b} --- gets \emph{a} to the power of \emph{b}, or $a ^b$. $0 ^0$ will return $1$. 57 | 58 | 59 | \section{Logical Operators} 60 | 61 | \subsection{AND} \emph{condition 1} AND \emph{condition 2} --- returns \textbf{TRUE} if both conditions (boolean value) are true; \textbf{FALSE} otherwise 62 | \subsection{OR} \emph{condition 1} OR \emph{condition 2} --- returns \textbf{TRUE} if one or more conditions (boolean value) are true; \textbf{FALSE} if both conditions are false 63 | \subsection{NOT} NOT \emph{condition} --- negates condition (boolean value) 64 | 65 | 66 | \section{Relational Operators} 67 | 68 | \subsection{EQUALTO} \emph{number} $=$$=$ \emph{number} --- returns \textbf{TRUE} if two numbers represent same quantity; \textbf{FALSE} otherwise 69 | \subsection{NOTEQUALTO} \emph{number} !$=$ \emph{number} --- returns \textbf{TRUE} if two numbers represent different quantity; \textbf{FALSE} otherwise; aliases: $<$$>$, $>$$<$ 70 | \subsection{GREATERTHAN} \emph{number a} $>$ \emph{number b} --- returns \textbf{TRUE} if $a > b$ is satisfied; \textbf{FALSE} otherwise 71 | \subsection{LESSTHAN} \emph{number a} $<$ \emph{number b} --- returns \textbf{TRUE} if $a < b$ is satisfied; \textbf{FALSE} otherwise 72 | \subsection{GEQTHAN} \emph{number a} $>$$=$ \emph{number b} --- returns \textbf{TRUE} if $a \geq b$ is satisfied; \textbf{FALSE} otherwise; alias: $=$$<$ 73 | \subsection{LEQTHAN} \emph{number a} $<$$=$ \emph{number b} --- returns \textbf{TRUE} if $a \leq b$ is satisfied; \textbf{FALSE} otherwise; alias: $=$$>$ 74 | 75 | 76 | \section{Assignment Operators} 77 | 78 | \subsection{ASSIGN} \emph{variable} $=$ \emph{value} --- assign \emph{value} to \emph{variable}; alias: :$=$ 79 | \subsection{PLUSASSIGN} \emph{variable} $+=$ \emph{value} --- increment \emph{variable} by \emph{value} and update \emph{variable} to this new value 80 | \subsection{MINUSASSIGN} \emph{variable} $-=$ \emph{value} --- decrement \emph{variable} by \emph{value} and update \emph{variable} to this new value 81 | \subsection{TIMESASSIGN} \emph{variable} *$=$ \emph{value} --- multiply \emph{variable} by \emph{value} and update \emph{variable} to this new value 82 | \subsection{DIVIDEASSIGN} \emph{variable} /$=$ \emph{value} --- divide \emph{variable} by \emph{value} and update \emph{variable} to this new value 83 | \subsection{MODULOASSIGN} \emph{variable} \%$=$ \emph{value} --- modulo \emph{variable} by \emph{value} and update \emph{variable} to this new value 84 | 85 | 86 | \section{Bitwise Operators} 87 | 88 | \subsection{BAND} \emph{integer} \& \emph{integer} --- performs bitwise AND on two integers 89 | \subsection{BOR} \emph{integer} | \emph{integer} --- performs bitwise OR on two integers 90 | \subsection{BNOT} ! \emph{integer} --- performs bitwise NOT on integer 91 | \subsection{BXOR} \emph{integer} XOR \emph{integer} --- performs bitwise XOR on two integers 92 | \subsection{LSHIFT} \emph{integer} $<$$<$ \emph{offset} --- performs left shift on \emph{integer} by \emph{offset} 93 | \subsection{RSHIFT} \emph{integer} $>$$>$ \emph{offset} --- performs signed right shift on \emph{integer} by \emph{offset}; if leftmost bit is 1, bits on the left side are padded with 1 94 | \subsection{URSHIFT} \emph{integer} $>$$>$$>$ \emph{offset} --- performs unsigned right shift on \emph{integer} by \emph{offset} 95 | 96 | 97 | \section{Miscellaneous Operators} 98 | 99 | \subsection{CONCAT} \emph{string} ; \emph{string} --- concatenates two strings 100 | \subsection{MINUS} $-$\emph{number} --- negates \emph{number}, or $-n$ 101 | \subsection{TO} \emph{number} TO \emph{number} --- creates integer sequence of specified range; \code{1 TO 4} will create an array of \emph{1, 2, 3, 4}; \code{8 TO 3} will create \emph{8, 7, 6, 5, 4, 3} 102 | \subsection{STEP} \emph{int array} STEP \emph{number} --- filters input array such that every $1+n$th number is remained; \code{1 TO 10 STEP 3} will create an array of {1, 4, 7, 10} -------------------------------------------------------------------------------- /TBASIC.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | TBASIC shell 3 | 4 | Synopsis: TBASIC (filename) 5 | If no file is specified, interactive mode will be started 6 | 7 | 8 | To debug EXEC and/or INCL, there's line ```local debug = false``` on each file; change it to ```true``` manually 9 | and you are all set. 10 | ]] 11 | 12 | 13 | if os and os.loadAPI then -- ComputerCraft 14 | os.loadAPI "TBASINCL.lua" 15 | os.loadAPI "TBASEXEC.lua" 16 | else 17 | require "TBASINCL" 18 | require "TBASEXEC" 19 | end 20 | 21 | args = {...} 22 | 23 | print(_G._TBASIC._HEADER) 24 | _TBASIC.PROMPT() 25 | _TBASIC.SHOWLUAERROR = false 26 | 27 | 28 | local function concat_lines(lines, startindex, endindex) 29 | local out = "" 30 | for i = startindex or 1, endindex or _TBASIC._INTPRTR.MAXLINES do 31 | if lines[i] ~= nil then 32 | out = out.."\n"..tostring(i).." "..lines[i] 33 | end 34 | end 35 | 36 | return out 37 | end 38 | 39 | 40 | if args[1] then 41 | local prog = nil 42 | if fs and fs.open then -- ComputerCraft 43 | local inp = assert(fs.open(args[1], "r")) 44 | prog = inp:readAll() 45 | inp:close() 46 | else 47 | local inp = assert(io.open(args[1], "r")) 48 | prog = inp:read("*all") 49 | inp:close() 50 | end 51 | 52 | _TBASIC.EXEC(prog) 53 | else 54 | local terminate_app = false 55 | 56 | local ptn_nums = "[0-9]+" 57 | local renum_targets = {"GOTO[ ]+"..ptn_nums, "GOSUB[ ]+"..ptn_nums } 58 | 59 | local lines = {} 60 | 61 | local linenum_match = "[0-9]+ " 62 | 63 | local get_linenum = function(line) return line:sub(1,6):match(linenum_match, 1) end -- line:sub(1,6) limits max linumber to be 99999 64 | local split_num_and_statements = function(line) 65 | local linenum = get_linenum(line) 66 | local statements = line:sub(#linenum + 1) 67 | return tonumber(linenum), statements 68 | end 69 | 70 | while not terminate_app do 71 | local __read = false 72 | local line = io.read() 73 | 74 | -- tokenise line by " " 75 | local args = {} -- shadows system args 76 | for word in line:gmatch("[^ ]+") do 77 | table.insert(args, word:upper()) 78 | end 79 | 80 | 81 | -- TODO more elegant code than IF-ELSEIF-ELSE 82 | 83 | -- massive if-else for running command, cos implementing proper command executor is too expensive here 84 | if line:sub(1,6):match(linenum_match) then -- enter new command 85 | local linenum, statements = split_num_and_statements(line) 86 | lines[tonumber(linenum)] = statements 87 | __read = true 88 | elseif args[1] == "NEW" then 89 | lines = {} 90 | elseif args[1] == "RUN" then 91 | _TBASIC.EXEC(concat_lines(lines)) 92 | elseif args[1] == "LIST" then -- LIST, LIST 42, LIST 10-80 93 | if not args[2] then 94 | print(concat_lines(lines)) 95 | else 96 | if args[2]:match("-") then -- ranged 97 | local range = {} 98 | for n in args[2]:gmatch("[^-]+") do 99 | table.insert(range, n) 100 | end 101 | local rangestart = tonumber(range[1]) 102 | local rangeend = tonumber(range[2]) 103 | 104 | if not rangestart or not rangeend then 105 | _TBASIC._ERROR.ILLEGALARG() 106 | else 107 | print(concat_lines(lines, rangestart, rangeend)) 108 | end 109 | else 110 | local linenum = tonumber(args[2]) 111 | if not linenum then 112 | _TBASIC._ERROR.ILLEGALARG() 113 | else 114 | print(concat_lines(lines, linenum, linenum)) 115 | end 116 | end 117 | end 118 | _TBASIC.PROMPT() 119 | __read = true 120 | elseif args[1] == "DELETE" then -- DELETE 30, DELETE 454-650 121 | if not args[2] then 122 | _TBASIC._ERROR.ILLEGALARG() 123 | else 124 | if args[2]:match("-") then -- ranged 125 | local range = {} 126 | for n in args[2]:gmatch("[^-]+") do 127 | table.insert(range, n) 128 | end 129 | local rangestart = tonumber(range[1]) 130 | local rangeend = tonumber(range[2]) 131 | 132 | if not rangestart or not rangeend then 133 | _TBASIC._ERROR.ILLEGALARG() 134 | else 135 | for i = rangestart, rangeend do 136 | lines[i] = nil 137 | end 138 | end 139 | else 140 | local linenum = tonumber(args[2]) 141 | if not linenum then 142 | _TBASIC._ERROR.ILLEGALARG() 143 | else 144 | lines[linenum] = nil 145 | end 146 | end 147 | end 148 | elseif args[1] == "EXIT" then 149 | terminate_app = true 150 | break 151 | elseif args[1] == "SAVE" then 152 | local status, err = pcall(function() 153 | if fs and fs.open then -- computercraft 154 | local file = fs.open(args[2], "w") 155 | file.write(concat_lines(lines)) 156 | file.close() 157 | else 158 | local file = assert(io.open(args[2], "w")) 159 | file:write(concat_lines(lines)) 160 | file:close() 161 | end 162 | end 163 | ) 164 | if err then 165 | if _TBASIC.SHOWLUAERROR then 166 | print(err) 167 | end 168 | _TBASIC._ERROR.IOERR() 169 | else 170 | print("FILE SAVED") 171 | end 172 | elseif args[1] == "LOAD" then 173 | local status, err = pcall(function() 174 | lines = {} 175 | if fs and fs.open then -- computercraft 176 | local file = fs.open(args[2], "r") 177 | local data = file.readAll("*all") 178 | for dataline in data:gmatch("[^\n]+") do 179 | if #dataline > 0 then 180 | local linenum, statements = split_num_and_statements(dataline) 181 | lines[linenum] = statements 182 | end 183 | end 184 | file.close() 185 | else 186 | local file = assert(io.open(args[2], "r")) 187 | local data = file:read("*all") 188 | for dataline in data:gmatch("[^\n]+") do 189 | if #dataline > 0 then 190 | local linenum, statements = split_num_and_statements(dataline) 191 | lines[linenum] = statements 192 | end 193 | end 194 | file:close() 195 | end 196 | end 197 | ) 198 | if err then 199 | if _TBASIC.SHOWLUAERROR then 200 | error(err) 201 | end 202 | _TBASIC._ERROR.IOERR() 203 | else 204 | print("FILE LOADED") 205 | end 206 | elseif args[1] == "RENUM" then 207 | local statement_table = {} 208 | local renumbering_table = {} 209 | local new_linenum_counter = 10 210 | -- first, get the list of commands, without line number indexing 211 | for i = 1, _TBASIC._INTPRTR.MAXLINES do 212 | if lines[i] ~= nil then 213 | --table.insert(statement_table, lines[i]) 214 | statement_table[new_linenum_counter] = lines[i] 215 | renumbering_table[i] = new_linenum_counter 216 | 217 | -- test 218 | --print("old line", i, "new line", new_linenum_counter) 219 | 220 | new_linenum_counter = new_linenum_counter + 10 221 | end 222 | end 223 | -- copy statement_table into lines table 224 | lines = statement_table 225 | 226 | -- re-number GOTO and GOSUB line numbers 227 | local line_counter = 0 -- loop counter 228 | for line_pc = 0, _TBASIC._INTPRTR.MAXLINES do 229 | local line = lines[line_pc] 230 | if line then 231 | line_counter = line_counter + 1 232 | 233 | -- replace 234 | -- extract a <- "GOTO 320" 235 | -- extract n_from from a (320), make n_to from it 236 | -- make new string b <- "GOTO "..n_to 237 | for _, match_string in ipairs(renum_targets) do 238 | local match = line:match(match_string) 239 | if match then 240 | local matching_statement = match:gsub("[ ]+"..ptn_nums, "") 241 | local target_line_old = tonumber(match:match(ptn_nums)) 242 | local target_line_new = renumbering_table[target_line_old] 243 | 244 | local gsub_from = match 245 | local gsub_to = matching_statement.." "..target_line_new 246 | 247 | -- test 248 | --print("matching_statement", matching_statement, "target_line_old", target_line_old, "target_line_new", target_line_new) 249 | --print("gsub_from", gsub_from, "gsub_to", gsub_to) 250 | 251 | -- substitute 252 | lines[line_pc] = line:gsub(gsub_from, gsub_to) 253 | end 254 | end 255 | end 256 | end 257 | elseif #line == 0 and line:byte(1) ~= 10 and line:byte(1) ~= 13 then 258 | __read = true 259 | else 260 | _TBASIC.EXEC("1 "..line) -- execute command right away 261 | end 262 | 263 | -- reset 264 | if not __read then 265 | _TBASIC.PROMPT() 266 | end 267 | end 268 | end 269 | 270 | 271 | --[[ 272 | Terran BASIC (TBASIC) 273 | Copyright (c) 2016-2017 Torvald (minjaesong) and the contributors. 274 | 275 | Permission is hereby granted, free of charge, to any person obtaining a copy of 276 | this software and associated documentation files (the Software), to deal in the 277 | Software without restriction, including without limitation the rights to use, 278 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 279 | Software, and to permit persons to whom the Software is furnished to do so, 280 | subject to the following conditions: 281 | 282 | The above copyright notice and this permission notice shall be included in all 283 | copies or substantial portions of the Software. 284 | 285 | THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 286 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 287 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 288 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 289 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 290 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 291 | SOFTWARE. 292 | ]] 293 | -------------------------------------------------------------------------------- /TBASEXEC.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | TBASIC: Simple BASIC language based on the Commodore BASIC Version 2. 3 | 4 | (C64 rulz? Nope.) 5 | 6 | 7 | How to use in your program: 8 | 9 | 1. load the script by: 10 | if you're using ComputerCraft, use: 11 | os.loadAPI "TBASEXEC.lua" 12 | else, use: 13 | require "TBASEXEC" 14 | 15 | 2. run: 16 | _TBASIC.EXEC(string of whole command) 17 | ]] 18 | 19 | if os and os.loadAPI then -- ComputerCraft 20 | os.loadAPI "TBASINCL.lua" 21 | else 22 | require "TBASINCL" 23 | end 24 | 25 | table.concat = function(t, delimeter) 26 | if #t == 0 then return "" end 27 | local outstr = t[1] 28 | for i = 2, #t do 29 | outstr = outstr..delimeter..tostring(t[i]) 30 | end 31 | 32 | return outstr 33 | end 34 | 35 | -- Copy from TBASINCL; looks like OpenComputers has a bug... 36 | function string_hash(str) 37 | local hash = 2166136261 38 | for i = 1, #str do 39 | hash = hash * 16777619 40 | hash = bit.bxor(hash, str:byte(i)) 41 | end 42 | return hash 43 | end 44 | 45 | 46 | 47 | 48 | 49 | -- INTERPRETER STATUS --------------------------------------------------------- 50 | 51 | local programlist = {} 52 | 53 | -- LEXER ---------------------------------------------------------------------- 54 | 55 | local function appendcommand(lineno, statement) 56 | if lineno > _TBASIC._INTPRTR.MAXLINES then 57 | _TBASIC._ERROR.LINETOOBIG() 58 | elseif lineno < 0 then 59 | _TBASIC._ERROR.NOLINENUM() 60 | else 61 | programlist[lineno] = statement 62 | end 63 | end 64 | 65 | do -- Avoid heap allocs for performance 66 | local tokens = {" ", "\t", ",", "(", ")"} -- initial obvious tokens 67 | local longest_token_len = 0 68 | -- build 'tokens' table from list of operators from the language 69 | for _, v in ipairs(_TBASIC._OPERATR) do 70 | if not v:match("[A-Za-z]") then -- we want non-alphabetic operators as a token 71 | table.insert(tokens, v) 72 | -- get longest_token_len, will be used for 'lookahead' 73 | local tokenlen = #v 74 | if longest_token_len < #v then 75 | longest_token_len = #v 76 | end 77 | end 78 | end 79 | -- sort them out using ther hash for binary search 80 | table.sort(tokens, function(a, b) return string_hash(a) < string_hash(b) end) 81 | 82 | 83 | function parsewords(line) 84 | if line == nil then return end 85 | local upperline = line:upper() 86 | 87 | ----------------------- 88 | -- check line sanity -- 89 | ----------------------- 90 | 91 | -- filter internal-use-only functions/operators (aka OPCODES) 92 | for _, opcode in ipairs(_G._TBASIC.OPILLEGAL) do 93 | if upperline:find(opcode) ~= nil then 94 | _TBASIC._ERROR.SYNTAXAT(opcode) 95 | end 96 | end 97 | -- filter for IF statement 98 | if upperline:sub(1, 2) == "IF" then 99 | -- no matching THEN 100 | if not upperline:match("THEN") then 101 | _TBASIC._ERROR.NOMATCHING("IF", "THEN") 102 | -- assignment on IF clause 103 | elseif upperline:match("IF[^\n]+THEN"):match("[^=+%-*/%%<>!]=[^=<>]") or 104 | upperline:match("IF[^\n]+THEN"):match(":=") then 105 | _TBASIC._ERROR.ASGONIF() 106 | end 107 | end 108 | 109 | -------------------------------------------------- 110 | -- automatically infer and insert some commands -- 111 | -------------------------------------------------- 112 | -- (This is starting to get dirty...) 113 | 114 | -- unary minus 115 | for matchobj in line:gmatch("%-[0-9]+") do 116 | local newline = line:gsub(matchobj, "MINUS "..matchobj:sub(2, #matchobj)) 117 | line = newline 118 | end 119 | -- conditional for IF 120 | -- if IF statement has no appended paren 121 | if upperline:sub(1, 2) == "IF" and not upperline:match("IF[ ]*%(") then 122 | local newline = line:gsub("[Ii][Ff]", "IF ( ", 1):gsub("[Tt][Hh][Ee][Nn]", " ) THEN", 1) 123 | line = newline 124 | end 125 | -- special treatment for FOR 126 | if upperline:sub(1, 3) == "FOR" then 127 | if line:match("[0-9]?%.[0-9]") then -- real number used (e.g. "3.14", ".5") 128 | _TBASIC._ERROR.ILLEGALARG() 129 | else 130 | local varnameintm = line:match(" [^\n]+[ =]") 131 | 132 | if varnameintm then 133 | local varname = varnameintm:match("[^= ]+") 134 | if varname then 135 | local newline = line:gsub(" "..varname.."[ =]", " $"..varname.." "..varname.." = ") 136 | line = newline:gsub("= =", "=") 137 | else 138 | _TBASIC._ERROR.SYNTAX() 139 | end 140 | end 141 | -- basically, "FOR x x = 1 TO 10", which converts to "x x 1 10 TO = FOR", 142 | -- which is executed (in RPN) in steps of: 143 | -- "x x 1 10 TO = FOR" 144 | -- "x x (arr) = FOR" 145 | -- "x FOR" -- see this part? we need extra 'x' to feed in to make the FOR statement work 146 | end 147 | end 148 | -- array assignation 149 | -- FROM: arrayname(...) = 42 150 | -- TO : ASSIGNARRAY(%arrayname, 42, ...) 151 | if line:match("[a-zA-Z_0-9]+%([0-9]+[, 0-9]*%) ?= ?[^\n]+") then 152 | local arrayname = line:match("[a-zA-Z_0-9]+") 153 | local dimension = line:match("%([0-9]+[, 0-9]*%)") 154 | dimension = dimension:sub(2, #dimension - 1) 155 | local assign_value = line:sub(#line:match("[a-zA-Z_0-9]+%([0-9]+[, 0-9]*%) ?= ?") + 1, #line) 156 | 157 | line = "ASSIGNARRAY("..arrayname..","..assign_value..","..dimension..")" 158 | end 159 | 160 | 161 | 162 | 163 | printdbg("\nparsing line", line) 164 | 165 | 166 | 167 | lextable = {} 168 | isquote = false 169 | quotemode = false 170 | wordbuffer = "" 171 | local function flush() 172 | if (#wordbuffer > 0) then 173 | table.insert(lextable, wordbuffer) 174 | wordbuffer = "" 175 | end 176 | end 177 | local function append(char) 178 | wordbuffer = wordbuffer..char 179 | end 180 | local function append_no_whitespace(char) 181 | if char ~= " " and char ~= "\t" then 182 | wordbuffer = wordbuffer..char 183 | end 184 | end 185 | 186 | -- return: lookless_count on success, nil on failure 187 | local function isdelimeter(string) 188 | local cmpval = function(table_elem) return string_hash(table_elem) end 189 | local lookless_count = #string 190 | local ret = nil 191 | repeat 192 | ret = table.binsearch(tokens, string:sub(1, lookless_count), cmpval) 193 | lookless_count = lookless_count - 1 194 | until ret or lookless_count < 1 195 | return ret and lookless_count + 1 or false 196 | end 197 | 198 | local i = 1 -- Lua Protip: variable in 'for' is immutable, and is different from general variable table, even if they have same name 199 | while i <= #line do 200 | local c = string.char(line:byte(i)) 201 | 202 | local lookahead = line:sub(i, i+longest_token_len) 203 | 204 | if isquote then 205 | if c == [["]] then 206 | flush() 207 | isquote = false 208 | else 209 | append(c) 210 | end 211 | else 212 | if c == [["]] then 213 | isquote = true 214 | append_no_whitespace("~") 215 | else 216 | local delimsize = isdelimeter(lookahead) -- returns nil if no matching delimeter found 217 | if delimsize then 218 | flush() -- flush buffer 219 | append_no_whitespace(lookahead:sub(1, delimsize)) 220 | flush() -- flush this delimeter 221 | i = i + delimsize - 1 222 | else 223 | append_no_whitespace(c) 224 | end 225 | end 226 | end 227 | 228 | i = i + 1 229 | end 230 | flush() -- don't forget this! 231 | 232 | 233 | return lextable 234 | end 235 | end 236 | 237 | local function readprogram(program) 238 | for line in program:gmatch("[^\n]+") do 239 | lineno = line:match("[0-9]+ ", 1) 240 | 241 | if not lineno then 242 | _TBASIC._ERROR.NOLINENUM() 243 | end 244 | 245 | statement = line:sub(#lineno + 1) 246 | 247 | appendcommand(tonumber(lineno), statement) 248 | end 249 | end 250 | 251 | do -- Avoid heap allocs for performance 252 | local function stackpush(t, v) 253 | t[#t + 1] = v 254 | end 255 | 256 | local function stackpop(t) 257 | local v = t[#t] 258 | t[#t] = nil 259 | return v 260 | end 261 | 262 | local function stackpeek(t) 263 | local v = t[#t] 264 | return v 265 | end 266 | 267 | local function unmark(word) 268 | if type(word) == "table" then return word end 269 | return word:sub(2, #word) 270 | end 271 | 272 | local function isoperator(word) 273 | if word == nil then return false end 274 | return word:byte(1) == 35 275 | end 276 | 277 | local isvariable = _TBASIC.isvariable 278 | local isarray = _TBASIC.isarray 279 | local isnumber = _TBASIC.isnumber 280 | local isstring = _TBASIC.isstring 281 | 282 | local function isuserfunc(word) 283 | if type(word) == "table" then return false end 284 | if word == nil then return false end 285 | return word:byte(1) == 64 286 | end 287 | 288 | local function isbuiltin(word) 289 | if type(word) == "table" then return false end 290 | if word == nil then return false end 291 | return word:byte(1) == 38 292 | end 293 | 294 | local function iskeyword(word) 295 | if word == nil then return false end 296 | return isoperator(word) or isuserfunc(word) or isbuiltin(word) 297 | end 298 | 299 | local function isassign(word) 300 | if word == nil then return false end 301 | return word ~= "==" and word ~= ">=" and word ~= "<=" and word:byte(#word) == 61 302 | end 303 | 304 | -- returns truthy value "terminate_loop" upon termination of loop; nil otherwise. 305 | local function execword(word, args) 306 | if not _TBASIC.__appexit then 307 | printdbg("--> execword", word) 308 | printdbg("--> execword_args", table.unpack(args)) 309 | 310 | if word == "IF" then 311 | printdbg("--> branch statement 'IF'") 312 | if not _TBASIC.__readvar(args[1]) then -- if condition 'false' 313 | printdbg("--> if condition 'false'", table.unpack(args)) 314 | return "terminate_loop" -- evaluated as 'true' to Lua 315 | else 316 | printdbg("--> if condition 'true'", table.unpack(args)) 317 | end 318 | end 319 | 320 | printdbg("--> execword_outarg", table.unpack(args)) 321 | local result = _TBASIC.LUAFN[word][1](table.unpack(args)) 322 | 323 | printdbg("--> result", result) 324 | stackpush(execstack, result) 325 | end 326 | end 327 | 328 | function printdbg(...) 329 | local debug = true--false 330 | if debug then print("TBASEXEC", ...) end 331 | end 332 | 333 | 334 | function interpretline(line) 335 | if not _TBASIC.__appexit then 336 | --[[ 337 | impl 338 | 339 | 1. (normalise expr using parsewords) 340 | 2. use _TBASIC.RPNPARSR to convert to RPN 341 | 3. execute RPN op set like FORTH 342 | 343 | * "&" - internal functions 344 | * "@" - user-defined functions 345 | * "$" - variables (builtin constants and user-defined) -- familiar, eh? 346 | * "#" - operators 347 | * "%" - arrays 348 | * "~" - strings 349 | * none prepended - data (number or string) 350 | ]] 351 | 352 | lextable = parsewords(line) 353 | local vararg = -13 -- magic 354 | 355 | 356 | if lextable and lextable[1] ~= nil then 357 | if lextable[1]:upper() == "REM" then return nil end 358 | 359 | printdbg("lextable", table.concat(lextable, "|")) 360 | 361 | -- execute expression 362 | exprlist = _TBASIC.TORPN(lextable) -- 2 2 #+ &PRINT for "PRINT 2+2" 363 | 364 | printdbg("trying to exec", table.concat(exprlist, " "), "\n--------") 365 | 366 | execstack = {} 367 | 368 | for _, word in ipairs(exprlist) do 369 | printdbg("stack before", table.concat(execstack, " ")) 370 | printdbg("word", word) 371 | 372 | if iskeyword(word) then 373 | printdbg("is keyword") 374 | 375 | funcname = unmark(word) 376 | args = {} 377 | argsize = _TBASIC._GETARGS(funcname) 378 | 379 | printdbg("argsize", argsize) 380 | 381 | if not argsize then 382 | _TBASIC._ERROR.DEV_UNIMPL(funcname) 383 | else 384 | if argsize ~= vararg then 385 | -- consume 'argsize' elements from the stack 386 | for argcnt = argsize, 1, -1 do 387 | if #execstack == 0 then 388 | _TBASIC._ERROR.ARGMISSING(funcname) 389 | end 390 | args[argcnt] = stackpop(execstack) 391 | end 392 | else 393 | -- consume entire stack 394 | local reversedargs = {} 395 | 396 | while #execstack > 0 and 397 | (isvariable(stackpeek(execstack)) or isnumber(stackpeek(execstack)) or 398 | isstring(stackpeek(execstack))) 399 | do 400 | stackpush(reversedargs, stackpop(execstack)) 401 | end 402 | -- reverse 'args' 403 | while #reversedargs > 0 do 404 | stackpush(args, stackpop(reversedargs)) 405 | end 406 | end 407 | 408 | local terminate_loop = execword(funcname, args) 409 | 410 | if terminate_loop then 411 | printdbg("--> termination of loop") 412 | printdbg("--------") 413 | break 414 | end 415 | end 416 | elseif isvariable(word) then 417 | printdbg("is variable") 418 | stackpush(execstack, word) -- push raw variable ($ sign retained) 419 | elseif isarray(word) then 420 | printdbg("is array") 421 | 422 | -- stack: ARR(3,2,5) -> 3 2 5 %ARR 423 | -- ARR(3,2,5) = 42 -> 3 2 5 [42] %ARR &= 424 | -- ARR(0,1,2) = ARR(10,11,12) -> 0 1 2 [10 11 12 %ARR] 425 | 426 | -- consume entire stack for array indices 427 | local arrayargs = {} 428 | local reversedargs = {} 429 | 430 | while #execstack > 0 and 431 | (isvariable(stackpeek(execstack)) or isnumber(stackpeek(execstack))) 432 | do 433 | stackpush(reversedargs, stackpop(execstack)) 434 | end 435 | -- reverse 'args' 436 | while #reversedargs > 0 do 437 | stackpush(arrayargs, stackpop(reversedargs)) 438 | end 439 | 440 | 441 | print(word.."("..table.concat(arrayargs, ",")..")") 442 | 443 | 444 | 445 | 446 | else 447 | printdbg("is data") 448 | stackpush(execstack, word) -- push number or string 449 | end 450 | 451 | printdbg("stack after", table.concat(execstack, " ")) 452 | printdbg("--------") 453 | end 454 | 455 | -- if execstack is not empty, something is wrong 456 | if #execstack > 0 then 457 | _TBASIC._ERROR.SYNTAX() -- cannot reliably pinpoint which statement has error; use generic error 458 | end 459 | end 460 | end 461 | end 462 | end 463 | 464 | 465 | local function termination_condition() 466 | return terminated or 467 | _TBASIC.__appexit or 468 | #_TBASIC._INTPRTR.CALLSTCK > _TBASIC._INTPRTR.STACKMAX 469 | end 470 | 471 | local function fetchnextcmd() 472 | cmd = nil 473 | repeat 474 | _TBASIC._INTPRTR.PROGCNTR = _TBASIC._INTPRTR.PROGCNTR + 1 475 | cmd = programlist[_TBASIC._INTPRTR.PROGCNTR] 476 | 477 | if _TBASIC._INTPRTR.PROGCNTR > _TBASIC._INTPRTR.MAXLINES then 478 | terminated = true 479 | break 480 | end 481 | until cmd ~= nil 482 | 483 | if cmd ~= nil then 484 | if _TBASIC._INTPRTR.TRACE then 485 | print("PC", _TBASIC._INTPRTR.PROGCNTR) 486 | end 487 | 488 | return cmd 489 | end 490 | end 491 | 492 | 493 | local function interpretall() 494 | 495 | terminated = false 496 | 497 | repeat 498 | interpretline(fetchnextcmd()) 499 | until termination_condition() 500 | end 501 | 502 | -- END OF LEXER --------------------------------------------------------------- 503 | 504 | -- _TBASIC.SHOWLUAERROR = false -- commented; let the shell handle it 505 | 506 | local testprogram = nil 507 | 508 | _G._TBASIC.EXEC = function(cmdstring) -- you can access this interpreter with this global function 509 | _TBASIC._INTPRTR.RESET() 510 | programlist = {} -- wipe out previous commands from interpreter (do not delete) 511 | readprogram(cmdstring) 512 | interpretall() 513 | end 514 | 515 | 516 | if testprogram then 517 | _TBASIC._INTPRTR.RESET() 518 | programlist = {} -- wipe out previous commands from interpreter (do not delete) 519 | readprogram(testprogram) 520 | interpretall() 521 | end 522 | 523 | 524 | --[[ 525 | Terran BASIC (TBASIC) 526 | Copyright (c) 2016-2017 Torvald (minjaesong) and the contributors. 527 | 528 | Permission is hereby granted, free of charge, to any person obtaining a copy of 529 | this software and associated documentation files (the Software), to deal in the 530 | Software without restriction, including without limitation the rights to use, 531 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 532 | Software, and to permit persons to whom the Software is furnished to do so, 533 | subject to the following conditions: 534 | 535 | The above copyright notice and this permission notice shall be included in all 536 | copies or substantial portions of the Software. 537 | 538 | THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 539 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 540 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 541 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 542 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 543 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 544 | SOFTWARE. 545 | ]] 546 | -------------------------------------------------------------------------------- /TBASINCL.lua: -------------------------------------------------------------------------------- 1 | -- TBASIC includes 2 | 3 | if not _G.bit and not _G.bit32 then 4 | error("This lua implementation does not have bit/bit32 library, aborting.") 5 | end 6 | 7 | if not _G.unpack and not table.unpack then 8 | error("This lua implementation does not have unpack() function, aborting.") 9 | end 10 | 11 | if _G.bit32 then _G.bit = bit32 end -- Lua 5.2 and LuaJIT compatibility (which has 'bit32' but no 'bit') 12 | if _G.unpack and not _G.table.unpack then _G.table.unpack = unpack end -- LuaJIT compatibility 13 | 14 | 15 | -- simple binary search stole and improved from Kotlin Language 16 | -- @param cmpval: function that returns numerical value of the value used for searching. 17 | -- implementation: function(s) return whateverhashornumber(s) end 18 | -- e.g. function(s) return string.hash(s) end -- for string values 19 | -- you must implement it by yourself! 20 | do -- Avoid heap allocs for performance 21 | local default_cmp_fn = function(s) return string.hash(tostring(s)) end 22 | 23 | function table.binsearch(t, value, cmpval) 24 | local low = 1 25 | local high = #t 26 | local cmp = cmpval or default_cmp_fn 27 | 28 | local value = cmp(value) 29 | 30 | while low <= high do 31 | local mid = bit.rshift((low + high), 1) 32 | local midVal = t[mid] 33 | 34 | if value > cmp(midVal) then 35 | low = mid + 1 36 | elseif value < cmp(midVal) then 37 | high = mid - 1 38 | else 39 | return mid -- key found 40 | end 41 | end 42 | return nil -- key not found 43 | end 44 | end 45 | 46 | 47 | _G._TBASIC = {} 48 | _G._TBASIC._VERNUM = 0x0004 -- 0.4 49 | _G._TBASIC._VERSION = tonumber(string.format("%d.%d", bit.rshift(_TBASIC._VERNUM, 8), bit.band(_TBASIC._VERNUM, 0xFF))) 50 | _G._TBASIC._HEADER = string.format(" **** TERRAN BASIC V%d.%d **** ", bit.rshift(_TBASIC._VERNUM, 8), bit.band(_TBASIC._VERNUM, 0xFF)) 51 | _G._TBASIC.PROMPT = function() print("\nREADY.") end 52 | _G._TBASIC._INVOKEERR = function(msg, msg1) 53 | if msg1 then 54 | print("?L".._G._TBASIC._INTPRTR.PROGCNTR..": "..msg.." "..msg1) 55 | else 56 | print("?L".._G._TBASIC._INTPRTR.PROGCNTR..": "..msg, "ERROR") 57 | end 58 | if _TBASIC.SHOWLUAERROR then error("Error thrown") end 59 | --os.exit(1) -- terminate 60 | _G._TBASIC.__appexit = true -- duh, computercraft 61 | end 62 | _G._TBASIC._ERROR = { 63 | SYNTAX = function() _TBASIC._INVOKEERR("SYNTAX") end, 64 | SYNTAXAT = function(word) _TBASIC._INVOKEERR("SYNTAX ERROR AT", "'"..word.."'") end, 65 | TYPE = function() _TBASIC._INVOKEERR("TYPE MISMATCH") end, 66 | ILLEGALNAME = function(name, reason) 67 | if reason then 68 | _TBASIC._INVOKEERR("ILLEGAL NAME: ".."'"..name.."'", "REASON:"..reason) 69 | else 70 | _TBASIC._INVOKEERR("ILLEGAL NAME:", "'"..name.."'") 71 | end 72 | end, 73 | ILLEGALARG = function(expected, got) 74 | if (not expected) and (not got) then 75 | _TBASIC._INVOKEERR("ILLEGAL QUANTITY") 76 | elseif not got then 77 | _TBASIC._INVOKEERR(expected:upper().." EXPECTED") 78 | else 79 | _TBASIC._INVOKEERR(expected:upper().." EXPECTED,", "GOT "..got:upper()) 80 | end 81 | end, 82 | NOSUCHLINE = function(line) _TBASIC._INVOKEERR("NO SUCH LINE:", line) end, 83 | NULFN = function(var) _TBASIC._INVOKEERR("UNDEFINED FUNCTION:", "'"..var.."'") end, 84 | NULVAR = function(var) _TBASIC._INVOKEERR("UNDEFINED VARIABLE:", "'"..var.."'") end, 85 | DIV0 = function() _TBASIC._INVOKEERR("DIVISION BY ZERO") end, 86 | NAN = function() _TBASIC._INVOKEERR("NOT A NUMBER") end, 87 | STACKOVFL = function() _TBASIC._INVOKEERR("TOO MANY RECURSION") end, 88 | LINETOOBIG = function() _TBASIC._INVOKEERR("TOO BIG LINE NUMBER") end, 89 | NOLINENUM = function() _TBASIC._INVOKEERR("NO LINE NUMBER") end, 90 | ABORT = function(reason) 91 | if reason then 92 | _TBASIC._INVOKEERR("PROGRAM", "ABORTED: "..reason) 93 | else 94 | _TBASIC._INVOKEERR("PROGRAM", "ABORTED") 95 | end 96 | end, 97 | ARGMISSING = function(fname, remark) 98 | if remark then 99 | _TBASIC._INVOKEERR("MISSING ARGUMENT(S) FOR", "'"..fname.."' ("..remark..")") 100 | else 101 | _TBASIC._INVOKEERR("MISSING ARGUMENT(S) FOR", "'"..fname.."'") 102 | end 103 | end, 104 | NOMATCHING = function(fname, match) _TBASIC._INVOKEERR("'"..fname.."' HAS NO MACTHING", "'"..match.."'") end, 105 | TOOLONGEXEC = function() _TBASIC._INVOKEERR("TOO LONG WITHOUT YIELDING") end, 106 | RETURNWOSUB = function() _TBASIC._INVOKEERR("RETURN WITHOUT GOSUB") end, 107 | NEXTWOFOR = function() _TBASIC._INVOKEERR("NEXT WITHOUT FOR") end, 108 | ASGONIF = function() _TBASIC._INVOKEERR("ASSIGNMENT ON IF CLAUSE") end, 109 | SHELLCMD = function() _TBASIC._INVOKEERR("THIS IS A SHELL COMMAND") end, 110 | IOERR = function() _TBASIC._INVOKEERR("READ/WRITE") end, 111 | NOSYMFORNEXT = function() _TBASIC._INVOKEERR("NO VAR FOR NEXT CLAUSE") end, 112 | 113 | DEV_FUCKIT = function() _TBASIC._INVOKEERR("FEELING DIRTY") end, 114 | DEV_UNIMPL = function(fname) _TBASIC._INVOKEERR("UNIMPLEMENTED SYNTAX:", "'"..fname.."'") end 115 | } 116 | _G._TBASIC._FNCTION = { -- aka OPCODES because of some internal-use-only functions 117 | -- variable control 118 | "CLR", -- deletes all user-defined variables and functions 119 | "DIM", -- allocates an array 120 | "DEF", -- defines new function. Synopsis "DEF FN FOOBAR(arg)" 121 | "FN", -- denotes function 122 | -- flow control 123 | "GO", "GOTO", -- considered harmful 124 | "GOSUB", "RETURN", 125 | "FOR", "NEXT", "IN", 126 | "DO", -- reserved only 127 | "IF", "THEN", 128 | "LABEL", -- line number alias 129 | --"ELSE", "ELSEIF", -- reserved only, will not be implemented 130 | "END", -- terminate program cleanly 131 | "ABORT", -- break as if an error occured 132 | "ABORTM", -- ABORT with message 133 | -- stdio 134 | "PRINT", 135 | "INPUT", 136 | "GET", -- read single key 137 | "HTAB", "TAB", -- set cursor's X position 138 | "VTAB", -- set cursor's Y position 139 | "SCROLL", 140 | "CLS", -- clear screen 141 | "TEXTCOL", -- foreground colour 142 | "BACKCOL", -- background colour 143 | -- mathematics 144 | "ABS", "SIN", "COS", "TAN", "FLOOR", "CEIL", "ROUND", "LOG", 145 | "INT", -- integer part of a number (3.78 -> 3, -3.03 -> -3) 146 | "RND", -- random number 0.0 <= x < 1.0 147 | "SGN", -- sign of a number (-1, 0, 1) 148 | "SQRT", -- square root 149 | "CBRT", -- cubic root 150 | "MAX", "MIN", 151 | "INV", -- returns (1.0 / arg) 152 | "RAD", -- converts deg into rad 153 | -- string manipulation 154 | "LEN", 155 | "LEFT", -- just like in Excel 156 | "MID", -- -- just like in Excel (substring) 157 | "RIGHT", -- just like in Excel 158 | -- type conversion 159 | "ASC", -- converts a charactor into its code point 160 | "CHR", -- converts an integer into corresponding character 161 | "STR", -- number to string 162 | "VAL", -- string to number 163 | -- misc 164 | "REM", -- mark this line as comment 165 | "NEW", -- clean up any programs on the buffer (this is a Shell function) 166 | -- pc speaker 167 | "BEEP", -- beeps. Synopsis: "BEEP", "BEEP [pattern]" (not for CC) 168 | "TEMIT", -- emits a tone. Synopsis: "TEMIT [frequency] [seconds]" (not for CC) 169 | -- commands 170 | "RUN", -- run a program or a line. Synopsis: "RUN", "RUN [line]" (this is a Shell function) 171 | "LIST", -- list currently entered program. Synopsis: "LIST", "LIST [line]", "LIST [from "-" to]" (this is a Shell function) 172 | "NEW", -- clear program lines buffer (this is a Shell function) 173 | "RENUM", -- re-number BASIC statements (this is a Shell function) 174 | "DELETE", -- delete line (this is a Shell function) 175 | -- external IO 176 | "LOAD", -- file load. Synopsis: "LOAD [filename]" 177 | "SAVE", -- file save. Synopsis: "SAVE [filename]" 178 | -- internal use only!! 179 | "ASSIGNARRAY", 180 | "READARRAY", 181 | } 182 | _G._TBASIC._OPERATR = { 183 | -- operators 184 | ">>>", "<<", ">>", "|", "&", "XOR", "!", -- bitwise operations 185 | ";", -- string concatenation 186 | "==", ">", "<", "<=", "=<", ">=", "=>", -- TURN OFF your font ligature for this part if you're seeing two identical symbols! 187 | "!=", "<>", "><", -- not equal 188 | "=", ":=", -- assign 189 | "AND", "OR", "NOT", 190 | "^", -- math.pow, 0^0 should return 1. 191 | "*", "/", "+", "-", -- arithmetic operations 192 | "%", -- math.fmod 193 | "TO", "STEP", -- integer sequence operator 194 | "MINUS", -- unary minus (internal use only!!) 195 | "+=", "-=", "*=", "/=", "%=" -- C-style assign 196 | } 197 | _G._TBASIC.OPILLEGAL = { -- illegal functions and operators (internal-use-only opcodes) 198 | "ASSIGNARRAY", 199 | "READARRAY", 200 | "MINUS", 201 | } 202 | _G._TBASIC._INTPRTR = {} 203 | _G._TBASIC._INTPRTR.TRACE = false -- print program counter while execution 204 | _G._TBASIC.SHOWLUAERROR = true 205 | 206 | local function stackpush(t, v) 207 | t[#t + 1] = v 208 | end 209 | 210 | local function stackpop(t) 211 | local v = t[#t] 212 | t[#t] = nil 213 | return v 214 | end 215 | 216 | local function stackpeek(t) 217 | local v = t[#t] 218 | return v 219 | end 220 | 221 | function string.hash(str) -- FNV-1 32-bit 222 | local hash = 2166136261 223 | for i = 1, #str do 224 | hash = hash * 16777619 225 | hash = bit.bxor(hash, str:byte(i)) 226 | end 227 | return hash 228 | end 229 | 230 | _G._TBASIC._INTPRTR.RESET = function() 231 | _TBASIC.__appexit = false 232 | _TBASIC._INTPRTR.PROGCNTR = 0 233 | _TBASIC._INTPRTR.MAXLINES = 999999 234 | _TBASIC._INTPRTR.VARTABLE = {} -- table of variables. [NAME] = data 235 | _TBASIC._INTPRTR.FNCTABLE = {} -- table of functions. [NAME] = array of strings? (TBA) 236 | _TBASIC._INTPRTR.CALLSTCK = {} -- return points (line number) 237 | _TBASIC._INTPRTR.LINELABL = {} -- LABEL statement table 238 | _TBASIC._INTPRTR.STACKMAX = 2000 239 | _TBASIC._INTPRTR.CNSTANTS = { 240 | M_PI = 3.141592653589793, -- this is a standard implementation 241 | M_2PI = 6.283185307179586, -- this is a standard implementation 242 | M_E = 2.718281828459045, -- this is a standard implementation 243 | M_ROOT2 = 1.414213562373095, -- this is a standard implementation 244 | TRUE = true, 245 | FALSE = false, 246 | NIL = nil, 247 | _VERSION = _TBASIC._VERSION 248 | } 249 | end 250 | 251 | 252 | 253 | -- FUNCTION IMPLEMENTS -------------------------------------------------------- 254 | 255 | local function __readvar(varname) 256 | -- varname could be either real name, or a data 257 | -- if varname is a string that can be represented as number, returns tonumber(varname) ("4324" -> 4324) 258 | -- if varname is a TBASIC string, return resolved string ("~FOOBAR" -> "FOOBAR") 259 | -- if varname is a TBASIC variable, return resolved variable ("$FOO" -> any value stored in variable 'FOO') 260 | 261 | --print("readvar_varname", varname) 262 | 263 | if type(varname) == "table" or type(varname) == "nil" or type(varname) == "boolean" then 264 | return varname 265 | end 266 | 267 | if tonumber(varname) then 268 | return tonumber(varname) 269 | end 270 | 271 | if varname:byte(1) == 126 then 272 | return varname:sub(2, #varname) 273 | end 274 | 275 | if varname:byte(1) == 36 then 276 | local data = varname:sub(2, #varname) 277 | if tonumber(data) then 278 | return tonumber(data) 279 | else 280 | -- try for constants 281 | local retval = _TBASIC._INTPRTR.CNSTANTS[data:upper()] 282 | if retval ~= nil then return retval 283 | -- try for variable table 284 | else return _TBASIC._INTPRTR.VARTABLE[data:upper()] end 285 | end 286 | elseif varname:byte(1) == 37 then 287 | local array = _TBASIC._INTPRTR.VARTABLE[varname:sub(2, #varname):upper()] 288 | if not array or type(array) ~= "table" then 289 | return false 290 | elseif array.identifier == "tbasicarray" then 291 | return array 292 | else 293 | error(varname.." is not an TBASIC array") 294 | end 295 | else 296 | return varname -- already resolved 297 | end 298 | end 299 | 300 | local function __makenewtbasicarray(dimensional) 301 | local t = {} 302 | t.dimension = dimensional 303 | t.data = {} -- this data WILL BE one-based whilst TBASIC is zero-based. BEWARE! 304 | t.identifier = "tbasicarray" 305 | 306 | return t 307 | end 308 | 309 | -- ARRNAME(3,2,4), arguments denote max possible index, starting from zero 310 | function gfnarrayget(arrname, ...) 311 | local t = __readvar(arrname) 312 | 313 | local function getdimensionalsum(iteration) 314 | local i = 0 315 | for dim = iteration, (#t.dimension) - 1 do 316 | i = i + t.dimension[dim] 317 | end 318 | return i 319 | end 320 | 321 | local indices = {...} 322 | local actualIndex = 0 323 | for d = 1, #indices do 324 | if (d < #indices) then 325 | actualIndex = actualIndex + getdimensionalsum(d) * indices[d] 326 | else 327 | actualIndex = actualIndex + indices[d] 328 | end 329 | end 330 | 331 | return t.data[actualIndex + 1] -- actualIndex is zero-based, but t.data is one-based 332 | end 333 | 334 | function gfnarrayset(arrname, value, ...) 335 | local t = __readvar(arrname) 336 | 337 | local function getdimensionalsum(iteration) 338 | local i = 0 339 | for dim = iteration, (#t.dimension) - 1 do 340 | i = i + t.dimension[dim] 341 | end 342 | return i 343 | end 344 | 345 | local indices = {...} 346 | 347 | local actualIndex = 0 348 | for d = 1, #indices do 349 | if (d < #indices) then 350 | actualIndex = actualIndex + getdimensionalsum(d) * indices[d] 351 | else 352 | actualIndex = actualIndex + indices[d] 353 | end 354 | end 355 | 356 | t.data[actualIndex + 1] = value -- actualIndex is zero-based, but t.data is one-based 357 | end 358 | 359 | local function __assert(aarg, expected) 360 | local arg = __readvar(aarg) 361 | 362 | if type(arg) ~= expected then 363 | _TBASIC._ERROR.ILLEGALARG(expected, type(arg)) 364 | return 365 | end 366 | end 367 | 368 | local function __assertlhand(llval, expected) 369 | local lval = __readvar(llval) 370 | 371 | if type(lval) ~= expected then 372 | _TBASIC._ERROR.ILLEGALARG("LHAND: "..expected, type(lval)) 373 | return 374 | end 375 | end 376 | 377 | local function __assertrhand(rrval, expected) 378 | local rval = __readvar(rrval) 379 | 380 | if type(rval) ~= expected then 381 | _TBASIC._ERROR.ILLEGALARG("RHAND: "..expected, type(rval)) 382 | return 383 | end 384 | end 385 | 386 | local function __checknumber(aarg) 387 | local arg = __readvar(aarg) 388 | 389 | if arg == nil then 390 | _TBASIC._ERROR.ILLEGALARG("number", type(arg)) 391 | return 392 | else 393 | if type(arg) == "table" then 394 | repeat 395 | tval = arg[1] 396 | arg = tval 397 | until type(tval) ~= "table" 398 | end 399 | 400 | n = tonumber(arg) 401 | if n == nil then 402 | _TBASIC._ERROR.ILLEGALARG("number", type(arg)) 403 | return 404 | else 405 | return n 406 | end 407 | end 408 | end 409 | 410 | local function __checkstring(aarg) 411 | local arg = __readvar(aarg) 412 | 413 | if type(arg) == "function" then 414 | _TBASIC._ERROR.ILLEGALARG("STRING/NUMBER/BOOL", type(arg)) 415 | return 416 | end 417 | 418 | if type(arg) == "table" then 419 | repeat 420 | tval = arg[1] 421 | arg = tval 422 | until type(tval) ~= "table" 423 | end 424 | 425 | local strarg = tostring(arg) 426 | return strarg:byte(1) == 126 and strarg:sub(2, #strarg) or strarg 427 | end 428 | 429 | local function __resolvevararg(...) 430 | local ret = {} 431 | for _, varname in ipairs({...}) do 432 | table.insert(ret, __readvar(varname)) 433 | end 434 | return ret 435 | end 436 | 437 | _G._TBASIC.__assert = __assert 438 | _G._TBASIC.__assertlhand = __assertlhand 439 | _G._TBASIC.__assertrhand = __assertrhand 440 | _G._TBASIC.__checknumber = __checknumber 441 | _G._TBASIC.__checkstring = __checkstring 442 | _G._TBASIC.__readvar = __readvar 443 | _G._TBASIC.__resolvevararg = __resolvevararg 444 | 445 | 446 | --[[ 447 | Function implementations 448 | 449 | Cautions: 450 | * Every function that returns STRING must prepend "~" 451 | ]] 452 | 453 | 454 | local function _fnprint(...) 455 | function printarg(arg) 456 | if type(arg) == "function" then 457 | _TBASIC._ERROR.SYNTAX() 458 | return 459 | end 460 | 461 | if type(arg) == "boolean" then 462 | if arg then io.write(" TRUE") 463 | else io.write(" FALSE") end 464 | elseif _TBASIC.isstring(arg) then 465 | io.write(__checkstring(arg)) 466 | elseif _TBASIC.isnumber(arg) then -- if argument can be turned into a number (e.g. 14321, "541") 467 | io.write(" "..arg) 468 | elseif type(arg) == "table" then 469 | printarg(arg[1]) -- recursion 470 | else 471 | io.write(tostring(arg)) 472 | end 473 | end 474 | 475 | local args = __resolvevararg(...) 476 | 477 | if #args < 1 then 478 | io.write "" 479 | else 480 | for i, arg in ipairs(args) do 481 | if i > 1 then io.write "\t" end 482 | 483 | printarg(arg) 484 | end 485 | end 486 | 487 | io.write "\n" 488 | end 489 | 490 | local function _fngoto(lnum) 491 | local linenum = nil 492 | if _TBASIC.isnumber(lnum) then 493 | linenum = __checknumber(lnum) 494 | else 495 | linenum = _TBASIC._INTPRTR.LINELABL[__checkstring(lnum)] 496 | end 497 | 498 | if linenum == nil or linenum < 1 then 499 | _TBASIC._ERROR.NOSUCHLINE(linenum) 500 | return 501 | end 502 | 503 | _TBASIC._INTPRTR.PROGCNTR = linenum - 1 504 | end 505 | 506 | local function _fnnewvar(varname, value) 507 | _TBASIC._INTPRTR.VARTABLE[varname:upper()] = __readvar(value) 508 | end 509 | 510 | local function _fngosub(lnum) 511 | local linenum = nil 512 | if _TBASIC.isnumber(lnum) then 513 | linenum = __checknumber(lnum) 514 | else 515 | linenum = _TBASIC._INTPRTR.LINELABL[__checkstring(lnum)] 516 | end 517 | 518 | stackpush(_TBASIC._INTPRTR.CALLSTCK, _TBASIC._INTPRTR.PROGCNTR) -- save current line number 519 | _fngoto(linenum) 520 | end 521 | 522 | local function _fnreturn() 523 | if #_TBASIC._INTPRTR.CALLSTCK == 0 then -- nowhere to return 524 | _TBASIC._ERROR.RETURNWOSUB() 525 | return 526 | end 527 | 528 | local return_line = stackpop(_TBASIC._INTPRTR.CALLSTCK) + 1 -- the line has GOSUB, so advance one 529 | _fngoto(return_line) 530 | end 531 | 532 | local function _fnabort() 533 | _TBASIC._ERROR.ABORT() 534 | end 535 | 536 | local function _fnabortmsg(reason) 537 | _TBASIC._ERROR.ABORT(__checkstring(__readvar(reason))) 538 | end 539 | 540 | local function _fnif(bbool) 541 | local bool = __readvar(bbool) 542 | 543 | __assert(bool, "boolean") 544 | 545 | if bool == nil then 546 | _TBASIC._ERROR.ILLEGALARG() 547 | return 548 | end 549 | 550 | if not bool then 551 | _TBASIC._INTPRTR.PROGCNTR = _TBASIC._INTPRTR.PROGCNTR + 1 552 | end 553 | end 554 | 555 | local function _fnnop() 556 | return 557 | end 558 | 559 | local function _fnfor(seq) 560 | stackpush(_TBASIC._INTPRTR.CALLSTCK, _TBASIC._INTPRTR.PROGCNTR) 561 | end 562 | 563 | local function _fnnext(...) 564 | if #_TBASIC._INTPRTR.CALLSTCK == 0 then -- nowhere to return 565 | _TBASIC._ERROR.NEXTWOFOR() 566 | return 567 | end 568 | 569 | local variables = {...} -- array of strings(varname) e.g. "$X, $Y, $Z" 570 | 571 | -- error if no symbol is specified (a common "mistake") 572 | if #variables == 0 then 573 | _TBASIC._ERROR.NOSYMFORNEXT() 574 | end 575 | 576 | local branch = false 577 | -- dequeue intsequences 578 | for i, v in ipairs(variables) do 579 | local t = nil 580 | if _TBASIC.isvariable(v) then 581 | t = _TBASIC._INTPRTR.VARTABLE[v:sub(2, #v)] 582 | 583 | if type(t) ~= "table" then 584 | _TBASIC._ERROR.ILLEGALARG("ARRAY", type(t)) 585 | return 586 | end 587 | 588 | table.remove(t, 1) 589 | 590 | -- unassign variable 591 | if #t == 0 then 592 | _TBASIC._INTPRTR.VARTABLE[v] = nil 593 | branch = true 594 | end 595 | else 596 | _TBASIC._ERROR.ILLEGALARG("ARRAY", type(t)) 597 | return 598 | end 599 | end 600 | 601 | -- branch? or go back? 602 | if not branch then 603 | _fngoto(stackpeek(_TBASIC._INTPRTR.CALLSTCK) + 1) -- the line has FOR statement 604 | else 605 | stackpop(_TBASIC._INTPRTR.CALLSTCK) -- dump the stack 606 | end 607 | end 608 | 609 | local function _fnabs(n) 610 | return math.abs(__checknumber(n)) 611 | end 612 | 613 | local function _fnsin(n) 614 | return math.sin(__checknumber(n)) 615 | end 616 | 617 | local function _fncos(n) 618 | return math.cos(__checknumber(n)) 619 | end 620 | 621 | local function _fntan(n) 622 | return math.tan(__checknumber(n)) 623 | end 624 | 625 | local function _fntorad(n) 626 | return math.rad(__checknumber(n)) 627 | end 628 | 629 | local function _fnascii(char) 630 | return __checkstring(char):byte(1) 631 | end 632 | 633 | local function _fncbrt(n) 634 | return __checknumber(n)^3 635 | end 636 | 637 | local function _fnceil(n) 638 | return math.ceil(__checknumber(n)) 639 | end 640 | 641 | local function _fnchar(code) 642 | return "~"..string.char(__checknumber(code)) -- about "~".. ? read the cautions above! 643 | end 644 | 645 | local function _fnfloor(n) 646 | return math.floor(__checknumber(n)) 647 | end 648 | 649 | local function _fngetkeycode(...) 650 | -- TODO get a single character from the keyboard and saves the code of the character to the given variable(s) 651 | end 652 | 653 | local function _fnint(n) 654 | num = __checknumber(n) 655 | return num >= 0 and math.floor(n) or math.ceil(n) 656 | end 657 | 658 | local function _fnmultinv(n) -- multiplicative invert 659 | return 1.0 / __checknumber(n) 660 | end 661 | 662 | local function _fnsubstrleft(str, n) 663 | return "~"..__checkstring(str):sub(1, __checknumber(n)) 664 | end 665 | 666 | local function _fnsubstr(str, left, right) 667 | return "~"..__checkstring(str):sub(__checknumber(left), __checknumber(right)) 668 | end 669 | 670 | local function _fnsubstrright(str, n) 671 | return "~"..__checkstring(str):sub(-__checknumber(n)) 672 | end 673 | 674 | local function _fnlen(var) 675 | local value = __readvar(var) 676 | return #value 677 | end 678 | 679 | local function _fnloge(n) 680 | return math.log(__checknumber(n)) 681 | end 682 | 683 | local function _fnmax(...) 684 | local args = __resolvevararg(...) 685 | if #args < 1 then 686 | _TBASIC._ERROR.ARGMISSING("MAX") 687 | return 688 | end 689 | 690 | local max = -math.huge 691 | for _, i in ipairs(args) do 692 | local n = __checknumber(i) 693 | if max < n then max = n end 694 | end 695 | return max 696 | end 697 | 698 | local function _fnmin(...) 699 | local args = __resolvevararg(...) 700 | if #args < 1 then 701 | _TBASIC._ERROR.ARGMISSING("MIN") 702 | return 703 | end 704 | 705 | local min = math.huge 706 | for _, i in ipairs(args) do 707 | local n = __checknumber(i) 708 | if min > n then min = n end 709 | end 710 | return min 711 | end 712 | 713 | local function _fnrand() 714 | return math.random() 715 | end 716 | 717 | local function _fnround(n) 718 | return math.floor(__checknumber(n) + 0.5) 719 | end 720 | 721 | local function _fnsign(n) 722 | local num = __checknumber(n) 723 | return num > 0 and 1.0 or num < 0 and -1.0 or 0.0 724 | end 725 | 726 | local function _fnsqrt(n) 727 | return __checknumber(n)^(0.5) 728 | end 729 | 730 | local function _fntostring(n) 731 | local ret = tostring(__checknumber(n)) 732 | if not ret then 733 | _TBASIC._ERROR.ILLEGALARG() 734 | return 735 | else 736 | return "~"..ret 737 | end 738 | end 739 | 740 | local function _fntonumber(s) 741 | if tonumber(s) then return s end 742 | return tonumber(__checkstring(s)) 743 | end 744 | 745 | local function _fntan(n) 746 | return math.tan(__checknumber(n)) 747 | end 748 | 749 | local function _fninput(...) -- INPUT(var1, [var2, var3 ...]) 750 | local args = {...} 751 | local prompt = "YOUR INPUT ? " 752 | local prompt_numbered = "YOUR INPUT (%d OF %d) ? " 753 | 754 | function prompt_and_get_input() 755 | -- if there's two or more input, a number will be shown 756 | if #args >= 2 then 757 | io.write(string.format(prompt_numbered, argcount, #args)) 758 | else 759 | io.write(prompt) 760 | end 761 | io.flush() -- print out the line right away 762 | 763 | local value = io.read() 764 | 765 | return value 766 | end 767 | 768 | if #args < 1 then 769 | _TBASIC._ERROR.ARGMISSING("INPUT") 770 | return 771 | else 772 | for argcount, varname in ipairs(args) do 773 | local inputvalue = nil 774 | while inputvalue == nil or inputvalue == "" do 775 | inputvalue = prompt_and_get_input() 776 | _opassign(varname, inputvalue) 777 | end 778 | end 779 | end 780 | end 781 | 782 | local function _fnlabel(lname) 783 | _TBASIC._INTPRTR.LINELABL[__checkstring(lname)] = _TBASIC._INTPRTR.PROGCNTR 784 | end 785 | 786 | -- dim(max_index, max_index, ...) 787 | local function _fndim(...) 788 | local args = {...} 789 | local varname = args[1] 790 | local dimensional = {} 791 | for i, v in ipairs(args) do 792 | if i > 1 then 793 | dimensional[i - 1] = v + 1 -- stores size, not max_index 794 | end 795 | end 796 | 797 | _opassign(varname, __makenewtbasicarray(dimensional)) 798 | end 799 | 800 | local function _fnassignarray(arrname, value, ...) 801 | gfnarrayset(arrname, value, ...) 802 | end 803 | 804 | local function _fnreadarray(arrname, ...) 805 | return gfnarrayget(arrname, ...) 806 | end 807 | 808 | 809 | 810 | -- OPERATOR IMPLEMENTS -------------------------------------------------------- 811 | 812 | local function booleanise(bool) 813 | return bool and "$TRUE" or "$FALSE" 814 | end 815 | 816 | function _opconcat(llval, rrval) 817 | local lval = __readvar(llval) 818 | local rval = __readvar(rrval) 819 | 820 | if type(lval) == "function" then _TBASIC._ERROR.ILLEGALARG("VALUE", "FUNCTION") return end 821 | if type(rval) == "function" then _TBASIC._ERROR.ILLEGALARG("VALUE", "FUNCTION") return end 822 | 823 | local l = (type(lval) == "string" and lval:byte(1)) == 126 and lval:sub(2, #lval) or __checkstring(lval) 824 | local r = (type(rval) == "string" and rval:byte(1)) == 126 and rval:sub(2, #rval) or __checkstring(rval) 825 | 826 | return "~"..l..r 827 | end 828 | 829 | function _opplus(lval, rval) 830 | local l = __checknumber(lval) 831 | local r = __checknumber(rval) 832 | 833 | return l + r 834 | end 835 | 836 | function _optimes(lval, rval) 837 | local l = __checknumber(lval) 838 | local r = __checknumber(rval) 839 | 840 | return l * r 841 | end 842 | 843 | function _opminus(lval, rval) 844 | local l = __checknumber(lval) 845 | local r = __checknumber(rval) 846 | 847 | return l - r 848 | end 849 | 850 | function _opdiv(lval, rval) 851 | local l = __checknumber(lval) 852 | local r = __checknumber(rval) 853 | 854 | if r == 0 then 855 | _TBASIC._ERROR.DIV0() 856 | return 857 | else 858 | return _optimes(l, 1.0 / r) 859 | end 860 | end 861 | 862 | function _opmodulo(lval, rval) 863 | local expected = "number" 864 | local l = __checknumber(lval) 865 | local r = __checknumber(rval) 866 | 867 | return math.fmod(l, r) 868 | end 869 | 870 | function _oppower(lval, rval) 871 | local expected = "number" 872 | local l = __checknumber(lval) 873 | local r = __checknumber(rval) 874 | 875 | return math.pow(l, r) -- 0^0 is 1 according to the spec, and so is the Lua's. 876 | end 877 | 878 | function _opassign(var, value) 879 | if _TBASIC.isnumber(var) or _TBASIC.isfunction(var) or _TBASIC.isoperator(var) or _TBASIC.isargsep(var) then 880 | _TBASIC._ERROR.ILLEGALNAME(var) 881 | return 882 | end 883 | 884 | -- remove uncaught "$" 885 | local varname = var:byte(1) == 36 and var:sub(2, #var) or var 886 | 887 | -- if it still has "$", the programmer just broke the law 888 | if varname:byte(1) == 36 then 889 | _TBASIC._ERROR.ILLEGALNAME(varname, "HAS ILLEGAL CHARACTER '$'") 890 | return 891 | end 892 | 893 | _TBASIC._INTPRTR.VARTABLE[varname:upper()] = __readvar(value) 894 | end 895 | 896 | function _opeq(llval, rrval) 897 | local lval = __readvar(llval) 898 | local rval = __readvar(rrval) 899 | 900 | if tonumber(lval) and tonumber(rval) then 901 | return booleanise(tonumber(lval) == tonumber(rval)) 902 | else 903 | return booleanise(__checkstring(lval) == __checkstring(rval)) 904 | end 905 | end 906 | 907 | function _opne(llval, rrval) 908 | local lval = __readvar(llval) 909 | local rval = __readvar(rrval) 910 | 911 | if tonumber(lval) and tonumber(rval) then 912 | return booleanise(tonumber(lval) ~= tonumber(rval)) 913 | else 914 | return booleanise(__checkstring(lval) ~= __checkstring(rval)) 915 | end 916 | end 917 | 918 | function _opgt(lval, rval) 919 | local expected = "number" 920 | local l = __checknumber(lval) 921 | local r = __checknumber(rval) 922 | 923 | return booleanise(l > r) 924 | end 925 | 926 | function _oplt(lval, rval) 927 | local expected = "number" 928 | local l = __checknumber(lval) 929 | local r = __checknumber(rval) 930 | 931 | return booleanise(l < r) 932 | end 933 | 934 | function _opge(lval, rval) 935 | local expected = "number" 936 | local l = __checknumber(lval) 937 | local r = __checknumber(rval) 938 | 939 | return booleanise(l >= r) 940 | end 941 | 942 | function _ople(lval, rval) 943 | local expected = "number" 944 | local l = __checknumber(lval) 945 | local r = __checknumber(rval) 946 | 947 | return booleanise(l <= r) 948 | end 949 | 950 | function _opband(lval, rval) 951 | local expected = "number" 952 | local l = __checknumber(lval) 953 | local r = __checknumber(rval) 954 | 955 | return bit.band(l, r) 956 | end 957 | 958 | function _opbor(lval, rval) 959 | local expected = "number" 960 | local l = __checknumber(lval) 961 | local r = __checknumber(rval) 962 | 963 | return bit.bor(l, r) 964 | end 965 | 966 | function _opbxor(lval, rval) 967 | local expected = "number" 968 | local l = __checknumber(lval) 969 | local r = __checknumber(rval) 970 | 971 | return bit.bxor(l, r) 972 | end 973 | 974 | function _opbnot(val) 975 | local expected = "number" 976 | local v = __checknumber(val) 977 | 978 | return bit.bnot(v) 979 | end 980 | 981 | function _oplshift(lval, rval) 982 | local expected = "number" 983 | local l = __checknumber(lval) 984 | local r = __checknumber(rval) 985 | 986 | return bit.lshift(l, r) 987 | end 988 | 989 | function _oprshift(lval, rval) 990 | local expected = "number" 991 | local l = __checknumber(lval) 992 | local r = __checknumber(rval) 993 | 994 | return bit.arshift(l, r) 995 | end 996 | 997 | function _opurshift(lval, rval) 998 | local expected = "number" 999 | local l = __checknumber(lval) 1000 | local r = __checknumber(rval) 1001 | 1002 | return bit.rshift(l, r) 1003 | end 1004 | 1005 | function _opland(lhand, rhand) 1006 | return booleanise(__readvar(lhand) and __readvar(rhand)) 1007 | end 1008 | 1009 | function _oplor(lhand, rhand) 1010 | return booleanise(__readvar(lhand) or __readvar(rhand)) 1011 | end 1012 | 1013 | function _oplnot(rhand) 1014 | return booleanise(not __readvar(rhand)) 1015 | end 1016 | 1017 | function _opintrange(x, y) -- x TO y -> {x..y} 1018 | local from = __checknumber(x) 1019 | local to = __checknumber(y) 1020 | 1021 | local seq = {} 1022 | if from < to then 1023 | for i = from, to do 1024 | table.insert(seq, i) 1025 | end 1026 | else 1027 | for i = from, to, -1 do 1028 | table.insert(seq, i) 1029 | end 1030 | end 1031 | 1032 | return seq 1033 | end 1034 | 1035 | function _opintrangestep(sseq, sstp) -- i know you can just use "for i = from, to, step" 1036 | local seq = __readvar(sseq) 1037 | local stp = __readvar(sstp) 1038 | local step = __checknumber(stp) -- but that's just how not this stack machine works... 1039 | __assert(seq, "table") 1040 | 1041 | if step == 1 then return seq end 1042 | if step < 1 then _TBASIC._ERROR.ILLEGALARG() return end 1043 | 1044 | local newseq = {} 1045 | for i, v in ipairs(seq) do 1046 | if i % step == 1 then 1047 | table.insert(newseq, v) 1048 | end 1049 | end 1050 | 1051 | return newseq 1052 | end 1053 | 1054 | function _opunaryminus(n) 1055 | local num = __checknumber(n) 1056 | return -num 1057 | end 1058 | 1059 | function _opplusassign(var, value) 1060 | if type(__readvar(var)) == "number" then 1061 | _opassign(var, __readvar(var) + __checknumber(value)) 1062 | else 1063 | _TBASIC._ERROR.ILLEGALARG() 1064 | return 1065 | end 1066 | end 1067 | 1068 | function _opminusassign(var, value) 1069 | if type(__readvar(var)) == "number" then 1070 | _opassign(var, __readvar(var) - __checknumber(value)) 1071 | else 1072 | _TBASIC._ERROR.ILLEGALARG() 1073 | return 1074 | end 1075 | end 1076 | 1077 | function _optimesassign(var, value) 1078 | if type(__readvar(var)) == "number" then 1079 | _opassign(var, __readvar(var) * __checknumber(value)) 1080 | else 1081 | _TBASIC._ERROR.ILLEGALARG() 1082 | return 1083 | end 1084 | end 1085 | 1086 | function _opdivassign(var, value) 1087 | if type(__readvar(var)) == "number" then 1088 | if __checknumber(value) == 0 then 1089 | _TBASIC._ERROR.DIV0() 1090 | return 1091 | else 1092 | _opassign(var, __readvar(var) / __checknumber(value)) 1093 | end 1094 | else 1095 | _TBASIC._ERROR.ILLEGALARG() 1096 | return 1097 | end 1098 | end 1099 | 1100 | function _opmodassign(var, value) 1101 | if type(__readvar(var)) == "number" then 1102 | if __checknumber(value) == 0 then 1103 | _TBASIC._ERROR.DIV0() 1104 | return 1105 | else 1106 | _opassign(var, math.fmod(__readvar(var), __checknumber(value))) 1107 | end 1108 | else 1109 | _TBASIC._ERROR.ILLEGALARG() 1110 | return 1111 | end 1112 | end 1113 | 1114 | 1115 | local vararg = -13 -- magic 1116 | 1117 | _G._TBASIC.LUAFN = { 1118 | -- variable control 1119 | CLR = {function() _TBASIC._INTPRTR.VARTABLE = {} end, 0}, 1120 | DIM = {_fndim, vararg}, 1121 | ASSIGNARRAY = {_fnassignarray, vararg}, 1122 | READARRY = {_fnreadarray, vararg}, 1123 | -- flow control 1124 | IF = {_fnif, 1}, 1125 | THEN = {_fnnop, 0}, 1126 | GOTO = {_fngoto, 1}, 1127 | GOSUB = {_fngosub, 1}, 1128 | RETURN = {_fnreturn, 0}, 1129 | END = {function() _G._TBASIC.__appexit = true end, 0}, 1130 | ABORT = {_fnabort, 0}, 1131 | ABORTM = {_fnabortmsg, 1}, 1132 | FOR = {_fnfor, 1}, 1133 | NEXT = {_fnnext, vararg}, 1134 | LABEL = {_fnlabel, 1}, 1135 | -- stdio 1136 | PRINT = {_fnprint, vararg}, 1137 | INPUT = {_fninput, vararg}, 1138 | -- mathematics 1139 | ABS = {_fnabs, 1}, 1140 | CBRT = {_fncbrt, 1}, 1141 | CEIL = {_fnceil, 1}, 1142 | COS = {_fncos, 1}, 1143 | FLOOR = {_fnfloor, 1}, 1144 | INT = {_fnint, 1}, 1145 | INV = {_fnmultinv, 1}, 1146 | LOG = {_fnloge, 1}, 1147 | MAX = {_fnmax, vararg}, 1148 | MIN = {_fnmin, vararg}, 1149 | RAD = {_fntorad, 1}, 1150 | RND = {_fnrand, 0}, 1151 | ROUND = {_fnround, 1}, 1152 | SGN = {_fnsign, 1}, 1153 | SIN = {_fnsin, 1}, 1154 | SQRT = {_fnsqrt, 1}, 1155 | TAN = {_fntan, 1}, 1156 | -- string manipulation 1157 | LEFT = {_fnsubstrleft, 2}, 1158 | LEN = {_fnlen, 1}, 1159 | MID = {_fnsubstr, 3}, 1160 | RIGHT = {_fnsubstrright, 2}, 1161 | -- type conversion 1162 | ASC = {_fnascii, 1}, 1163 | CHR = {_fnchar, 1}, 1164 | STR = {_fntostring, 1}, 1165 | VAL = {_fntonumber, 1}, 1166 | --------------- 1167 | -- operators -- 1168 | --------------- 1169 | [";"] = {_opconcat, 2}, 1170 | ["+"] = {_opplus, 2}, 1171 | ["*"] = {_optimes, 2}, 1172 | ["-"] = {_opminus, 2}, 1173 | ["/"] = {_opdiv, 2}, 1174 | ["%"] = {_opmodulo, 2}, 1175 | ["^"] = {_oppower, 2}, 1176 | ["=="] = {_opeq, 2}, 1177 | ["!="] = {_opne, 2}, ["<>"] = {_opne, 2}, ["><"] = {_opne, 2}, 1178 | [">="] = {_opge, 2}, ["=>"] = {_opge, 2}, 1179 | ["<="] = {_ople, 2}, ["=<"] = {_ople, 2}, 1180 | [">"] = {_opgt, 2}, 1181 | ["<"] = {_oplt, 2}, 1182 | ["="] = {_opassign, 2}, [":="] = {_opassign, 2}, 1183 | ["+="] = {_opplusassign, 2}, ["-="] = {_opminusassign, 2}, 1184 | ["*="] = {_optimesassign, 2}, ["/="] = {_opdivassign, 2}, ["%="] = {_opmodassign, 2}, 1185 | MINUS = {_opunaryminus, 1}, 1186 | -- logical operators 1187 | AND = {_opland, 2}, 1188 | OR = {_oplor, 2}, 1189 | NOT = {_oplnot, 1}, 1190 | -- bit operators 1191 | ["<<"] = {_oplshift, 2}, 1192 | [">>"] = {_oprshift, 2}, -- bit.arshift 1193 | [">>>"] = {_opurshift, 2}, -- bit.rshift 1194 | ["|"] = {_opbor, 2}, 1195 | ["&"] = {_opband, 2}, 1196 | ["!"] = {_opbnot, 1}, 1197 | XOR = {_opbxor, 2}, 1198 | -- int sequence 1199 | TO = {_opintrange, 2}, 1200 | STEP = {_opintrangestep, 2}, 1201 | -- misc 1202 | REM = {_fnnop, 0} 1203 | } 1204 | _G._TBASIC._GETARGS = function(func) 1205 | local f = _TBASIC.LUAFN[func] 1206 | if f == nil then return nil end 1207 | return f[2] 1208 | end 1209 | 1210 | 1211 | 1212 | -- PARSER IMPL ---------------------------------------------------------------- 1213 | 1214 | local opprecedence = { 1215 | {":=", "=", "+=", "-=", "*=", "/=", "%="}, -- least important 1216 | {"OR"}, 1217 | {"AND"}, 1218 | {"|"}, 1219 | {"XOR"}, 1220 | {"&"}, 1221 | {"==", "!=", "<>", "><"}, 1222 | {"<=", ">=", "=<", "=>", "<", ">"}, 1223 | {"TO", "STEP"}, 1224 | {">>>", "<<", ">>"}, 1225 | {";"}, 1226 | {"+", "-"}, 1227 | {"*", "/", "%"}, 1228 | {"NOT", "!"}, 1229 | {"^"}, -- most important 1230 | {"MINUS"} 1231 | } 1232 | local opassoc = { 1233 | rtl = {";", "^", "NOT", "!"} 1234 | } 1235 | local function exprerr(token) 1236 | _TBASIC._ERROR.SYNTAXAT(token) 1237 | end 1238 | function _op_precd(op) 1239 | -- take care of prematurely prepended '#' 1240 | local t1 = op:byte(1) == 35 and op:sub(2, #op) or op 1241 | op = t1:upper() 1242 | 1243 | for i = 1, #opprecedence do 1244 | for _, op_in_quo in ipairs(opprecedence[i]) do 1245 | if op == op_in_quo then 1246 | return i 1247 | end 1248 | end 1249 | end 1250 | exprerr("precedence of "..op) 1251 | end 1252 | 1253 | function _op_isrtl(op) 1254 | for _, v in ipairs(opassoc.rtl) do 1255 | if op == v then return true end 1256 | end 1257 | return false 1258 | end 1259 | 1260 | function _op_isltr(op) 1261 | return not _op_isrtl(op) 1262 | end 1263 | 1264 | 1265 | 1266 | function _G._TBASIC.isnumber(token) 1267 | return tonumber(token) and true or false 1268 | end 1269 | 1270 | function _G._TBASIC.isoperator(token) 1271 | if token == nil then return false end 1272 | 1273 | -- take care of prematurely prepended '#' 1274 | local t1 = token:byte(1) == 35 and token:sub(2, #token) or token 1275 | token = t1 1276 | 1277 | for _, tocheck in ipairs(_TBASIC._OPERATR) do 1278 | if tocheck == token:upper() then return true end 1279 | end 1280 | return false 1281 | end 1282 | 1283 | function _G._TBASIC.isvariable(word) 1284 | if type(word) == "number" then return false end 1285 | if type(word) == "boolean" then return true end 1286 | if type(word) == "table" then return true end 1287 | if word == nil then return false end 1288 | return word:byte(1) == 36 1289 | end 1290 | 1291 | function _G._TBASIC.isargsep(token) 1292 | return token == "," 1293 | end 1294 | 1295 | function _G._TBASIC.isfunction(token) 1296 | if token == nil then return false end 1297 | 1298 | -- take care of prematurely prepended '&' 1299 | local t1 = token:byte(1) == 38 and token:sub(2, #token) or token 1300 | token = t1 1301 | 1302 | -- try for builtin 1303 | local cmpval = function(table_elem) return string.hash(table_elem) end 1304 | 1305 | local found = table.binsearch(_TBASIC._FNCTION, token, cmpval) 1306 | 1307 | if found then 1308 | return true 1309 | end 1310 | 1311 | -- try for user-defined functions 1312 | found = table.binsearch(_TBASIC._INTPRTR.FNCTABLE, token, cmpval) 1313 | if found then -- found is either Table or Nil. We want boolean value. 1314 | return true 1315 | else 1316 | return false 1317 | end 1318 | end 1319 | 1320 | function _G._TBASIC.isstring(token) 1321 | if type(token) ~= "string" then return false end 1322 | return token:byte(1) == 126 1323 | end 1324 | 1325 | function _G._TBASIC.isarray(token) 1326 | if token:byte(1) == 37 then 1327 | return true 1328 | else 1329 | local var = __readvar("%"..token) 1330 | return type(var) == "table" and var.identifier == "tbasicarray" 1331 | end 1332 | end 1333 | 1334 | 1335 | 1336 | local function printdbg(...) 1337 | local debug = false 1338 | if debug then print("TBASINCL", ...) end 1339 | end 1340 | 1341 | 1342 | -- implementation of the Shunting Yard algo 1343 | _G._TBASIC.TORPN = function(exprarray) 1344 | local stack = {} 1345 | local outqueue = {} 1346 | 1347 | local loophookkeylist = {} 1348 | local function infloophook(key) 1349 | if not _G[key] then 1350 | _G[key] = 0 1351 | table.insert(loophookkeylist, key) 1352 | end 1353 | _G[key] = _G[key] + 1 1354 | 1355 | if _G[key] > 50000 then 1356 | error(key..": too long without yielding") 1357 | end 1358 | end 1359 | 1360 | local isfunction = _TBASIC.isfunction 1361 | local isoperator = _TBASIC.isoperator 1362 | local isargsep = _TBASIC.isargsep 1363 | local isnumber = _TBASIC.isnumber 1364 | local isstring = _TBASIC.isstring 1365 | local isarray = _TBASIC.isarray 1366 | 1367 | for _, token in ipairs(exprarray) do--expr:gmatch("[^ ]+") do 1368 | if token == nil then error("Token is nil!") end 1369 | 1370 | -- hack: remove single prepended whitespace 1371 | t1 = token:byte(1) == 32 and token:sub(2, #token) or token 1372 | token = t1 1373 | 1374 | printdbg("TOKEN", "'"..token.."'") 1375 | if isfunction(token:upper()) then 1376 | printdbg("is function") 1377 | 1378 | stackpush(stack, "&"..token:upper()) 1379 | elseif isarray(token:upper()) then 1380 | printdbg("is array") 1381 | 1382 | stackpush(stack, "%"..token:upper()) 1383 | elseif isargsep(token) then 1384 | printdbg("is argument separator") 1385 | 1386 | if not (stackpeek(stack) == "(" or #stack == 0) then 1387 | repeat 1388 | stackpush(outqueue, stackpop(stack)) 1389 | 1390 | infloophook("repeat1") 1391 | until stackpeek(stack) == "(" or #stack == 0 1392 | end 1393 | -- no left paren encountered, ERROR! 1394 | if #stack == 0 then exprerr(token) end -- misplaces sep or mismatched parens 1395 | elseif isoperator(token) then 1396 | printdbg("is operator") 1397 | 1398 | local o1 = token 1399 | 1400 | while isoperator(stackpeek(stack)) and ( 1401 | (_op_isltr(o1) and _op_precd(o1) <= _op_precd(stackpeek(stack))) or 1402 | (_op_isrtl(o1) and _op_precd(o1) < _op_precd(stackpeek(stack))) 1403 | ) do 1404 | local o2 = stackpeek(stack) 1405 | 1406 | printdbg("--> push o2 to stack, o2:", o2) 1407 | 1408 | stackpop(stack) -- drop 1409 | stackpush(outqueue, (o2:byte(1) == 35) and o2 or "#"..o2:upper()) -- try to rm excess '#' 1410 | 1411 | infloophook("while") 1412 | end 1413 | 1414 | stackpush(stack, "#"..o1:upper()) 1415 | elseif token == "(" then 1416 | stackpush(stack, token) 1417 | elseif token == ")" then 1418 | while stackpeek(stack) ~= "(" do 1419 | if #stack == 0 then 1420 | exprerr(token) 1421 | end 1422 | 1423 | printdbg("--> stack will pop", stackpeek(stack)) 1424 | 1425 | stackpush(outqueue, stackpop(stack)) 1426 | 1427 | infloophook("") 1428 | end 1429 | 1430 | printdbg("--> will drop", stackpeek(stack), "(should be left paren!)") 1431 | 1432 | --[[found_left_paren = false 1433 | if stackpeek(stack) ~= "(" then 1434 | exprerr(token) 1435 | else 1436 | found_left_paren = true 1437 | end]] 1438 | stackpop(stack) -- drop 1439 | 1440 | printdbg("--> stack peek after drop", stackpeek(stack)) 1441 | 1442 | if isfunction(stackpeek(stack)) then 1443 | printdbg("--> will enq fn", stackpeek(stack)) 1444 | stackpush(outqueue, stackpop(stack)) 1445 | end 1446 | printdbg("--> STACKTRACE_ITMD", table.concat(stack, " ")) 1447 | printdbg("--> OUTPUT_ITMD", table.concat(outqueue, " ")) 1448 | 1449 | -- stack empty without finding left paren, ERROR! 1450 | --if not found_left_paren and #stack == 0 then exprerr(token) end -- mismatched parens 1451 | elseif isstring(token) or isnumber(token) then 1452 | printdbg("is data") 1453 | stackpush(outqueue, token) -- arbitrary data 1454 | else -- a word without '~' or anything; assume it's a variable name 1455 | printdbg("is variable") 1456 | stackpush(outqueue, "$"..token:upper()) 1457 | end 1458 | printdbg("STACKTRACE", table.concat(stack, " ")) 1459 | printdbg("OUTPUT", table.concat(outqueue, " ")) 1460 | printdbg() 1461 | end 1462 | 1463 | while #stack > 0 do 1464 | if stackpeek(stack) == "(" or stackpeek(stack) == ")" then 1465 | exprerr("(paren)") -- mismatched parens 1466 | end 1467 | stackpush(outqueue, stackpop(stack)) 1468 | 1469 | infloophook("while3") 1470 | end 1471 | 1472 | printdbg("FINAL RESULT: "..table.concat(outqueue, " ")) 1473 | 1474 | for _, key in ipairs(loophookkeylist) do 1475 | _G[key] = nil 1476 | end 1477 | 1478 | return outqueue 1479 | end 1480 | 1481 | 1482 | -- INIT ----------------------------------------------------------------------- 1483 | 1484 | -- load extensions 1485 | local status, err = pcall( 1486 | function() 1487 | if os and os.loadAPI then -- ComputerCraft 1488 | os.loadAPI "TBASEXTN.lua" 1489 | else 1490 | require "TBASEXTN" 1491 | end 1492 | end 1493 | ) 1494 | if err then 1495 | error(err) 1496 | end 1497 | 1498 | 1499 | --sort builtin keywords list 1500 | table.sort(_TBASIC._FNCTION, function(a, b) return string.hash(a) < string.hash(b) end) 1501 | 1502 | 1503 | _G._TBASIC._INTPRTR.RESET() 1504 | 1505 | 1506 | 1507 | --[[ 1508 | Terran BASIC (TBASIC) 1509 | Copyright (c) 2016-2017 Torvald (minjaesong) and the contributors. 1510 | 1511 | Permission is hereby granted, free of charge, to any person obtaining a copy of 1512 | this software and associated documentation files (the Software), to deal in the 1513 | Software without restriction, including without limitation the rights to use, 1514 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the 1515 | Software, and to permit persons to whom the Software is furnished to do so, 1516 | subject to the following conditions: 1517 | 1518 | The above copyright notice and this permission notice shall be included in all 1519 | copies or substantial portions of the Software. 1520 | 1521 | THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 1522 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 1523 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 1524 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 1525 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 1526 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 1527 | SOFTWARE. 1528 | ]] 1529 | --------------------------------------------------------------------------------