├── _config.yml ├── LICENSE ├── README.md └── paloaltobasic.rb /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 pankuznetsov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PaloAlto BASIC in Ruby less then in 500 lines 2 | __PaloAlto BASIC__ (also known as __Tiny BASIC__) interpreter less than in 500 lines writen in Ruby. 3 | The original program takes only 3KB of ROM and runs on Intel 8080, Motorola 6800 and MOS Technology 6502 processors. 4 | 5 | This BASIC understands a few statements and all variables there are 16 or 32-bit integers. 6 | 7 | [Take a look on Wikipedia page](https://en.wikipedia.org/wiki/Tiny_BASIC). 8 | 9 | # Just take a look on some __code examples__ 10 | 11 | __Print statement__: `PRINT "HELLO WORLD"`, `PRINT A` or `PRINT "X: ", X, ", Y: ", Y, ", Z: ", Z` 12 | 13 | __Input staement__: `INPUT A`, or you can use list of variables: `INPUT A, B, C` 14 | 15 | __Variable defenition/assigment__: `LET A = 10`, `LET B = (2 + 6) * 4` or `LET C = (A * B) + (B / A) - 1` 16 | 17 | __Goto__: `10 GOTO 10` is endless cycle. `GOTO A * 10 + 230` is also possible. 18 | 19 | __If__: `IF A = B THEN PRINT "EQUALITY"`, `IF A > B * 2 THEN LET C = C + (B - A)` or `IF A <> B THEN PRINT "UNEQUALITY"` 20 | 21 | __GoSub__: `10 IF A = 1 THEN GOSUB 50` ... `50 LET X = X * X` ... `60 RETURN` 22 | 23 | >__*Important Note*__: To run the program just type in `RUN` and hit ``! 24 | 25 | 26 | # EBNF Grammer 27 | 28 | The grammer is also pretty simple and can be described in EBNF just in a very few lines: 29 | 30 | 31 | CR _stands for Carret Return (`\r\n` or `\n`, depends on OS)_ 32 | 33 | __empty__ _stands for nothing._ 34 | 35 | __space__ _stands for any white space _ASCII_ symbol._ 36 | 37 | (__...__)* _means that ... may be repeated zero or more times._ 38 | 39 | 40 | __digit__ ::= _0_ | _1_ | _2_ | _3_ | _4_ | _5_ | _6_ | _7_ | _8_ | _9_ 41 | 42 | __lowercase__ ::= _a_ | _b_ | _c_ ... _x_ | _y_ | _z_ 43 | 44 | __uppercase__ ::= _A_ | _B_ | _C_ ... _X_ | _Y_ | _Z_ 45 | 46 | __letter__ ::= __lowercase__ | __uppercase__ 47 | 48 | __number__ ::= __digit__ (__digit__)* 49 | 50 | __string__ ::= _"_ (__letter__ | __digit__ | __space__)* _"_ 51 | 52 | __var__ ::= __uppercase__ 53 | 54 | __var-list__ ::= __var__ (_,_ __var__)* 55 | 56 | __factor__ ::= __var__ | __number__ | _(_ __expression__ _)_ 57 | 58 | __term__ ::= __factor__ ((_*_ | _/_) __factor__)* 59 | 60 | __expression__ ::= (_+_ | _-_ | __empty__) __term__ ((_+_ | _-_) __term__)* 61 | 62 | __expression-list__ ::= (__expression__ | __string__) (_,_ __expression__ | __string__)* 63 | 64 | __ralational-operator__ ::= (_<_ (_>_ | _=_ | __empty__)) | (_>_ (_<_ | _=_ | __empty__)) | _=_ 65 | 66 | __statement__ ::= (_PRINT_ __expression-list__) 67 | | (_INPUT_ __var-list__) 68 | | (_LET_ __var__ _=_ __expression__) 69 | | (_GOTO_ __expression__) 70 | | (_GOSUB_ __expression__) 71 | | (_RETURN_) 72 | | (_IF_ __expression__ __relational-operator__ __expression__ _THEN_ __statement__) 73 | | (_END_) 74 | 75 | __line__ ::= ((__number__ __statement__) | (__statement__)) CR 76 | 77 | 78 | --- 79 | 80 | 81 | To __run the programm__ just download _paloaltobasic.rb_ and run it using `ruby paloaltobasic.rb`. Program works stable on Ruby version __2.6__. 82 | 83 | >__Attention!__ Program works unstable under *GIT Bash*. Try using some other terminal (ex. *Windows __CMD__*) 84 | 85 | 86 | --- 87 | 88 | 89 | Contact me via e-mail `kuznetsovsa_user@protonmail.com`. 90 | -------------------------------------------------------------------------------- /paloaltobasic.rb: -------------------------------------------------------------------------------- 1 | $code = '' # Line for execution 2 | $line_no = 0 # Line number 3 | $base = 0 # Points first non-parsed symbol 4 | $vars = { } # Variables hash table 5 | $lines = [ ] # Lines of program 6 | $return_stack = [ ] # Call stack 7 | $run = true # False if END 8 | 9 | def peek(oft) 10 | if oft < 0 or oft >= $code.size then return nil end 11 | return $code[$base + oft] 12 | end 13 | 14 | def letter?(symb) 15 | return symb.match?(/[[:alpha:]]/) 16 | end 17 | 18 | def numeric?(symb) 19 | return symb.match?(/[[0-9\.!#\-]]/) 20 | end 21 | 22 | def skip_trash # Function skipping whitespaces. 23 | i = 0 24 | while $base + i < $code.size and peek(i).match?(/\s/) do 25 | i += 1 26 | end 27 | $base += i 28 | return i 29 | end 30 | 31 | def skip_to_newline # Call it when the rest of line makes no sense 32 | i = 0 33 | while $base + i < $code.size and peek(i) != "\n" do 34 | i += 1 35 | end 36 | $base += i 37 | return i 38 | end 39 | 40 | def substring_at_place(idx, substr, ignore_case) # Checks for string in certan place 41 | if !ignore_case 42 | return $code[idx..(idx + substr.size - 1)] == substr 43 | else 44 | return $code[idx..(idx + substr.size - 1)].downcase == substr.downcase 45 | end 46 | end 47 | 48 | def parse_number # Function for parsing integer 49 | n = '' 50 | i = 0 51 | while $base + i < $code.size and numeric?(peek(i)) 52 | n += peek(i) 53 | i += 1 54 | end 55 | $base += i 56 | skip_trash 57 | return n.to_i 58 | end 59 | 60 | def parse_string # Returns string content between double quotes 61 | i = 1 62 | str = '' 63 | while $base + i < $code.size and peek(i) != '"' do 64 | if $base + i >= $code.size then 65 | raise 'Unexpected EOF: " was Expected' 66 | return nil 67 | end 68 | str += peek(i) 69 | i += 1 70 | end 71 | $base += str.size + 2 72 | skip_trash 73 | return str 74 | end 75 | 76 | def get_var # Parsing veriable 77 | if letter?(peek(0)) and peek(0).upcase == peek(0) then 78 | v = $vars[peek(0)].to_i 79 | $base += 1 80 | return v 81 | else 82 | return nil 83 | end 84 | end 85 | 86 | def get_var_list # Parsing list of variables 87 | vrs = [] 88 | if letter?(peek(0)) and peek(0).upcase == peek(0) then 89 | vrs << peek(0) 90 | $base += 1 91 | end 92 | skip_trash 93 | while peek(0) == ',' do 94 | $base += 1 95 | skip_trash 96 | if letter?(peek(0)) and peek(0).upcase == peek(0) then 97 | vrs << peek(0) 98 | $base += 1 99 | end 100 | skip_trash 101 | end 102 | return vrs 103 | end 104 | 105 | def get_relop # This pice of code works for determinating relation between expressions in IF block 106 | if peek(0) == '<' then 107 | if peek(1) == '>' then 108 | $base += 2 109 | return :LS_OR_GR 110 | elsif peek(1) == '=' then 111 | $base += 2 112 | return :LS_OR_EQ 113 | else 114 | $base += 1 115 | return :LS 116 | end 117 | elsif peek(0) == '>' then 118 | if peek(1) == '<' then 119 | $base += 2 120 | return :LS_OR_GR 121 | elsif peek(1) == '=' then 122 | $base += 2 123 | return :GR_OR_EQ 124 | else 125 | $base += 1 126 | return :GR 127 | end 128 | elsif peek(0) == '=' 129 | $base += 1 130 | return :EQ 131 | else 132 | return nil 133 | end 134 | end 135 | 136 | def exec_factor 137 | if letter?(peek(0)) and peek(0).upcase == peek(0) then 138 | return get_var 139 | elsif numeric?(peek(0)) then 140 | return parse_number 141 | elsif peek(0) == '(' 142 | $base += 1 143 | res = exec_expr 144 | if peek(0) == ')' 145 | $base += 1 146 | return res 147 | else 148 | raise ') Expected' 149 | end 150 | end 151 | return nil 152 | end 153 | 154 | def exec_term # Multiplication and division in expressions 155 | sum = exec_factor 156 | skip_trash 157 | while peek(0) == '/' or peek(0) == '*' do 158 | if peek(0) == '/' then 159 | $base += 1 160 | skip_trash 161 | sum /= exec_factor 162 | elsif peek(0) == '*' then 163 | $base += 1 164 | skip_trash 165 | sum *= exec_factor 166 | end 167 | skip_trash 168 | end 169 | return sum 170 | end 171 | 172 | def exec_expr # Addition and substraction in expressions 173 | pst = false 174 | ngt = false 175 | if peek(0) == '-' then 176 | pst = false 177 | ngt = true 178 | $base += 1 179 | end 180 | if peek(0) == '+' then 181 | pst = true 182 | ngt = false 183 | $base += 1 184 | end 185 | sum = 0 186 | f1 = exec_term 187 | if ngt == true then 188 | sum -= abs(f1) 189 | end 190 | if pst == true then 191 | sum += abs(f1) 192 | end 193 | if ngt == false and pst == false then 194 | sum += f1 195 | end 196 | skip_trash 197 | while peek(0) == '-' or peek(0) == '+' do 198 | if peek(0) == '-' then 199 | $base += 1 200 | skip_trash 201 | sum += -(exec_term.abs) 202 | elsif peek(0) == '+' then 203 | $base += 1 204 | skip_trash 205 | sum += exec_term.abs 206 | end 207 | skip_trash 208 | end 209 | skip_trash 210 | return sum 211 | end 212 | 213 | def exec_expr_list 214 | rests = [] 215 | if peek(0) == '"' then 216 | rests << parse_string 217 | else 218 | rests << exec_expr 219 | end 220 | skip_trash 221 | while peek(0) == ',' do 222 | $base += 1 223 | skip_trash 224 | if peek(0) == '"' then 225 | rests << parse_string 226 | else 227 | rests << exec_expr 228 | end 229 | skip_trash 230 | end 231 | return rests 232 | end 233 | 234 | def exec_print 235 | $base += 5 236 | skip_trash 237 | ress = exec_expr_list 238 | for i in 0...ress.size 239 | print ress[i] 240 | end 241 | $line_no += 1 242 | end 243 | 244 | def exec_input 245 | $base += 5 246 | skip_trash 247 | vrs = get_var_list 248 | for i in 0...vrs.size 249 | $vars[vrs[i]] = gets.chomp.to_i 250 | end 251 | $line_no += 1 252 | end 253 | 254 | def exec_let 255 | $base += 3 256 | skip_trash 257 | v = peek(0) 258 | if letter?(peek(0)) and peek(0).upcase == peek(0) then 259 | $base += 1 260 | skip_trash 261 | if peek(0) == '=' then 262 | $base += 1 263 | skip_trash 264 | $vars[v] = exec_expr 265 | else 266 | raise '= expected' 267 | end 268 | else 269 | raise 'Variable expected' 270 | end 271 | $line_no += 1 272 | end 273 | 274 | def exec_goto 275 | $base += 4 276 | skip_trash 277 | gl = exec_expr.to_i / 10 - 1 278 | if gl < $lines.size then 279 | $line_no = gl 280 | else 281 | raise 'No such line' 282 | end 283 | end 284 | 285 | def exec_if 286 | $base += 2 287 | skip_trash 288 | r1 = exec_expr 289 | skip_trash 290 | op = get_relop 291 | if op == nil then return 'Relational operator expected' end 292 | skip_trash 293 | r2 = exec_expr 294 | skip_trash 295 | if substring_at_place($base, "THEN", true) then 296 | $base += 4 297 | skip_trash 298 | if op == :LS_OR_GR and r1 != r2 then exec_statement 299 | elsif op == :LS_OR_EQ and r1 <= r2 then exec_statement 300 | elsif op == :LS and r1 < r2 then exec_statement 301 | elsif op == :GR and r1 > r2 then exec_statement 302 | elsif op == :GR_OR_EQ and r1 >= r2 then exec_statement 303 | elsif op == :EQ and r1 == r2 then exec_statement 304 | else 305 | if $line_no < $lines.size then 306 | $line_no += 1 307 | else 308 | $run = false 309 | end 310 | end 311 | else 312 | raise 'THEN expected' 313 | end 314 | end 315 | 316 | def exec_gosub 317 | $base += 5 318 | skip_trash 319 | gl = exec_expr.to_i / 10 - 1 320 | if gl < $lines.size then 321 | $return_stack << $line_no 322 | $line_no = gl 323 | else 324 | raise 'No such line' 325 | end 326 | end 327 | 328 | def exec_return 329 | $base += 6 330 | $line_no = $return_stack.last + 1 331 | if $return_stack.size > 0 then 332 | $return_stack.drop($return_stack.size - 1) 333 | else 334 | return 'Trying to RETURN, but stack is empty' 335 | end 336 | end 337 | 338 | def exec_statement 339 | if substring_at_place($base, "PRINT", true) then exec_print 340 | elsif substring_at_place($base, "LET", true) then exec_let 341 | elsif substring_at_place($base, "INPUT", true) then exec_input 342 | elsif substring_at_place($base, "GOTO", true) then exec_goto 343 | elsif substring_at_place($base, "IF", true) then exec_if 344 | elsif substring_at_place($base, "GOSUB", true) then exec_gosub 345 | elsif substring_at_place($base, "RETURN", true) then exec_return 346 | elsif substring_at_place($base, "END", true) then 347 | $run = false 348 | else 349 | raise 'Line does not contains valid statement' 350 | end 351 | $base = 0 352 | end 353 | 354 | def exec_prog(prog) 355 | ls = prog.split("\n") 356 | pl = 0 357 | for i in 0...ls.size 358 | $code = ls[i] 359 | $lines[pl] = $code[0, $code.size] 360 | pl += 1 361 | end 362 | $base = 0 363 | $line_no = 0 364 | last = $lines.size 365 | while $line_no != nil and $line_no < last 366 | if $run == true then 367 | $code = $lines[$line_no] 368 | skip_trash 369 | exec_statement 370 | else 371 | break 372 | end 373 | end 374 | end 375 | 376 | def repl # Read Eval Print Loop 377 | in_str = '' 378 | prog = '' 379 | n = 0 380 | while true do 381 | print (n + 1) * 10, "\t" 382 | in_str = gets.chomp 383 | if in_str != 'RUN' then 384 | prog += in_str + "\n" 385 | else 386 | break 387 | end 388 | n += 1 389 | end 390 | puts '----------------' 391 | exec_prog(prog) 392 | end 393 | 394 | def enterance 395 | puts 'PaloAlto BASIC interpreter made by Kuznetsov S. A., 2019' 396 | repl 397 | end 398 | 399 | enterance 400 | --------------------------------------------------------------------------------